Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 81c60832

History | View | Annotate | Download (44.7 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(size)]
393
        if size == 1:
394
            return [self.client.create_server(**servers[0])]
395
        return self.client.async_run(self.client.create_server, servers)
396

    
397
    @errors.generic.all
398
    @errors.cyclades.connection
399
    @errors.plankton.id
400
    @errors.cyclades.flavor_id
401
    def _run(self, name, flavor_id, image_id):
402
        for r in self._create_cluster(
403
                name, flavor_id, image_id, size=self['cluster_size'] or 1):
404
            print 'HEY I GOT A', r
405
            print 'MKEY?????????????????'
406
            usernames = self._uuids2usernames([r['user_id'], r['tenant_id']])
407
            r['user_id'] += ' (%s)' % usernames[r['user_id']]
408
            r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
409
            self._print(r, self.print_dict)
410
            if self['wait']:
411
                self._wait(r['id'], r['status'])
412
            self.error('')
413

    
414
    def main(self, name, flavor_id, image_id):
415
        super(self.__class__, self)._run()
416
        self._run(name=name, flavor_id=flavor_id, image_id=image_id)
417

    
418

    
419
@command(server_cmds)
420
class server_rename(_init_cyclades, _optional_output_cmd):
421
    """Set/update a virtual server name
422
    virtual server names are not unique, therefore multiple servers may share
423
    the same name
424
    """
425

    
426
    @errors.generic.all
427
    @errors.cyclades.connection
428
    @errors.cyclades.server_id
429
    def _run(self, server_id, new_name):
430
        self._optional_output(
431
            self.client.update_server_name(int(server_id), new_name))
432

    
433
    def main(self, server_id, new_name):
434
        super(self.__class__, self)._run()
435
        self._run(server_id=server_id, new_name=new_name)
436

    
437

    
438
@command(server_cmds)
439
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
440
    """Delete a virtual server"""
441

    
442
    arguments = dict(
443
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
444
        cluster=FlagArgument(
445
            '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
446
            'prefix. In that case, the prefix replaces the server id',
447
            '--cluster')
448
    )
449

    
450
    def _server_ids(self, server_var):
451
        if self['cluster']:
452
            return [s['id'] for s in self.client.list_servers() if (
453
                s['name'].startswith(server_var))]
454

    
455
        @errors.cyclades.server_id
456
        def _check_server_id(self, server_id):
457
            return server_id
458

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

    
461
    @errors.generic.all
462
    @errors.cyclades.connection
463
    def _run(self, server_var):
464
        for server_id in self._server_ids(server_var):
465
            if self['wait']:
466
                details = self.client.get_server_details(server_id)
467
                status = details['status']
468

    
469
            r = self.client.delete_server(server_id)
470
            self._optional_output(r)
471

    
472
            if self['wait']:
473
                self._wait(server_id, status)
474

    
475
    def main(self, server_id_or_cluster_prefix):
476
        super(self.__class__, self)._run()
477
        self._run(server_id_or_cluster_prefix)
478

    
479

    
480
@command(server_cmds)
481
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
482
    """Reboot a virtual server"""
483

    
484
    arguments = dict(
485
        hard=FlagArgument(
486
            'perform a hard reboot (deprecated)', ('-f', '--force')),
487
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
488
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
489
    )
490

    
491
    @errors.generic.all
492
    @errors.cyclades.connection
493
    @errors.cyclades.server_id
494
    def _run(self, server_id):
495
        hard_reboot = self['hard']
496
        if hard_reboot:
497
            self.error(
498
                'WARNING: -f/--force will be deprecated in version 0.12\n'
499
                '\tIn the future, please use --type=hard instead')
500
        if self['type']:
501
            if self['type'].lower() in ('soft', ):
502
                hard_reboot = False
503
            elif self['type'].lower() in ('hard', ):
504
                hard_reboot = True
505
            else:
506
                raise CLISyntaxError(
507
                    'Invalid reboot type %s' % self['type'],
508
                    importance=2, details=[
509
                        '--type values are either SOFT (default) or HARD'])
510

    
511
        r = self.client.reboot_server(int(server_id), hard_reboot)
512
        self._optional_output(r)
513

    
514
        if self['wait']:
515
            self._wait(server_id, 'REBOOT')
516

    
517
    def main(self, server_id):
518
        super(self.__class__, self)._run()
519
        self._run(server_id=server_id)
520

    
521

    
522
@command(server_cmds)
523
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
524
    """Start an existing virtual server"""
525

    
526
    arguments = dict(
527
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
528
    )
529

    
530
    @errors.generic.all
531
    @errors.cyclades.connection
532
    @errors.cyclades.server_id
533
    def _run(self, server_id):
534
        status = 'ACTIVE'
535
        if self['wait']:
536
            details = self.client.get_server_details(server_id)
537
            status = details['status']
538
            if status in ('ACTIVE', ):
539
                return
540

    
541
        r = self.client.start_server(int(server_id))
542
        self._optional_output(r)
543

    
544
        if self['wait']:
545
            self._wait(server_id, status)
546

    
547
    def main(self, server_id):
548
        super(self.__class__, self)._run()
549
        self._run(server_id=server_id)
550

    
551

    
552
@command(server_cmds)
553
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
554
    """Shutdown an active virtual server"""
555

    
556
    arguments = dict(
557
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
558
    )
559

    
560
    @errors.generic.all
561
    @errors.cyclades.connection
562
    @errors.cyclades.server_id
563
    def _run(self, server_id):
564
        status = 'STOPPED'
565
        if self['wait']:
566
            details = self.client.get_server_details(server_id)
567
            status = details['status']
568
            if status in ('STOPPED', ):
569
                return
570

    
571
        r = self.client.shutdown_server(int(server_id))
572
        self._optional_output(r)
573

    
574
        if self['wait']:
575
            self._wait(server_id, status)
576

    
577
    def main(self, server_id):
578
        super(self.__class__, self)._run()
579
        self._run(server_id=server_id)
580

    
581

    
582
@command(server_cmds)
583
class server_console(_init_cyclades, _optional_json):
584
    """Get a VNC console to access an existing virtual server
585
    Console connection information provided (at least):
586
    - host: (url or address) a VNC host
587
    - port: (int) the gateway to enter virtual server on host
588
    - password: for VNC authorization
589
    """
590

    
591
    @errors.generic.all
592
    @errors.cyclades.connection
593
    @errors.cyclades.server_id
594
    def _run(self, server_id):
595
        self._print(
596
            self.client.get_server_console(int(server_id)), self.print_dict)
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_resize(_init_cyclades, _optional_output_cmd):
605
    """Set a different flavor for an existing server
606
    To get server ids and flavor ids:
607
    /server list
608
    /flavor list
609
    """
610

    
611
    @errors.generic.all
612
    @errors.cyclades.connection
613
    @errors.cyclades.server_id
614
    @errors.cyclades.flavor_id
615
    def _run(self, server_id, flavor_id):
616
        self._optional_output(self.client.resize_server(server_id, flavor_id))
617

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

    
622

    
623
@command(server_cmds)
624
class server_firewall(_init_cyclades):
625
    """Manage virtual server firewall profiles for public networks"""
626

    
627

    
628
@command(server_cmds)
629
class server_firewall_set(
630
        _init_cyclades, _optional_output_cmd, _firewall_wait):
631
    """Set the firewall profile on virtual server public network
632
    Values for profile:
633
    - DISABLED: Shutdown firewall
634
    - ENABLED: Firewall in normal mode
635
    - PROTECTED: Firewall in secure mode
636
    """
637

    
638
    arguments = dict(
639
        wait=FlagArgument('Wait server firewall to build', ('-w', '--wait')),
640
        timeout=IntArgument(
641
            'Set wait timeout in seconds (default: 60)', '--timeout',
642
            default=60)
643
    )
644

    
645
    @errors.generic.all
646
    @errors.cyclades.connection
647
    @errors.cyclades.server_id
648
    @errors.cyclades.firewall
649
    def _run(self, server_id, profile):
650
        if self['timeout'] and not self['wait']:
651
            raise CLIInvalidArgument('Invalid use of --timeout', details=[
652
                'Timeout is used only along with -w/--wait'])
653
        old_profile = self.client.get_firewall_profile(server_id)
654
        if old_profile.lower() == profile.lower():
655
            self.error('Firewall of server %s: allready in status %s' % (
656
                server_id, old_profile))
657
        else:
658
            self._optional_output(self.client.set_firewall_profile(
659
                server_id=int(server_id), profile=('%s' % profile).upper()))
660
            if self['wait']:
661
                self._wait(server_id, old_profile, timeout=self['timeout'])
662

    
663
    def main(self, server_id, profile):
664
        super(self.__class__, self)._run()
665
        self._run(server_id=server_id, profile=profile)
666

    
667

    
668
@command(server_cmds)
669
class server_firewall_get(_init_cyclades):
670
    """Get the firewall profile for a virtual servers' public network"""
671

    
672
    @errors.generic.all
673
    @errors.cyclades.connection
674
    @errors.cyclades.server_id
675
    def _run(self, server_id):
676
        self.writeln(self.client.get_firewall_profile(server_id))
677

    
678
    def main(self, server_id):
679
        super(self.__class__, self)._run()
680
        self._run(server_id=server_id)
681

    
682

    
683
@command(server_cmds)
684
class server_addr(_init_cyclades, _optional_json):
685
    """List the addresses of all network interfaces on a virtual server"""
686

    
687
    arguments = dict(
688
        enum=FlagArgument('Enumerate results', '--enumerate')
689
    )
690

    
691
    @errors.generic.all
692
    @errors.cyclades.connection
693
    @errors.cyclades.server_id
694
    def _run(self, server_id):
695
        reply = self.client.list_server_nics(int(server_id))
696
        self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
697

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

    
702

    
703
@command(server_cmds)
704
class server_metadata(_init_cyclades):
705
    """Manage Server metadata (key:value pairs of server attributes)"""
706

    
707

    
708
@command(server_cmds)
709
class server_metadata_list(_init_cyclades, _optional_json):
710
    """Get server metadata"""
711

    
712
    @errors.generic.all
713
    @errors.cyclades.connection
714
    @errors.cyclades.server_id
715
    @errors.cyclades.metadata
716
    def _run(self, server_id, key=''):
717
        self._print(
718
            self.client.get_server_metadata(int(server_id), key),
719
            self.print_dict)
720

    
721
    def main(self, server_id, key=''):
722
        super(self.__class__, self)._run()
723
        self._run(server_id=server_id, key=key)
724

    
725

    
726
@command(server_cmds)
727
class server_metadata_set(_init_cyclades, _optional_json):
728
    """Set / update virtual server metadata
729
    Metadata should be given in key/value pairs in key=value format
730
    For example: /server metadata set <server id> key1=value1 key2=value2
731
    Old, unreferenced metadata will remain intact
732
    """
733

    
734
    @errors.generic.all
735
    @errors.cyclades.connection
736
    @errors.cyclades.server_id
737
    def _run(self, server_id, keyvals):
738
        assert keyvals, 'Please, add some metadata ( key=value)'
739
        metadata = dict()
740
        for keyval in keyvals:
741
            k, sep, v = keyval.partition('=')
742
            if sep and k:
743
                metadata[k] = v
744
            else:
745
                raiseCLIError(
746
                    'Invalid piece of metadata %s' % keyval,
747
                    importance=2, details=[
748
                        'Correct metadata format: key=val',
749
                        'For example:',
750
                        '/server metadata set <server id>'
751
                        'key1=value1 key2=value2'])
752
        self._print(
753
            self.client.update_server_metadata(int(server_id), **metadata),
754
            self.print_dict)
755

    
756
    def main(self, server_id, *key_equals_val):
757
        super(self.__class__, self)._run()
758
        self._run(server_id=server_id, keyvals=key_equals_val)
759

    
760

    
761
@command(server_cmds)
762
class server_metadata_delete(_init_cyclades, _optional_output_cmd):
763
    """Delete virtual server metadata"""
764

    
765
    @errors.generic.all
766
    @errors.cyclades.connection
767
    @errors.cyclades.server_id
768
    @errors.cyclades.metadata
769
    def _run(self, server_id, key):
770
        self._optional_output(
771
            self.client.delete_server_metadata(int(server_id), key))
772

    
773
    def main(self, server_id, key):
774
        super(self.__class__, self)._run()
775
        self._run(server_id=server_id, key=key)
776

    
777

    
778
@command(server_cmds)
779
class server_stats(_init_cyclades, _optional_json):
780
    """Get virtual server statistics"""
781

    
782
    @errors.generic.all
783
    @errors.cyclades.connection
784
    @errors.cyclades.server_id
785
    def _run(self, server_id):
786
        self._print(
787
            self.client.get_server_stats(int(server_id)), self.print_dict)
788

    
789
    def main(self, server_id):
790
        super(self.__class__, self)._run()
791
        self._run(server_id=server_id)
792

    
793

    
794
@command(server_cmds)
795
class server_wait(_init_cyclades, _server_wait):
796
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
797

    
798
    arguments = dict(
799
        timeout=IntArgument(
800
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
801
    )
802

    
803
    @errors.generic.all
804
    @errors.cyclades.connection
805
    @errors.cyclades.server_id
806
    def _run(self, server_id, current_status):
807
        r = self.client.get_server_details(server_id)
808
        if r['status'].lower() == current_status.lower():
809
            self._wait(server_id, current_status, timeout=self['timeout'])
810
        else:
811
            self.error(
812
                'Server %s: Cannot wait for status %s, '
813
                'status is already %s' % (
814
                    server_id, current_status, r['status']))
815

    
816
    def main(self, server_id, current_status='BUILD'):
817
        super(self.__class__, self)._run()
818
        self._run(server_id=server_id, current_status=current_status)
819

    
820

    
821
@command(flavor_cmds)
822
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
823
    """List available hardware flavors"""
824

    
825
    PERMANENTS = ('id', 'name')
826

    
827
    arguments = dict(
828
        detail=FlagArgument('show detailed output', ('-l', '--details')),
829
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
830
        more=FlagArgument(
831
            'output results in pages (-n to set items per page, default 10)',
832
            '--more'),
833
        enum=FlagArgument('Enumerate results', '--enumerate'),
834
        ram=ValueArgument('filter by ram', ('--ram')),
835
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
836
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
837
        disk_template=ValueArgument(
838
            'filter by disk_templace', ('--disk-template'))
839
    )
840

    
841
    def _apply_common_filters(self, flavors):
842
        common_filters = dict()
843
        if self['ram']:
844
            common_filters['ram'] = self['ram']
845
        if self['vcpus']:
846
            common_filters['vcpus'] = self['vcpus']
847
        if self['disk']:
848
            common_filters['disk'] = self['disk']
849
        if self['disk_template']:
850
            common_filters['SNF:disk_template'] = self['disk_template']
851
        return filter_dicts_by_dict(flavors, common_filters)
852

    
853
    @errors.generic.all
854
    @errors.cyclades.connection
855
    def _run(self):
856
        withcommons = self['ram'] or self['vcpus'] or (
857
            self['disk'] or self['disk_template'])
858
        detail = self['detail'] or withcommons
859
        flavors = self.client.list_flavors(detail)
860
        flavors = self._filter_by_name(flavors)
861
        flavors = self._filter_by_id(flavors)
862
        if withcommons:
863
            flavors = self._apply_common_filters(flavors)
864
        if not (self['detail'] or (
865
                self['json_output'] or self['output_format'])):
866
            remove_from_items(flavors, 'links')
867
        if detail and not self['detail']:
868
            for flv in flavors:
869
                for key in set(flv).difference(self.PERMANENTS):
870
                    flv.pop(key)
871
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
872
        self._print(
873
            flavors,
874
            with_redundancy=self['detail'], with_enumeration=self['enum'],
875
            **kwargs)
876
        if self['more']:
877
            pager(kwargs['out'].getvalue())
878

    
879
    def main(self):
880
        super(self.__class__, self)._run()
881
        self._run()
882

    
883

    
884
@command(flavor_cmds)
885
class flavor_info(_init_cyclades, _optional_json):
886
    """Detailed information on a hardware flavor
887
    To get a list of available flavors and flavor ids, try /flavor list
888
    """
889

    
890
    @errors.generic.all
891
    @errors.cyclades.connection
892
    @errors.cyclades.flavor_id
893
    def _run(self, flavor_id):
894
        self._print(
895
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
896

    
897
    def main(self, flavor_id):
898
        super(self.__class__, self)._run()
899
        self._run(flavor_id=flavor_id)
900

    
901

    
902
def _add_name(self, net):
903
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
904
        if user_id:
905
            uuids.append(user_id)
906
        if tenant_id:
907
            uuids.append(tenant_id)
908
        if uuids:
909
            usernames = self._uuids2usernames(uuids)
910
            if user_id:
911
                net['user_id'] += ' (%s)' % usernames[user_id]
912
            if tenant_id:
913
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]
914

    
915

    
916
@command(network_cmds)
917
class network_info(_init_cyclades, _optional_json):
918
    """Detailed information on a network
919
    To get a list of available networks and network ids, try /network list
920
    """
921

    
922
    @errors.generic.all
923
    @errors.cyclades.connection
924
    @errors.cyclades.network_id
925
    def _run(self, network_id):
926
        network = self.client.get_network_details(int(network_id))
927
        _add_name(self, network)
928
        self._print(network, self.print_dict, exclude=('id'))
929

    
930
    def main(self, network_id):
931
        super(self.__class__, self)._run()
932
        self._run(network_id=network_id)
933

    
934

    
935
@command(network_cmds)
936
class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
937
    """List networks"""
938

    
939
    PERMANENTS = ('id', 'name')
940

    
941
    arguments = dict(
942
        detail=FlagArgument('show detailed output', ('-l', '--details')),
943
        limit=IntArgument('limit # of listed networks', ('-n', '--number')),
944
        more=FlagArgument(
945
            'output results in pages (-n to set items per page, default 10)',
946
            '--more'),
947
        enum=FlagArgument('Enumerate results', '--enumerate'),
948
        status=ValueArgument('filter by status', ('--status')),
949
        public=FlagArgument('only public networks', ('--public')),
950
        private=FlagArgument('only private networks', ('--private')),
951
        dhcp=FlagArgument('show networks with dhcp', ('--with-dhcp')),
952
        no_dhcp=FlagArgument('show networks without dhcp', ('--without-dhcp')),
953
        user_id=ValueArgument('filter by user id', ('--user-id')),
954
        user_name=ValueArgument('filter by user name', ('--user-name')),
955
        gateway=ValueArgument('filter by gateway (IPv4)', ('--gateway')),
956
        gateway6=ValueArgument('filter by gateway (IPv6)', ('--gateway6')),
957
        cidr=ValueArgument('filter by cidr (IPv4)', ('--cidr')),
958
        cidr6=ValueArgument('filter by cidr (IPv6)', ('--cidr6')),
959
        type=ValueArgument('filter by type', ('--type')),
960
    )
961

    
962
    def _apply_common_filters(self, networks):
963
        common_filter = dict()
964
        if self['public']:
965
            if self['private']:
966
                return []
967
            common_filter['public'] = self['public']
968
        elif self['private']:
969
            common_filter['public'] = False
970
        if self['dhcp']:
971
            if self['no_dhcp']:
972
                return []
973
            common_filter['dhcp'] = True
974
        elif self['no_dhcp']:
975
            common_filter['dhcp'] = False
976
        if self['user_id'] or self['user_name']:
977
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
978
            common_filter['user_id'] = uuid
979
        for term in ('status', 'gateway', 'gateway6', 'cidr', 'cidr6', 'type'):
980
            if self[term]:
981
                common_filter[term] = self[term]
982
        return filter_dicts_by_dict(networks, common_filter)
983

    
984
    def _add_name(self, networks, key='user_id'):
985
        uuids = self._uuids2usernames(
986
            list(set([net[key] for net in networks])))
987
        for net in networks:
988
            v = net.get(key, None)
989
            if v:
990
                net[key] += ' (%s)' % uuids[v]
991
        return networks
992

    
993
    @errors.generic.all
994
    @errors.cyclades.connection
995
    def _run(self):
996
        withcommons = False
997
        for term in (
998
                'status', 'public', 'private', 'user_id', 'user_name', 'type',
999
                'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
1000
            if self[term]:
1001
                withcommons = True
1002
                break
1003
        detail = self['detail'] or withcommons
1004
        networks = self.client.list_networks(detail)
1005
        networks = self._filter_by_name(networks)
1006
        networks = self._filter_by_id(networks)
1007
        if withcommons:
1008
            networks = self._apply_common_filters(networks)
1009
        if not (self['detail'] or (
1010
                self['json_output'] or self['output_format'])):
1011
            remove_from_items(networks, 'links')
1012
        if detail and not self['detail']:
1013
            for net in networks:
1014
                for key in set(net).difference(self.PERMANENTS):
1015
                    net.pop(key)
1016
        if self['detail'] and not (
1017
                self['json_output'] or self['output_format']):
1018
            self._add_name(networks)
1019
            self._add_name(networks, 'tenant_id')
1020
        kwargs = dict(with_enumeration=self['enum'])
1021
        if self['more']:
1022
            kwargs['out'] = StringIO()
1023
            kwargs['title'] = ()
1024
        if self['limit']:
1025
            networks = networks[:self['limit']]
1026
        self._print(networks, **kwargs)
1027
        if self['more']:
1028
            pager(kwargs['out'].getvalue())
1029

    
1030
    def main(self):
1031
        super(self.__class__, self)._run()
1032
        self._run()
1033

    
1034

    
1035
@command(network_cmds)
1036
class network_create(_init_cyclades, _optional_json, _network_wait):
1037
    """Create an (unconnected) network"""
1038

    
1039
    arguments = dict(
1040
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
1041
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
1042
        dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
1043
        type=ValueArgument(
1044
            'Valid network types are '
1045
            'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
1046
            '--with-type',
1047
            default='MAC_FILTERED'),
1048
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
1049
    )
1050

    
1051
    @errors.generic.all
1052
    @errors.cyclades.connection
1053
    @errors.cyclades.network_max
1054
    def _run(self, name):
1055
        r = self.client.create_network(
1056
            name,
1057
            cidr=self['cidr'],
1058
            gateway=self['gateway'],
1059
            dhcp=self['dhcp'],
1060
            type=self['type'])
1061
        _add_name(self, r)
1062
        self._print(r, self.print_dict)
1063
        if self['wait'] and r['status'] in ('PENDING', ):
1064
            self._wait(r['id'], 'PENDING')
1065

    
1066
    def main(self, name):
1067
        super(self.__class__, self)._run()
1068
        self._run(name)
1069

    
1070

    
1071
@command(network_cmds)
1072
class network_rename(_init_cyclades, _optional_output_cmd):
1073
    """Set the name of a network"""
1074

    
1075
    @errors.generic.all
1076
    @errors.cyclades.connection
1077
    @errors.cyclades.network_id
1078
    def _run(self, network_id, new_name):
1079
        self._optional_output(
1080
                self.client.update_network_name(int(network_id), new_name))
1081

    
1082
    def main(self, network_id, new_name):
1083
        super(self.__class__, self)._run()
1084
        self._run(network_id=network_id, new_name=new_name)
1085

    
1086

    
1087
@command(network_cmds)
1088
class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
1089
    """Delete a network"""
1090

    
1091
    arguments = dict(
1092
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
1093
    )
1094

    
1095
    @errors.generic.all
1096
    @errors.cyclades.connection
1097
    @errors.cyclades.network_in_use
1098
    @errors.cyclades.network_id
1099
    def _run(self, network_id):
1100
        status = 'DELETED'
1101
        if self['wait']:
1102
            r = self.client.get_network_details(network_id)
1103
            status = r['status']
1104
            if status in ('DELETED', ):
1105
                return
1106

    
1107
        r = self.client.delete_network(int(network_id))
1108
        self._optional_output(r)
1109

    
1110
        if self['wait']:
1111
            self._wait(network_id, status)
1112

    
1113
    def main(self, network_id):
1114
        super(self.__class__, self)._run()
1115
        self._run(network_id=network_id)
1116

    
1117

    
1118
@command(network_cmds)
1119
class network_connect(_init_cyclades, _optional_output_cmd):
1120
    """Connect a server to a network"""
1121

    
1122
    @errors.generic.all
1123
    @errors.cyclades.connection
1124
    @errors.cyclades.server_id
1125
    @errors.cyclades.network_id
1126
    def _run(self, server_id, network_id):
1127
        self._optional_output(
1128
                self.client.connect_server(int(server_id), int(network_id)))
1129

    
1130
    def main(self, server_id, network_id):
1131
        super(self.__class__, self)._run()
1132
        self._run(server_id=server_id, network_id=network_id)
1133

    
1134

    
1135
@command(network_cmds)
1136
class network_disconnect(_init_cyclades):
1137
    """Disconnect a nic that connects a server to a network
1138
    Nic ids are listed as "attachments" in detailed network information
1139
    To get detailed network information: /network info <network id>
1140
    """
1141

    
1142
    @errors.cyclades.nic_format
1143
    def _server_id_from_nic(self, nic_id):
1144
        return nic_id.split('-')[1]
1145

    
1146
    @errors.generic.all
1147
    @errors.cyclades.connection
1148
    @errors.cyclades.server_id
1149
    @errors.cyclades.nic_id
1150
    def _run(self, nic_id, server_id):
1151
        num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
1152
        if not num_of_disconnected:
1153
            raise ClientError(
1154
                'Network Interface %s not found on server %s' % (
1155
                    nic_id, server_id),
1156
                status=404)
1157
        print('Disconnected %s connections' % num_of_disconnected)
1158

    
1159
    def main(self, nic_id):
1160
        super(self.__class__, self)._run()
1161
        server_id = self._server_id_from_nic(nic_id=nic_id)
1162
        self._run(nic_id=nic_id, server_id=server_id)
1163

    
1164

    
1165
@command(network_cmds)
1166
class network_wait(_init_cyclades, _network_wait):
1167
    """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1168

    
1169
    arguments = dict(
1170
        timeout=IntArgument(
1171
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
1172
    )
1173

    
1174
    @errors.generic.all
1175
    @errors.cyclades.connection
1176
    @errors.cyclades.network_id
1177
    def _run(self, network_id, current_status):
1178
        net = self.client.get_network_details(network_id)
1179
        if net['status'].lower() == current_status.lower():
1180
            self._wait(network_id, current_status, timeout=self['timeout'])
1181
        else:
1182
            self.error(
1183
                'Network %s: Cannot wait for status %s, '
1184
                'status is already %s' % (
1185
                    network_id, current_status, net['status']))
1186

    
1187
    def main(self, network_id, current_status='PENDING'):
1188
        super(self.__class__, self)._run()
1189
        self._run(network_id=network_id, current_status=current_status)
1190

    
1191

    
1192
@command(ip_cmds)
1193
class ip_pools(_init_cyclades, _optional_json):
1194
    """List pools of floating IPs"""
1195

    
1196
    @errors.generic.all
1197
    @errors.cyclades.connection
1198
    def _run(self):
1199
        r = self.client.get_floating_ip_pools()
1200
        self._print(r if self['json_output'] or self['output_format'] else r[
1201
            'floating_ip_pools'])
1202

    
1203
    def main(self):
1204
        super(self.__class__, self)._run()
1205
        self._run()
1206

    
1207

    
1208
@command(ip_cmds)
1209
class ip_list(_init_cyclades, _optional_json):
1210
    """List reserved floating IPs"""
1211

    
1212
    @errors.generic.all
1213
    @errors.cyclades.connection
1214
    def _run(self):
1215
        r = self.client.get_floating_ips()
1216
        self._print(r if self['json_output'] or self['output_format'] else r[
1217
            'floating_ips'])
1218

    
1219
    def main(self):
1220
        super(self.__class__, self)._run()
1221
        self._run()
1222

    
1223

    
1224
@command(ip_cmds)
1225
class ip_info(_init_cyclades, _optional_json):
1226
    """Details for an IP"""
1227

    
1228
    @errors.generic.all
1229
    @errors.cyclades.connection
1230
    def _run(self, ip):
1231
        self._print(self.client.get_floating_ip(ip), self.print_dict)
1232

    
1233
    def main(self, IP):
1234
        super(self.__class__, self)._run()
1235
        self._run(ip=IP)
1236

    
1237

    
1238
@command(ip_cmds)
1239
class ip_reserve(_init_cyclades, _optional_json):
1240
    """Reserve a floating IP
1241
    An IP is reserved from an IP pool. The default IP pool is chosen
1242
    automatically, but there is the option if specifying an explicit IP pool.
1243
    """
1244

    
1245
    arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
1246

    
1247
    @errors.generic.all
1248
    @errors.cyclades.connection
1249
    def _run(self, ip=None):
1250
        self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1251

    
1252
    def main(self, requested_IP=None):
1253
        super(self.__class__, self)._run()
1254
        self._run(ip=requested_IP)
1255

    
1256

    
1257
@command(ip_cmds)
1258
class ip_release(_init_cyclades, _optional_output_cmd):
1259
    """Release a floating IP
1260
    The release IP is "returned" to the IP pool it came from.
1261
    """
1262

    
1263
    @errors.generic.all
1264
    @errors.cyclades.connection
1265
    def _run(self, ip):
1266
        self._optional_output(self.client.delete_floating_ip(ip))
1267

    
1268
    def main(self, IP):
1269
        super(self.__class__, self)._run()
1270
        self._run(ip=IP)
1271

    
1272

    
1273
@command(ip_cmds)
1274
class ip_attach(_init_cyclades, _optional_output_cmd):
1275
    """Attach a floating IP to a server
1276
    """
1277

    
1278
    @errors.generic.all
1279
    @errors.cyclades.connection
1280
    @errors.cyclades.server_id
1281
    def _run(self, server_id, ip):
1282
        self._optional_output(self.client.attach_floating_ip(server_id, ip))
1283

    
1284
    def main(self, server_id, IP):
1285
        super(self.__class__, self)._run()
1286
        self._run(server_id=server_id, ip=IP)
1287

    
1288

    
1289
@command(ip_cmds)
1290
class ip_detach(_init_cyclades, _optional_output_cmd):
1291
    """Detach a floating IP from a server
1292
    """
1293

    
1294
    @errors.generic.all
1295
    @errors.cyclades.connection
1296
    @errors.cyclades.server_id
1297
    def _run(self, server_id, ip):
1298
        self._optional_output(self.client.detach_floating_ip(server_id, ip))
1299

    
1300
    def main(self, server_id, IP):
1301
        super(self.__class__, self)._run()
1302
        self._run(server_id=server_id, ip=IP)