Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ c788a761

History | View | Annotate | Download (42.1 kB)

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

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

    
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
from kamaki.cli.errors import (
43
    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
from kamaki.cli.commands import (
49
    _optional_output_cmd, _optional_json, _name_filter, _id_filter)
50

    
51

    
52
server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
53
flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
54
network_cmds = CommandTree('network', 'Cyclades/Compute API network commands')
55
ip_cmds = CommandTree('ip', 'Cyclades/Compute API floating ip commands')
56
_commands = [server_cmds, flavor_cmds, network_cmds, ip_cmds]
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
    '  PATH: local file to be injected (relative or absolute)',
67
    '  SERVER_PATH: destination location inside server Image',
68
    '  OWNER: virtual servers user id of the remote destination file',
69
    '  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
    wait_arguments = dict(
76
        progress_bar=ProgressBarArgument(
77
            'do not show progress bar', ('-N', '--no-progress-bar'), False)
78
    )
79

    
80
    def _wait(
81
            self, service, service_id, status_method, current_status,
82
            countdown=True, timeout=60):
83
        (progress_bar, wait_cb) = self._safe_progress_bar(
84
            '%s %s: status is still %s' % (
85
                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
                    service, service_id, new_mode))
94
            else:
95
                self.error('%s %s: status is still %s' % (
96
                    service, service_id, current_status))
97
        except KeyboardInterrupt:
98
            self.error('\n- canceled')
99
        finally:
100
            self._safe_progress_bar_finish(progress_bar)
101

    
102

    
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
            countdown=(current_status not in ('BUILD', )),
109
            timeout=timeout if current_status not in ('BUILD', ) else 100)
110

    
111

    
112
class _network_wait(_service_wait):
113

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

    
119

    
120
class _firewall_wait(_service_wait):
121

    
122
    def _wait(self, server_id, current_status, timeout=60):
123
        super(_firewall_wait, self)._wait(
124
            'Firewall of server',
125
            server_id, self.client.wait_firewall, current_status,
126
            timeout=timeout)
127

    
128

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

    
153
    def main(self):
154
        self._run()
155

    
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
    PERMANENTS = ('id', 'name')
164

    
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
        limit=IntArgument(
171
            '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
        enum=FlagArgument('Enumerate results', '--enumerate'),
176
        flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
177
        image_id=ValueArgument('filter by image id', ('--image-id')),
178
        user_id=ValueArgument('filter by user id', ('--user-id')),
179
        user_name=ValueArgument('filter by user name', ('--user-name')),
180
        status=ValueArgument(
181
            'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
182
            ('--status')),
183
        meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
184
        meta_like=KeyValueArgument(
185
            'print only if in key=value, the value is part of actual value',
186
            ('--metadata-like')),
187
    )
188

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

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

    
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
        return [srv for srv in servers if (
214
            '%s' % srv['image']['id'] == '%s' % fid)]
215

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

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

    
244
        servers = self._filter_by_name(servers)
245
        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
        if self['detail'] and not self['json_output']:
255
            servers = self._add_user_name(servers)
256
        elif not (self['detail'] or self['json_output']):
257
            remove_from_items(servers, 'links')
258
        if detail and not self['detail']:
259
            for srv in servers:
260
                for key in set(srv).difference(self.PERMANENTS):
261
                    srv.pop(key)
262
        kwargs = dict(with_enumeration=self['enum'])
263
        if self['more']:
264
            kwargs['out'] = StringIO()
265
            kwargs['title'] = ()
266
        if self['limit']:
267
            servers = servers[:self['limit']]
268
        self._print(servers, **kwargs)
269
        if self['more']:
270
            pager(kwargs['out'].getvalue())
271

    
272
    def main(self):
273
        super(self.__class__, self)._run()
274
        self._run()
275

    
276

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

    
287
    @errors.generic.all
288
    @errors.cyclades.connection
289
    @errors.cyclades.server_id
290
    def _run(self, server_id):
291
        vm = self.client.get_server_details(server_id)
292
        uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
293
        vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
294
        vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
295
        self._print(vm, self.print_dict)
296

    
297
    def main(self, server_id):
298
        super(self.__class__, self)._run()
299
        self._run(server_id=server_id)
300

    
301

    
302
class PersonalityArgument(KeyValueArgument):
303
    @property
304
    def value(self):
305
        return self._value if hasattr(self, '_value') else []
306

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

    
334

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

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

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

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

    
368

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

    
376
    @errors.generic.all
377
    @errors.cyclades.connection
378
    @errors.cyclades.server_id
379
    def _run(self, server_id, new_name):
380
        self._optional_output(
381
            self.client.update_server_name(int(server_id), new_name))
382

    
383
    def main(self, server_id, new_name):
384
        super(self.__class__, self)._run()
385
        self._run(server_id=server_id, new_name=new_name)
386

    
387

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

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

    
396
    @errors.generic.all
397
    @errors.cyclades.connection
398
    @errors.cyclades.server_id
399
    def _run(self, server_id):
400
            status = 'DELETED'
401
            if self['wait']:
402
                details = self.client.get_server_details(server_id)
403
                status = details['status']
404

    
405
            r = self.client.delete_server(int(server_id))
406
            self._optional_output(r)
407

    
408
            if self['wait']:
409
                self._wait(server_id, status)
410

    
411
    def main(self, server_id):
412
        super(self.__class__, self)._run()
413
        self._run(server_id=server_id)
414

    
415

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

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

    
427
    @errors.generic.all
428
    @errors.cyclades.connection
429
    @errors.cyclades.server_id
430
    def _run(self, server_id):
431
        hard_reboot = self['hard']
432
        if hard_reboot:
433
            self.error(
434
                'WARNING: -f/--force will be deprecated in version 0.12\n'
435
                '\tIn the future, please use --type=hard instead')
436
        if self['type']:
437
            if self['type'].lower() in ('soft', ):
438
                hard_reboot = False
439
            elif self['type'].lower() in ('hard', ):
440
                hard_reboot = True
441
            else:
442
                raise CLISyntaxError(
443
                    'Invalid reboot type %s' % self['type'],
444
                    importance=2, details=[
445
                        '--type values are either SOFT (default) or HARD'])
446

    
447
        r = self.client.reboot_server(int(server_id), hard_reboot)
448
        self._optional_output(r)
449

    
450
        if self['wait']:
451
            self._wait(server_id, 'REBOOT')
452

    
453
    def main(self, server_id):
454
        super(self.__class__, self)._run()
455
        self._run(server_id=server_id)
456

    
457

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

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

    
466
    @errors.generic.all
467
    @errors.cyclades.connection
468
    @errors.cyclades.server_id
469
    def _run(self, server_id):
470
        status = 'ACTIVE'
471
        if self['wait']:
472
            details = self.client.get_server_details(server_id)
473
            status = details['status']
474
            if status in ('ACTIVE', ):
475
                return
476

    
477
        r = self.client.start_server(int(server_id))
478
        self._optional_output(r)
479

    
480
        if self['wait']:
481
            self._wait(server_id, status)
482

    
483
    def main(self, server_id):
484
        super(self.__class__, self)._run()
485
        self._run(server_id=server_id)
486

    
487

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

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

    
496
    @errors.generic.all
497
    @errors.cyclades.connection
498
    @errors.cyclades.server_id
499
    def _run(self, server_id):
500
        status = 'STOPPED'
501
        if self['wait']:
502
            details = self.client.get_server_details(server_id)
503
            status = details['status']
504
            if status in ('STOPPED', ):
505
                return
506

    
507
        r = self.client.shutdown_server(int(server_id))
508
        self._optional_output(r)
509

    
510
        if self['wait']:
511
            self._wait(server_id, status)
512

    
513
    def main(self, server_id):
514
        super(self.__class__, self)._run()
515
        self._run(server_id=server_id)
516

    
517

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

    
527
    @errors.generic.all
528
    @errors.cyclades.connection
529
    @errors.cyclades.server_id
530
    def _run(self, server_id):
531
        self._print(
532
            self.client.get_server_console(int(server_id)), self.print_dict)
533

    
534
    def main(self, server_id):
535
        super(self.__class__, self)._run()
536
        self._run(server_id=server_id)
537

    
538

    
539
@command(server_cmds)
540
class server_resize(_init_cyclades, _optional_output_cmd):
541
    """Set a different flavor for an existing server
542
    To get server ids and flavor ids:
543
    /server list
544
    /flavor list
545
    """
546

    
547
    @errors.generic.all
548
    @errors.cyclades.connection
549
    @errors.cyclades.server_id
550
    @errors.cyclades.flavor_id
551
    def _run(self, server_id, flavor_id):
552
        self._optional_output(self.client.resize_server(server_id, flavor_id))
553

    
554
    def main(self, server_id, flavor_id):
555
        super(self.__class__, self)._run()
556
        self._run(server_id=server_id, flavor_id=flavor_id)
557

    
558

    
559
@command(server_cmds)
560
class server_firewall(_init_cyclades):
561
    """Manage virtual server firewall profiles for public networks"""
562

    
563

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

    
574
    arguments = dict(
575
        wait=FlagArgument('Wait server firewall to build', ('-w', '--wait')),
576
        timeout=IntArgument(
577
            'Set wait timeout in seconds (default: 60)', '--timeout',
578
            default=60)
579
    )
580

    
581
    @errors.generic.all
582
    @errors.cyclades.connection
583
    @errors.cyclades.server_id
584
    @errors.cyclades.firewall
585
    def _run(self, server_id, profile):
586
        if self['timeout'] and not self['wait']:
587
            raise CLIInvalidArgument('Invalid use of --timeout', details=[
588
                'Timeout is used only along with -w/--wait'])
589
        old_profile = self.client.get_firewall_profile(server_id)
590
        if old_profile.lower() == profile.lower():
591
            self.error('Firewall of server %s: allready in status %s' % (
592
                server_id, old_profile))
593
        else:
594
            self._optional_output(self.client.set_firewall_profile(
595
                server_id=int(server_id), profile=('%s' % profile).upper()))
596
            if self['wait']:
597
                self._wait(server_id, old_profile, timeout=self['timeout'])
598

    
599
    def main(self, server_id, profile):
600
        super(self.__class__, self)._run()
601
        self._run(server_id=server_id, profile=profile)
602

    
603

    
604
@command(server_cmds)
605
class server_firewall_get(_init_cyclades):
606
    """Get the firewall profile for a virtual servers' public network"""
607

    
608
    @errors.generic.all
609
    @errors.cyclades.connection
610
    @errors.cyclades.server_id
611
    def _run(self, server_id):
612
        self.writeln(self.client.get_firewall_profile(server_id))
613

    
614
    def main(self, server_id):
615
        super(self.__class__, self)._run()
616
        self._run(server_id=server_id)
617

    
618

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

    
623
    arguments = dict(
624
        enum=FlagArgument('Enumerate results', '--enumerate')
625
    )
626

    
627
    @errors.generic.all
628
    @errors.cyclades.connection
629
    @errors.cyclades.server_id
630
    def _run(self, server_id):
631
        reply = self.client.list_server_nics(int(server_id))
632
        self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
633

    
634
    def main(self, server_id):
635
        super(self.__class__, self)._run()
636
        self._run(server_id=server_id)
637

    
638

    
639
@command(server_cmds)
640
class server_metadata(_init_cyclades):
641
    """Manage Server metadata (key:value pairs of server attributes)"""
642

    
643

    
644
@command(server_cmds)
645
class server_metadata_list(_init_cyclades, _optional_json):
646
    """Get server metadata"""
647

    
648
    @errors.generic.all
649
    @errors.cyclades.connection
650
    @errors.cyclades.server_id
651
    @errors.cyclades.metadata
652
    def _run(self, server_id, key=''):
653
        self._print(
654
            self.client.get_server_metadata(int(server_id), key),
655
            self.print_dict)
656

    
657
    def main(self, server_id, key=''):
658
        super(self.__class__, self)._run()
659
        self._run(server_id=server_id, key=key)
660

    
661

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

    
670
    @errors.generic.all
671
    @errors.cyclades.connection
672
    @errors.cyclades.server_id
673
    def _run(self, server_id, keyvals):
674
        assert keyvals, 'Please, add some metadata ( key=value)'
675
        metadata = dict()
676
        for keyval in keyvals:
677
            k, sep, v = keyval.partition('=')
678
            if sep and k:
679
                metadata[k] = v
680
            else:
681
                raiseCLIError(
682
                    'Invalid piece of metadata %s' % keyval,
683
                    importance=2, details=[
684
                        'Correct metadata format: key=val',
685
                        'For example:',
686
                        '/server metadata set <server id>'
687
                        'key1=value1 key2=value2'])
688
        self._print(
689
            self.client.update_server_metadata(int(server_id), **metadata),
690
            self.print_dict)
691

    
692
    def main(self, server_id, *key_equals_val):
693
        super(self.__class__, self)._run()
694
        self._run(server_id=server_id, keyvals=key_equals_val)
695

    
696

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

    
701
    @errors.generic.all
702
    @errors.cyclades.connection
703
    @errors.cyclades.server_id
704
    @errors.cyclades.metadata
705
    def _run(self, server_id, key):
706
        self._optional_output(
707
            self.client.delete_server_metadata(int(server_id), key))
708

    
709
    def main(self, server_id, key):
710
        super(self.__class__, self)._run()
711
        self._run(server_id=server_id, key=key)
712

    
713

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

    
718
    @errors.generic.all
719
    @errors.cyclades.connection
720
    @errors.cyclades.server_id
721
    def _run(self, server_id):
722
        self._print(
723
            self.client.get_server_stats(int(server_id)), self.print_dict)
724

    
725
    def main(self, server_id):
726
        super(self.__class__, self)._run()
727
        self._run(server_id=server_id)
728

    
729

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

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

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

    
752
    def main(self, server_id, current_status='BUILD'):
753
        super(self.__class__, self)._run()
754
        self._run(server_id=server_id, current_status=current_status)
755

    
756

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

    
761
    PERMANENTS = ('id', 'name')
762

    
763
    arguments = dict(
764
        detail=FlagArgument('show detailed output', ('-l', '--details')),
765
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
766
        more=FlagArgument(
767
            'output results in pages (-n to set items per page, default 10)',
768
            '--more'),
769
        enum=FlagArgument('Enumerate results', '--enumerate'),
770
        ram=ValueArgument('filter by ram', ('--ram')),
771
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
772
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
773
        disk_template=ValueArgument(
774
            'filter by disk_templace', ('--disk-template'))
775
    )
776

    
777
    def _apply_common_filters(self, flavors):
778
        common_filters = dict()
779
        if self['ram']:
780
            common_filters['ram'] = self['ram']
781
        if self['vcpus']:
782
            common_filters['vcpus'] = self['vcpus']
783
        if self['disk']:
784
            common_filters['disk'] = self['disk']
785
        if self['disk_template']:
786
            common_filters['SNF:disk_template'] = self['disk_template']
787
        return filter_dicts_by_dict(flavors, common_filters)
788

    
789
    @errors.generic.all
790
    @errors.cyclades.connection
791
    def _run(self):
792
        withcommons = self['ram'] or self['vcpus'] or (
793
            self['disk'] or self['disk_template'])
794
        detail = self['detail'] or withcommons
795
        flavors = self.client.list_flavors(detail)
796
        flavors = self._filter_by_name(flavors)
797
        flavors = self._filter_by_id(flavors)
798
        if withcommons:
799
            flavors = self._apply_common_filters(flavors)
800
        if not (self['detail'] or self['json_output']):
801
            remove_from_items(flavors, 'links')
802
        if detail and not self['detail']:
803
            for flv in flavors:
804
                for key in set(flv).difference(self.PERMANENTS):
805
                    flv.pop(key)
806
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
807
        self._print(
808
            flavors,
809
            with_redundancy=self['detail'], with_enumeration=self['enum'],
810
            **kwargs)
811
        if self['more']:
812
            pager(kwargs['out'].getvalue())
813

    
814
    def main(self):
815
        super(self.__class__, self)._run()
816
        self._run()
817

    
818

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

    
825
    @errors.generic.all
826
    @errors.cyclades.connection
827
    @errors.cyclades.flavor_id
828
    def _run(self, flavor_id):
829
        self._print(
830
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
831

    
832
    def main(self, flavor_id):
833
        super(self.__class__, self)._run()
834
        self._run(flavor_id=flavor_id)
835

    
836

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

    
850

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

    
857
    @errors.generic.all
858
    @errors.cyclades.connection
859
    @errors.cyclades.network_id
860
    def _run(self, network_id):
861
        network = self.client.get_network_details(int(network_id))
862
        _add_name(self, network)
863
        self._print(network, self.print_dict, exclude=('id'))
864

    
865
    def main(self, network_id):
866
        super(self.__class__, self)._run()
867
        self._run(network_id=network_id)
868

    
869

    
870
@command(network_cmds)
871
class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
872
    """List networks"""
873

    
874
    PERMANENTS = ('id', 'name')
875

    
876
    arguments = dict(
877
        detail=FlagArgument('show detailed output', ('-l', '--details')),
878
        limit=IntArgument('limit # of listed networks', ('-n', '--number')),
879
        more=FlagArgument(
880
            'output results in pages (-n to set items per page, default 10)',
881
            '--more'),
882
        enum=FlagArgument('Enumerate results', '--enumerate'),
883
        status=ValueArgument('filter by status', ('--status')),
884
        public=FlagArgument('only public networks', ('--public')),
885
        private=FlagArgument('only private networks', ('--private')),
886
        dhcp=FlagArgument('show networks with dhcp', ('--with-dhcp')),
887
        no_dhcp=FlagArgument('show networks without dhcp', ('--without-dhcp')),
888
        user_id=ValueArgument('filter by user id', ('--user-id')),
889
        user_name=ValueArgument('filter by user name', ('--user-name')),
890
        gateway=ValueArgument('filter by gateway (IPv4)', ('--gateway')),
891
        gateway6=ValueArgument('filter by gateway (IPv6)', ('--gateway6')),
892
        cidr=ValueArgument('filter by cidr (IPv4)', ('--cidr')),
893
        cidr6=ValueArgument('filter by cidr (IPv6)', ('--cidr6')),
894
        type=ValueArgument('filter by type', ('--type')),
895
    )
896

    
897
    def _apply_common_filters(self, networks):
898
        common_filter = dict()
899
        if self['public']:
900
            if self['private']:
901
                return []
902
            common_filter['public'] = self['public']
903
        elif self['private']:
904
            common_filter['public'] = False
905
        if self['dhcp']:
906
            if self['no_dhcp']:
907
                return []
908
            common_filter['dhcp'] = True
909
        elif self['no_dhcp']:
910
            common_filter['dhcp'] = False
911
        if self['user_id'] or self['user_name']:
912
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
913
            common_filter['user_id'] = uuid
914
        for term in ('status', 'gateway', 'gateway6', 'cidr', 'cidr6', 'type'):
915
            if self[term]:
916
                common_filter[term] = self[term]
917
        return filter_dicts_by_dict(networks, common_filter)
918

    
919
    def _add_name(self, networks, key='user_id'):
920
        uuids = self._uuids2usernames(
921
            list(set([net[key] for net in networks])))
922
        for net in networks:
923
            v = net.get(key, None)
924
            if v:
925
                net[key] += ' (%s)' % uuids[v]
926
        return networks
927

    
928
    @errors.generic.all
929
    @errors.cyclades.connection
930
    def _run(self):
931
        withcommons = False
932
        for term in (
933
                'status', 'public', 'private', 'user_id', 'user_name', 'type',
934
                'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
935
            if self[term]:
936
                withcommons = True
937
                break
938
        detail = self['detail'] or withcommons
939
        networks = self.client.list_networks(detail)
940
        networks = self._filter_by_name(networks)
941
        networks = self._filter_by_id(networks)
942
        if withcommons:
943
            networks = self._apply_common_filters(networks)
944
        if not (self['detail'] or self['json_output']):
945
            remove_from_items(networks, 'links')
946
        if detail and not self['detail']:
947
            for net in networks:
948
                for key in set(net).difference(self.PERMANENTS):
949
                    net.pop(key)
950
        if self['detail'] and not self['json_output']:
951
            self._add_name(networks)
952
            self._add_name(networks, 'tenant_id')
953
        kwargs = dict(with_enumeration=self['enum'])
954
        if self['more']:
955
            kwargs['out'] = StringIO()
956
            kwargs['title'] = ()
957
        if self['limit']:
958
            networks = networks[:self['limit']]
959
        self._print(networks, **kwargs)
960
        if self['more']:
961
            pager(kwargs['out'].getvalue())
962

    
963
    def main(self):
964
        super(self.__class__, self)._run()
965
        self._run()
966

    
967

    
968
@command(network_cmds)
969
class network_create(_init_cyclades, _optional_json, _network_wait):
970
    """Create an (unconnected) network"""
971

    
972
    arguments = dict(
973
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
974
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
975
        dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
976
        type=ValueArgument(
977
            'Valid network types are '
978
            'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
979
            '--with-type',
980
            default='MAC_FILTERED'),
981
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
982
    )
983

    
984
    @errors.generic.all
985
    @errors.cyclades.connection
986
    @errors.cyclades.network_max
987
    def _run(self, name):
988
        r = self.client.create_network(
989
            name,
990
            cidr=self['cidr'],
991
            gateway=self['gateway'],
992
            dhcp=self['dhcp'],
993
            type=self['type'])
994
        _add_name(self, r)
995
        self._print(r, self.print_dict)
996
        if self['wait'] and r['status'] in ('PENDING', ):
997
            self._wait(r['id'], 'PENDING')
998

    
999
    def main(self, name):
1000
        super(self.__class__, self)._run()
1001
        self._run(name)
1002

    
1003

    
1004
@command(network_cmds)
1005
class network_rename(_init_cyclades, _optional_output_cmd):
1006
    """Set the name of a network"""
1007

    
1008
    @errors.generic.all
1009
    @errors.cyclades.connection
1010
    @errors.cyclades.network_id
1011
    def _run(self, network_id, new_name):
1012
        self._optional_output(
1013
                self.client.update_network_name(int(network_id), new_name))
1014

    
1015
    def main(self, network_id, new_name):
1016
        super(self.__class__, self)._run()
1017
        self._run(network_id=network_id, new_name=new_name)
1018

    
1019

    
1020
@command(network_cmds)
1021
class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
1022
    """Delete a network"""
1023

    
1024
    arguments = dict(
1025
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
1026
    )
1027

    
1028
    @errors.generic.all
1029
    @errors.cyclades.connection
1030
    @errors.cyclades.network_id
1031
    @errors.cyclades.network_in_use
1032
    def _run(self, network_id):
1033
        status = 'DELETED'
1034
        if self['wait']:
1035
            r = self.client.get_network_details(network_id)
1036
            status = r['status']
1037
            if status in ('DELETED', ):
1038
                return
1039

    
1040
        r = self.client.delete_network(int(network_id))
1041
        self._optional_output(r)
1042

    
1043
        if self['wait']:
1044
            self._wait(network_id, status)
1045

    
1046
    def main(self, network_id):
1047
        super(self.__class__, self)._run()
1048
        self._run(network_id=network_id)
1049

    
1050

    
1051
@command(network_cmds)
1052
class network_connect(_init_cyclades, _optional_output_cmd):
1053
    """Connect a server to a network"""
1054

    
1055
    @errors.generic.all
1056
    @errors.cyclades.connection
1057
    @errors.cyclades.server_id
1058
    @errors.cyclades.network_id
1059
    def _run(self, server_id, network_id):
1060
        self._optional_output(
1061
                self.client.connect_server(int(server_id), int(network_id)))
1062

    
1063
    def main(self, server_id, network_id):
1064
        super(self.__class__, self)._run()
1065
        self._run(server_id=server_id, network_id=network_id)
1066

    
1067

    
1068
@command(network_cmds)
1069
class network_disconnect(_init_cyclades):
1070
    """Disconnect a nic that connects a server to a network
1071
    Nic ids are listed as "attachments" in detailed network information
1072
    To get detailed network information: /network info <network id>
1073
    """
1074

    
1075
    @errors.cyclades.nic_format
1076
    def _server_id_from_nic(self, nic_id):
1077
        return nic_id.split('-')[1]
1078

    
1079
    @errors.generic.all
1080
    @errors.cyclades.connection
1081
    @errors.cyclades.server_id
1082
    @errors.cyclades.nic_id
1083
    def _run(self, nic_id, server_id):
1084
        num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
1085
        if not num_of_disconnected:
1086
            raise ClientError(
1087
                'Network Interface %s not found on server %s' % (
1088
                    nic_id, server_id),
1089
                status=404)
1090
        print('Disconnected %s connections' % num_of_disconnected)
1091

    
1092
    def main(self, nic_id):
1093
        super(self.__class__, self)._run()
1094
        server_id = self._server_id_from_nic(nic_id=nic_id)
1095
        self._run(nic_id=nic_id, server_id=server_id)
1096

    
1097

    
1098
@command(network_cmds)
1099
class network_wait(_init_cyclades, _network_wait):
1100
    """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1101

    
1102
    arguments = dict(
1103
        timeout=IntArgument(
1104
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
1105
    )
1106

    
1107
    @errors.generic.all
1108
    @errors.cyclades.connection
1109
    @errors.cyclades.network_id
1110
    def _run(self, network_id, current_status):
1111
        net = self.client.get_network_details(network_id)
1112
        if net['status'].lower() == current_status.lower():
1113
            self._wait(network_id, current_status, timeout=self['timeout'])
1114
        else:
1115
            self.error(
1116
                'Network %s: Cannot wait for status %s, '
1117
                'status is already %s' % (
1118
                    network_id, current_status, net['status']))
1119

    
1120
    def main(self, network_id, current_status='PENDING'):
1121
        super(self.__class__, self)._run()
1122
        self._run(network_id=network_id, current_status=current_status)
1123

    
1124

    
1125
@command(ip_cmds)
1126
class ip_pools(_init_cyclades, _optional_json):
1127
    """List pools of floating IPs"""
1128

    
1129
    @errors.generic.all
1130
    @errors.cyclades.connection
1131
    def _run(self):
1132
        r = self.client.get_floating_ip_pools()
1133
        self._print(r if self['json_output'] else r['floating_ip_pools'])
1134

    
1135
    def main(self):
1136
        super(self.__class__, self)._run()
1137
        self._run()
1138

    
1139

    
1140
@command(ip_cmds)
1141
class ip_list(_init_cyclades, _optional_json):
1142
    """List reserved floating IPs"""
1143

    
1144
    @errors.generic.all
1145
    @errors.cyclades.connection
1146
    def _run(self):
1147
        r = self.client.get_floating_ips()
1148
        self._print(r if self['json_output'] else r['floating_ips'])
1149

    
1150
    def main(self):
1151
        super(self.__class__, self)._run()
1152
        self._run()
1153

    
1154

    
1155
@command(ip_cmds)
1156
class ip_info(_init_cyclades, _optional_json):
1157
    """Details for an IP"""
1158

    
1159
    @errors.generic.all
1160
    @errors.cyclades.connection
1161
    def _run(self, ip):
1162
        self._print(self.client.get_floating_ip(ip), self.print_dict)
1163

    
1164
    def main(self, IP):
1165
        super(self.__class__, self)._run()
1166
        self._run(ip=IP)
1167

    
1168

    
1169
@command(ip_cmds)
1170
class ip_reserve(_init_cyclades, _optional_json):
1171
    """Reserve a floating IP
1172
    An IP is reserved from an IP pool. The default IP pool is chosen
1173
    automatically, but there is the option if specifying an explicit IP pool.
1174
    """
1175

    
1176
    arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
1177

    
1178
    @errors.generic.all
1179
    @errors.cyclades.connection
1180
    def _run(self, ip=None):
1181
        self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1182

    
1183
    def main(self, requested_IP=None):
1184
        super(self.__class__, self)._run()
1185
        self._run(ip=requested_IP)
1186

    
1187

    
1188
@command(ip_cmds)
1189
class ip_release(_init_cyclades, _optional_output_cmd):
1190
    """Release a floating IP
1191
    The release IP is "returned" to the IP pool it came from.
1192
    """
1193

    
1194
    @errors.generic.all
1195
    @errors.cyclades.connection
1196
    def _run(self, ip):
1197
        self._optional_output(self.client.delete_floating_ip(ip))
1198

    
1199
    def main(self, IP):
1200
        super(self.__class__, self)._run()
1201
        self._run(ip=IP)
1202

    
1203

    
1204
@command(ip_cmds)
1205
class ip_attach(_init_cyclades, _optional_output_cmd):
1206
    """Attach a floating IP to a server
1207
    """
1208

    
1209
    @errors.generic.all
1210
    @errors.cyclades.connection
1211
    @errors.cyclades.server_id
1212
    def _run(self, server_id, ip):
1213
        self._optional_output(self.client.attach_floating_ip(server_id, ip))
1214

    
1215
    def main(self, server_id, IP):
1216
        super(self.__class__, self)._run()
1217
        self._run(server_id=server_id, ip=IP)
1218

    
1219

    
1220
@command(ip_cmds)
1221
class ip_detach(_init_cyclades, _optional_output_cmd):
1222
    """Detach a floating IP from a server
1223
    """
1224

    
1225
    @errors.generic.all
1226
    @errors.cyclades.connection
1227
    @errors.cyclades.server_id
1228
    def _run(self, server_id, ip):
1229
        self._optional_output(self.client.detach_floating_ip(server_id, ip))
1230

    
1231
    def main(self, server_id, IP):
1232
        super(self.__class__, self)._run()
1233
        self._run(server_id=server_id, ip=IP)