Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 40ddc207

History | View | Annotate | Download (45.6 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
    '  [local-path=]PATH: local file to be injected (relative or absolute)',
67
    '  [server-path=]SERVER_PATH: destination location inside server Image',
68
    '  [owner=]OWNER: virtual servers user id for the remote file',
69
    '  [group=]GROUP: virtual servers group id or name for the remote file',
70
    '  [mode=]MODE: permission in octal (e.g., 0777 or o+rwx)',
71
    'e.g., -p /tmp/my.file,owner=root,mode=0777']
72

    
73

    
74
class _service_wait(object):
75

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

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

    
103

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

    
112

    
113
class _network_wait(_service_wait):
114

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

    
120

    
121
class _firewall_wait(_service_wait):
122

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

    
129

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

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

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

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

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

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

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

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

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

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

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

    
279

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

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

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

    
304

    
305
class PersonalityArgument(KeyValueArgument):
306

    
307
    terms = (
308
        ('local-path', 'contents'),
309
        ('server-path', 'path'),
310
        ('owner', 'owner'),
311
        ('group', 'group'),
312
        ('mode', 'mode'))
313

    
314
    @property
315
    def value(self):
316
        return self._value if hasattr(self, '_value') else []
317

    
318
    @value.setter
319
    def value(self, newvalue):
320
        if newvalue == self.default:
321
            return self.value
322
        self._value, input_dict = [], {}
323
        for i, terms in enumerate(newvalue):
324
            termlist = terms.split(',')
325
            if len(termlist) > len(self.terms):
326
                msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
327
                raiseCLIError(CLISyntaxError(msg), details=howto_personality)
328

    
329
            for k, v in self.terms:
330
                prefix = '%s=' % k
331
                for item in termlist:
332
                    if item.lower().startswith(prefix):
333
                        input_dict[k] = item[len(k) + 1:]
334
                        break
335
                    item = None
336
                if item:
337
                    termlist.remove(item)
338

    
339
            try:
340
                path = input_dict['local-path']
341
            except KeyError:
342
                path = termlist.pop(0)
343
                if not path:
344
                    raise CLIInvalidArgument(
345
                        '--personality: No local path specified',
346
                        details=howto_personality)
347

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

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

    
365

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

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

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

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

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

    
439

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

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

    
454
    def main(self, server_id, new_name):
455
        super(self.__class__, self)._run()
456
        self._run(server_id=server_id, new_name=new_name)
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
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
465
        cluster=FlagArgument(
466
            '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
467
            'prefix. In that case, the prefix replaces the server id',
468
            '--cluster')
469
    )
470

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

    
476
        @errors.cyclades.server_id
477
        def _check_server_id(self, server_id):
478
            return server_id
479

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

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

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

    
493
            if self['wait']:
494
                self._wait(server_id, status)
495

    
496
    def main(self, server_id_or_cluster_prefix):
497
        super(self.__class__, self)._run()
498
        self._run(server_id_or_cluster_prefix)
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
        hard=FlagArgument(
507
            'perform a hard reboot (deprecated)', ('-f', '--force')),
508
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
509
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
510
    )
511

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

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

    
535
        if self['wait']:
536
            self._wait(server_id, 'REBOOT')
537

    
538
    def main(self, server_id):
539
        super(self.__class__, self)._run()
540
        self._run(server_id=server_id)
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
    arguments = dict(
548
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
549
    )
550

    
551
    @errors.generic.all
552
    @errors.cyclades.connection
553
    @errors.cyclades.server_id
554
    def _run(self, server_id):
555
        status = 'ACTIVE'
556
        if self['wait']:
557
            details = self.client.get_server_details(server_id)
558
            status = details['status']
559
            if status in ('ACTIVE', ):
560
                return
561

    
562
        r = self.client.start_server(int(server_id))
563
        self._optional_output(r)
564

    
565
        if self['wait']:
566
            self._wait(server_id, status)
567

    
568
    def main(self, server_id):
569
        super(self.__class__, self)._run()
570
        self._run(server_id=server_id)
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
    arguments = dict(
578
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
579
    )
580

    
581
    @errors.generic.all
582
    @errors.cyclades.connection
583
    @errors.cyclades.server_id
584
    def _run(self, server_id):
585
        status = 'STOPPED'
586
        if self['wait']:
587
            details = self.client.get_server_details(server_id)
588
            status = details['status']
589
            if status in ('STOPPED', ):
590
                return
591

    
592
        r = self.client.shutdown_server(int(server_id))
593
        self._optional_output(r)
594

    
595
        if self['wait']:
596
            self._wait(server_id, status)
597

    
598
    def main(self, server_id):
599
        super(self.__class__, self)._run()
600
        self._run(server_id=server_id)
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

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

    
619
    def main(self, server_id):
620
        super(self.__class__, self)._run()
621
        self._run(server_id=server_id)
622

    
623

    
624
@command(server_cmds)
625
class server_resize(_init_cyclades, _optional_output_cmd):
626
    """Set a different flavor for an existing server
627
    To get server ids and flavor ids:
628
    /server list
629
    /flavor list
630
    """
631

    
632
    @errors.generic.all
633
    @errors.cyclades.connection
634
    @errors.cyclades.server_id
635
    @errors.cyclades.flavor_id
636
    def _run(self, server_id, flavor_id):
637
        self._optional_output(self.client.resize_server(server_id, flavor_id))
638

    
639
    def main(self, server_id, flavor_id):
640
        super(self.__class__, self)._run()
641
        self._run(server_id=server_id, flavor_id=flavor_id)
642

    
643

    
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
class server_firewall_set(
651
        _init_cyclades, _optional_output_cmd, _firewall_wait):
652
    """Set the firewall profile on virtual server public network
653
    Values for profile:
654
    - DISABLED: Shutdown firewall
655
    - ENABLED: Firewall in normal mode
656
    - PROTECTED: Firewall in secure mode
657
    """
658

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

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

    
684
    def main(self, server_id, profile):
685
        super(self.__class__, self)._run()
686
        self._run(server_id=server_id, profile=profile)
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
    @errors.generic.all
694
    @errors.cyclades.connection
695
    @errors.cyclades.server_id
696
    def _run(self, server_id):
697
        self.writeln(self.client.get_firewall_profile(server_id))
698

    
699
    def main(self, server_id):
700
        super(self.__class__, self)._run()
701
        self._run(server_id=server_id)
702

    
703

    
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

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

    
719
    def main(self, server_id):
720
        super(self.__class__, self)._run()
721
        self._run(server_id=server_id)
722

    
723

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

    
728

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

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

    
742
    def main(self, server_id, key=''):
743
        super(self.__class__, self)._run()
744
        self._run(server_id=server_id, key=key)
745

    
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

    
755
    @errors.generic.all
756
    @errors.cyclades.connection
757
    @errors.cyclades.server_id
758
    def _run(self, server_id, keyvals):
759
        assert keyvals, 'Please, add some metadata ( key=value)'
760
        metadata = dict()
761
        for keyval in keyvals:
762
            k, sep, v = keyval.partition('=')
763
            if sep and k:
764
                metadata[k] = v
765
            else:
766
                raiseCLIError(
767
                    'Invalid piece of metadata %s' % keyval,
768
                    importance=2, details=[
769
                        'Correct metadata format: key=val',
770
                        'For example:',
771
                        '/server metadata set <server id>'
772
                        '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):
778
        super(self.__class__, self)._run()
779
        self._run(server_id=server_id, keyvals=key_equals_val)
780

    
781

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

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

    
794
    def main(self, server_id, key):
795
        super(self.__class__, self)._run()
796
        self._run(server_id=server_id, key=key)
797

    
798

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

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

    
810
    def main(self, server_id):
811
        super(self.__class__, self)._run()
812
        self._run(server_id=server_id)
813

    
814

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

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

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

    
837
    def main(self, server_id, current_status='BUILD'):
838
        super(self.__class__, self)._run()
839
        self._run(server_id=server_id, current_status=current_status)
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
    PERMANENTS = ('id', 'name')
847

    
848
    arguments = dict(
849
        detail=FlagArgument('show detailed output', ('-l', '--details')),
850
        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
        enum=FlagArgument('Enumerate results', '--enumerate'),
855
        ram=ValueArgument('filter by ram', ('--ram')),
856
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
857
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
858
        disk_template=ValueArgument(
859
            'filter by disk_templace', ('--disk-template'))
860
    )
861

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

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

    
900
    def main(self):
901
        super(self.__class__, self)._run()
902
        self._run()
903

    
904

    
905
@command(flavor_cmds)
906
class flavor_info(_init_cyclades, _optional_json):
907
    """Detailed information on a hardware flavor
908
    To get a list of available flavors and flavor ids, try /flavor list
909
    """
910

    
911
    @errors.generic.all
912
    @errors.cyclades.connection
913
    @errors.cyclades.flavor_id
914
    def _run(self, flavor_id):
915
        self._print(
916
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
917

    
918
    def main(self, flavor_id):
919
        super(self.__class__, self)._run()
920
        self._run(flavor_id=flavor_id)
921

    
922

    
923
def _add_name(self, net):
924
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
925
        if user_id:
926
            uuids.append(user_id)
927
        if tenant_id:
928
            uuids.append(tenant_id)
929
        if uuids:
930
            usernames = self._uuids2usernames(uuids)
931
            if user_id:
932
                net['user_id'] += ' (%s)' % usernames[user_id]
933
            if tenant_id:
934
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]
935

    
936

    
937
@command(network_cmds)
938
class network_info(_init_cyclades, _optional_json):
939
    """Detailed information on a network
940
    To get a list of available networks and network ids, try /network list
941
    """
942

    
943
    @errors.generic.all
944
    @errors.cyclades.connection
945
    @errors.cyclades.network_id
946
    def _run(self, network_id):
947
        network = self.client.get_network_details(int(network_id))
948
        _add_name(self, network)
949
        self._print(network, self.print_dict, exclude=('id'))
950

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

    
955

    
956
@command(network_cmds)
957
class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
958
    """List networks"""
959

    
960
    PERMANENTS = ('id', 'name')
961

    
962
    arguments = dict(
963
        detail=FlagArgument('show detailed output', ('-l', '--details')),
964
        limit=IntArgument('limit # of listed networks', ('-n', '--number')),
965
        more=FlagArgument(
966
            'output results in pages (-n to set items per page, default 10)',
967
            '--more'),
968
        enum=FlagArgument('Enumerate results', '--enumerate'),
969
        status=ValueArgument('filter by status', ('--status')),
970
        public=FlagArgument('only public networks', ('--public')),
971
        private=FlagArgument('only private networks', ('--private')),
972
        dhcp=FlagArgument('show networks with dhcp', ('--with-dhcp')),
973
        no_dhcp=FlagArgument('show networks without dhcp', ('--without-dhcp')),
974
        user_id=ValueArgument('filter by user id', ('--user-id')),
975
        user_name=ValueArgument('filter by user name', ('--user-name')),
976
        gateway=ValueArgument('filter by gateway (IPv4)', ('--gateway')),
977
        gateway6=ValueArgument('filter by gateway (IPv6)', ('--gateway6')),
978
        cidr=ValueArgument('filter by cidr (IPv4)', ('--cidr')),
979
        cidr6=ValueArgument('filter by cidr (IPv6)', ('--cidr6')),
980
        type=ValueArgument('filter by type', ('--type')),
981
    )
982

    
983
    def _apply_common_filters(self, networks):
984
        common_filter = dict()
985
        if self['public']:
986
            if self['private']:
987
                return []
988
            common_filter['public'] = self['public']
989
        elif self['private']:
990
            common_filter['public'] = False
991
        if self['dhcp']:
992
            if self['no_dhcp']:
993
                return []
994
            common_filter['dhcp'] = True
995
        elif self['no_dhcp']:
996
            common_filter['dhcp'] = False
997
        if self['user_id'] or self['user_name']:
998
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
999
            common_filter['user_id'] = uuid
1000
        for term in ('status', 'gateway', 'gateway6', 'cidr', 'cidr6', 'type'):
1001
            if self[term]:
1002
                common_filter[term] = self[term]
1003
        return filter_dicts_by_dict(networks, common_filter)
1004

    
1005
    def _add_name(self, networks, key='user_id'):
1006
        uuids = self._uuids2usernames(
1007
            list(set([net[key] for net in networks])))
1008
        for net in networks:
1009
            v = net.get(key, None)
1010
            if v:
1011
                net[key] += ' (%s)' % uuids[v]
1012
        return networks
1013

    
1014
    @errors.generic.all
1015
    @errors.cyclades.connection
1016
    def _run(self):
1017
        withcommons = False
1018
        for term in (
1019
                'status', 'public', 'private', 'user_id', 'user_name', 'type',
1020
                'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
1021
            if self[term]:
1022
                withcommons = True
1023
                break
1024
        detail = self['detail'] or withcommons
1025
        networks = self.client.list_networks(detail)
1026
        networks = self._filter_by_name(networks)
1027
        networks = self._filter_by_id(networks)
1028
        if withcommons:
1029
            networks = self._apply_common_filters(networks)
1030
        if not (self['detail'] or (
1031
                self['json_output'] or self['output_format'])):
1032
            remove_from_items(networks, 'links')
1033
        if detail and not self['detail']:
1034
            for net in networks:
1035
                for key in set(net).difference(self.PERMANENTS):
1036
                    net.pop(key)
1037
        if self['detail'] and not (
1038
                self['json_output'] or self['output_format']):
1039
            self._add_name(networks)
1040
            self._add_name(networks, 'tenant_id')
1041
        kwargs = dict(with_enumeration=self['enum'])
1042
        if self['more']:
1043
            kwargs['out'] = StringIO()
1044
            kwargs['title'] = ()
1045
        if self['limit']:
1046
            networks = networks[:self['limit']]
1047
        self._print(networks, **kwargs)
1048
        if self['more']:
1049
            pager(kwargs['out'].getvalue())
1050

    
1051
    def main(self):
1052
        super(self.__class__, self)._run()
1053
        self._run()
1054

    
1055

    
1056
@command(network_cmds)
1057
class network_create(_init_cyclades, _optional_json, _network_wait):
1058
    """Create an (unconnected) network"""
1059

    
1060
    arguments = dict(
1061
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
1062
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
1063
        dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
1064
        type=ValueArgument(
1065
            'Valid network types are '
1066
            'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
1067
            '--with-type',
1068
            default='MAC_FILTERED'),
1069
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
1070
    )
1071

    
1072
    @errors.generic.all
1073
    @errors.cyclades.connection
1074
    @errors.cyclades.network_max
1075
    def _run(self, name):
1076
        r = self.client.create_network(
1077
            name,
1078
            cidr=self['cidr'],
1079
            gateway=self['gateway'],
1080
            dhcp=self['dhcp'],
1081
            type=self['type'])
1082
        _add_name(self, r)
1083
        self._print(r, self.print_dict)
1084
        if self['wait'] and r['status'] in ('PENDING', ):
1085
            self._wait(r['id'], 'PENDING')
1086

    
1087
    def main(self, name):
1088
        super(self.__class__, self)._run()
1089
        self._run(name)
1090

    
1091

    
1092
@command(network_cmds)
1093
class network_rename(_init_cyclades, _optional_output_cmd):
1094
    """Set the name of a network"""
1095

    
1096
    @errors.generic.all
1097
    @errors.cyclades.connection
1098
    @errors.cyclades.network_id
1099
    def _run(self, network_id, new_name):
1100
        self._optional_output(
1101
                self.client.update_network_name(int(network_id), new_name))
1102

    
1103
    def main(self, network_id, new_name):
1104
        super(self.__class__, self)._run()
1105
        self._run(network_id=network_id, new_name=new_name)
1106

    
1107

    
1108
@command(network_cmds)
1109
class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
1110
    """Delete a network"""
1111

    
1112
    arguments = dict(
1113
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
1114
    )
1115

    
1116
    @errors.generic.all
1117
    @errors.cyclades.connection
1118
    @errors.cyclades.network_in_use
1119
    @errors.cyclades.network_id
1120
    def _run(self, network_id):
1121
        status = 'DELETED'
1122
        if self['wait']:
1123
            r = self.client.get_network_details(network_id)
1124
            status = r['status']
1125
            if status in ('DELETED', ):
1126
                return
1127

    
1128
        r = self.client.delete_network(int(network_id))
1129
        self._optional_output(r)
1130

    
1131
        if self['wait']:
1132
            self._wait(network_id, status)
1133

    
1134
    def main(self, network_id):
1135
        super(self.__class__, self)._run()
1136
        self._run(network_id=network_id)
1137

    
1138

    
1139
@command(network_cmds)
1140
class network_connect(_init_cyclades, _optional_output_cmd):
1141
    """Connect a server to a network"""
1142

    
1143
    @errors.generic.all
1144
    @errors.cyclades.connection
1145
    @errors.cyclades.server_id
1146
    @errors.cyclades.network_id
1147
    def _run(self, server_id, network_id):
1148
        self._optional_output(
1149
                self.client.connect_server(int(server_id), int(network_id)))
1150

    
1151
    def main(self, server_id, network_id):
1152
        super(self.__class__, self)._run()
1153
        self._run(server_id=server_id, network_id=network_id)
1154

    
1155

    
1156
@command(network_cmds)
1157
class network_disconnect(_init_cyclades):
1158
    """Disconnect a nic that connects a server to a network
1159
    Nic ids are listed as "attachments" in detailed network information
1160
    To get detailed network information: /network info <network id>
1161
    """
1162

    
1163
    @errors.cyclades.nic_format
1164
    def _server_id_from_nic(self, nic_id):
1165
        return nic_id.split('-')[1]
1166

    
1167
    @errors.generic.all
1168
    @errors.cyclades.connection
1169
    @errors.cyclades.server_id
1170
    @errors.cyclades.nic_id
1171
    def _run(self, nic_id, server_id):
1172
        num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
1173
        if not num_of_disconnected:
1174
            raise ClientError(
1175
                'Network Interface %s not found on server %s' % (
1176
                    nic_id, server_id),
1177
                status=404)
1178
        print('Disconnected %s connections' % num_of_disconnected)
1179

    
1180
    def main(self, nic_id):
1181
        super(self.__class__, self)._run()
1182
        server_id = self._server_id_from_nic(nic_id=nic_id)
1183
        self._run(nic_id=nic_id, server_id=server_id)
1184

    
1185

    
1186
@command(network_cmds)
1187
class network_wait(_init_cyclades, _network_wait):
1188
    """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1189

    
1190
    arguments = dict(
1191
        timeout=IntArgument(
1192
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
1193
    )
1194

    
1195
    @errors.generic.all
1196
    @errors.cyclades.connection
1197
    @errors.cyclades.network_id
1198
    def _run(self, network_id, current_status):
1199
        net = self.client.get_network_details(network_id)
1200
        if net['status'].lower() == current_status.lower():
1201
            self._wait(network_id, current_status, timeout=self['timeout'])
1202
        else:
1203
            self.error(
1204
                'Network %s: Cannot wait for status %s, '
1205
                'status is already %s' % (
1206
                    network_id, current_status, net['status']))
1207

    
1208
    def main(self, network_id, current_status='PENDING'):
1209
        super(self.__class__, self)._run()
1210
        self._run(network_id=network_id, current_status=current_status)
1211

    
1212

    
1213
@command(ip_cmds)
1214
class ip_pools(_init_cyclades, _optional_json):
1215
    """List pools of floating IPs"""
1216

    
1217
    @errors.generic.all
1218
    @errors.cyclades.connection
1219
    def _run(self):
1220
        r = self.client.get_floating_ip_pools()
1221
        self._print(r if self['json_output'] or self['output_format'] else r[
1222
            'floating_ip_pools'])
1223

    
1224
    def main(self):
1225
        super(self.__class__, self)._run()
1226
        self._run()
1227

    
1228

    
1229
@command(ip_cmds)
1230
class ip_list(_init_cyclades, _optional_json):
1231
    """List reserved floating IPs"""
1232

    
1233
    @errors.generic.all
1234
    @errors.cyclades.connection
1235
    def _run(self):
1236
        r = self.client.get_floating_ips()
1237
        self._print(r if self['json_output'] or self['output_format'] else r[
1238
            'floating_ips'])
1239

    
1240
    def main(self):
1241
        super(self.__class__, self)._run()
1242
        self._run()
1243

    
1244

    
1245
@command(ip_cmds)
1246
class ip_info(_init_cyclades, _optional_json):
1247
    """Details for an IP"""
1248

    
1249
    @errors.generic.all
1250
    @errors.cyclades.connection
1251
    def _run(self, ip):
1252
        self._print(self.client.get_floating_ip(ip), self.print_dict)
1253

    
1254
    def main(self, IP):
1255
        super(self.__class__, self)._run()
1256
        self._run(ip=IP)
1257

    
1258

    
1259
@command(ip_cmds)
1260
class ip_reserve(_init_cyclades, _optional_json):
1261
    """Reserve a floating IP
1262
    An IP is reserved from an IP pool. The default IP pool is chosen
1263
    automatically, but there is the option if specifying an explicit IP pool.
1264
    """
1265

    
1266
    arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
1267

    
1268
    @errors.generic.all
1269
    @errors.cyclades.connection
1270
    def _run(self, ip=None):
1271
        self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1272

    
1273
    def main(self, requested_IP=None):
1274
        super(self.__class__, self)._run()
1275
        self._run(ip=requested_IP)
1276

    
1277

    
1278
@command(ip_cmds)
1279
class ip_release(_init_cyclades, _optional_output_cmd):
1280
    """Release a floating IP
1281
    The release IP is "returned" to the IP pool it came from.
1282
    """
1283

    
1284
    @errors.generic.all
1285
    @errors.cyclades.connection
1286
    def _run(self, ip):
1287
        self._optional_output(self.client.delete_floating_ip(ip))
1288

    
1289
    def main(self, IP):
1290
        super(self.__class__, self)._run()
1291
        self._run(ip=IP)
1292

    
1293

    
1294
@command(ip_cmds)
1295
class ip_attach(_init_cyclades, _optional_output_cmd):
1296
    """Attach a floating IP to a server
1297
    """
1298

    
1299
    @errors.generic.all
1300
    @errors.cyclades.connection
1301
    @errors.cyclades.server_id
1302
    def _run(self, server_id, ip):
1303
        self._optional_output(self.client.attach_floating_ip(server_id, ip))
1304

    
1305
    def main(self, server_id, IP):
1306
        super(self.__class__, self)._run()
1307
        self._run(server_id=server_id, ip=IP)
1308

    
1309

    
1310
@command(ip_cmds)
1311
class ip_detach(_init_cyclades, _optional_output_cmd):
1312
    """Detach a floating IP from a server
1313
    """
1314

    
1315
    @errors.generic.all
1316
    @errors.cyclades.connection
1317
    @errors.cyclades.server_id
1318
    def _run(self, server_id, ip):
1319
        self._optional_output(self.client.detach_floating_ip(server_id, ip))
1320

    
1321
    def main(self, server_id, IP):
1322
        super(self.__class__, self)._run()
1323
        self._run(server_id=server_id, ip=IP)