Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 54b6be76

History | View | Annotate | Download (22.9 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 kamaki.cli import command
35
from kamaki.cli.command_tree import CommandTree
36
from kamaki.cli.utils import print_dict
37
from kamaki.cli.errors import raiseCLIError, CLISyntaxError, CLIBaseUrlError
38
from kamaki.clients.cyclades import CycladesClient, ClientError
39
from kamaki.cli.argument import FlagArgument, ValueArgument, KeyValueArgument
40
from kamaki.cli.argument import ProgressBarArgument, DateArgument, IntArgument
41
from kamaki.cli.commands import _command_init, errors
42
from kamaki.cli.commands import _optional_output_cmd, _optional_json
43

    
44
from base64 import b64encode
45
from os.path import exists
46

    
47

    
48
server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
49
flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
50
network_cmds = CommandTree('network', 'Cyclades/Compute API network commands')
51
_commands = [server_cmds, flavor_cmds, network_cmds]
52

    
53

    
54
about_authentication = '\nUser Authentication:\
55
    \n* to check authentication: /user authenticate\
56
    \n* to set authentication token: /config set token <token>'
57

    
58
howto_personality = [
59
    'Defines a file to be injected to VMs personality.',
60
    'Personality value syntax: PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
61
    '  PATH: of local file to be injected',
62
    '  SERVER_PATH: destination location inside server Image',
63
    '  OWNER: user id of destination file owner',
64
    '  GROUP: group id or name to own destination file',
65
    '  MODEL: permition in octal (e.g. 0777 or o+rwx)']
66

    
67

    
68
class _init_cyclades(_command_init):
69
    @errors.generic.all
70
    def _run(self, service='compute'):
71
        token = self.config.get(service, 'token')\
72
            or self.config.get('global', 'token')
73

    
74
        if getattr(self, 'auth_base', False):
75
            cyclades_endpoints = self.auth_base.get_service_endpoints(
76
                self.config.get('cyclades', 'type'),
77
                self.config.get('cyclades', 'version'))
78
            base_url = cyclades_endpoints['publicURL']
79
        else:
80
            base_url = self.config.get('cyclades', 'url')
81
        if not base_url:
82
            raise CLIBaseUrlError(service='cyclades')
83

    
84
        self.client = CycladesClient(base_url=base_url, token=token)
85
        self._set_log_params()
86
        self._update_max_threads()
87

    
88
    def main(self):
89
        self._run()
90

    
91

    
92
@command(server_cmds)
93
class server_list(_init_cyclades, _optional_json):
94
    """List Virtual Machines accessible by user"""
95

    
96
    __doc__ += about_authentication
97

    
98
    arguments = dict(
99
        detail=FlagArgument('show detailed output', ('-l', '--details')),
100
        since=DateArgument(
101
            'show only items since date (\' d/m/Y H:M:S \')',
102
            '--since'),
103
        limit=IntArgument('limit number of listed VMs', ('-n', '--number')),
104
        more=FlagArgument(
105
            'output results in pages (-n to set items per page, default 10)',
106
            '--more'),
107
        enum=FlagArgument('Enumerate results', '--enumerate')
108
    )
109

    
110
    @errors.generic.all
111
    @errors.cyclades.connection
112
    @errors.cyclades.date
113
    def _run(self):
114
        servers = self.client.list_servers(self['detail'], self['since'])
115

    
116
        kwargs = dict(with_enumeration=self['enum'])
117
        if self['more']:
118
            kwargs['page_size'] = self['limit'] if self['limit'] else 10
119
        elif self['limit']:
120
            servers = servers[:self['limit']]
121
        self._print(servers, **kwargs)
122

    
123
    def main(self):
124
        super(self.__class__, self)._run()
125
        self._run()
126

    
127

    
128
@command(server_cmds)
129
class server_info(_init_cyclades, _optional_json):
130
    """Detailed information on a Virtual Machine
131
    Contains:
132
    - name, id, status, create/update dates
133
    - network interfaces
134
    - metadata (e.g. os, superuser) and diagnostics
135
    - hardware flavor and os image ids
136
    """
137

    
138
    @errors.generic.all
139
    @errors.cyclades.connection
140
    @errors.cyclades.server_id
141
    def _run(self, server_id):
142
        self._print(self.client.get_server_details(server_id), print_dict)
143

    
144
    def main(self, server_id):
145
        super(self.__class__, self)._run()
146
        self._run(server_id=server_id)
147

    
148

    
149
class PersonalityArgument(KeyValueArgument):
150
    @property
151
    def value(self):
152
        return self._value if hasattr(self, '_value') else []
153

    
154
    @value.setter
155
    def value(self, newvalue):
156
        if newvalue == self.default:
157
            return self.value
158
        self._value = []
159
        for i, terms in enumerate(newvalue):
160
            termlist = terms.split(',')
161
            if len(termlist) > 5:
162
                msg = 'Wrong number of terms (should be 1 to 5)'
163
                raiseCLIError(CLISyntaxError(msg), details=howto_personality)
164
            path = termlist[0]
165
            if not exists(path):
166
                raiseCLIError(
167
                    None,
168
                    '--personality: File %s does not exist' % path,
169
                    importance=1,
170
                    details=howto_personality)
171
            self._value.append(dict(path=path))
172
            with open(path) as f:
173
                self._value[i]['contents'] = b64encode(f.read())
174
            try:
175
                self._value[i]['path'] = termlist[1]
176
                self._value[i]['owner'] = termlist[2]
177
                self._value[i]['group'] = termlist[3]
178
                self._value[i]['mode'] = termlist[4]
179
            except IndexError:
180
                pass
181

    
182

    
183
@command(server_cmds)
184
class server_create(_init_cyclades, _optional_json):
185
    """Create a server (aka Virtual Machine)
186
    Parameters:
187
    - name: (single quoted text)
188
    - flavor id: Hardware flavor. Pick one from: /flavor list
189
    - image id: OS images. Pick one from: /image list
190
    """
191

    
192
    arguments = dict(
193
        personality=PersonalityArgument(
194
            (80 * ' ').join(howto_personality), ('-p', '--personality'))
195
    )
196

    
197
    @errors.generic.all
198
    @errors.cyclades.connection
199
    @errors.plankton.id
200
    @errors.cyclades.flavor_id
201
    def _run(self, name, flavor_id, image_id):
202
        self._print(
203
            self.client.create_server(
204
                name, int(flavor_id), image_id, self['personality']),
205
            print_dict)
206

    
207
    def main(self, name, flavor_id, image_id):
208
        super(self.__class__, self)._run()
209
        self._run(name=name, flavor_id=flavor_id, image_id=image_id)
210

    
211

    
212
@command(server_cmds)
213
class server_rename(_init_cyclades, _optional_output_cmd):
214
    """Set/update a server (VM) name
215
    VM names are not unique, therefore multiple servers may share the same name
216
    """
217

    
218
    @errors.generic.all
219
    @errors.cyclades.connection
220
    @errors.cyclades.server_id
221
    def _run(self, server_id, new_name):
222
        self._optional_output(
223
            self.client.update_server_name(int(server_id), new_name))
224

    
225
    def main(self, server_id, new_name):
226
        super(self.__class__, self)._run()
227
        self._run(server_id=server_id, new_name=new_name)
228

    
229

    
230
@command(server_cmds)
231
class server_delete(_init_cyclades, _optional_output_cmd):
232
    """Delete a server (VM)"""
233

    
234
    @errors.generic.all
235
    @errors.cyclades.connection
236
    @errors.cyclades.server_id
237
    def _run(self, server_id):
238
            self._optional_output(self.client.delete_server(int(server_id)))
239

    
240
    def main(self, server_id):
241
        super(self.__class__, self)._run()
242
        self._run(server_id=server_id)
243

    
244

    
245
@command(server_cmds)
246
class server_reboot(_init_cyclades, _optional_output_cmd):
247
    """Reboot a server (VM)"""
248

    
249
    arguments = dict(
250
        hard=FlagArgument('perform a hard reboot', ('-f', '--force'))
251
    )
252

    
253
    @errors.generic.all
254
    @errors.cyclades.connection
255
    @errors.cyclades.server_id
256
    def _run(self, server_id):
257
        self._optional_output(
258
            self.client.reboot_server(int(server_id), self['hard']))
259

    
260
    def main(self, server_id):
261
        super(self.__class__, self)._run()
262
        self._run(server_id=server_id)
263

    
264

    
265
@command(server_cmds)
266
class server_start(_init_cyclades, _optional_output_cmd):
267
    """Start an existing server (VM)"""
268

    
269
    @errors.generic.all
270
    @errors.cyclades.connection
271
    @errors.cyclades.server_id
272
    def _run(self, server_id):
273
        self._optional_output(self.client.start_server(int(server_id)))
274

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

    
279

    
280
@command(server_cmds)
281
class server_shutdown(_init_cyclades, _optional_output_cmd):
282
    """Shutdown an active server (VM)"""
283

    
284
    @errors.generic.all
285
    @errors.cyclades.connection
286
    @errors.cyclades.server_id
287
    def _run(self, server_id):
288
        self._optional_output(self.client.shutdown_server(int(server_id)))
289

    
290
    def main(self, server_id):
291
        super(self.__class__, self)._run()
292
        self._run(server_id=server_id)
293

    
294

    
295
@command(server_cmds)
296
class server_console(_init_cyclades, _optional_json):
297
    """Get a VNC console to access an existing server (VM)
298
    Console connection information provided (at least):
299
    - host: (url or address) a VNC host
300
    - port: (int) the gateway to enter VM on host
301
    - password: for VNC authorization
302
    """
303

    
304
    @errors.generic.all
305
    @errors.cyclades.connection
306
    @errors.cyclades.server_id
307
    def _run(self, server_id):
308
        self._print(
309
            self.client.get_server_console(int(server_id)), print_dict)
310

    
311
    def main(self, server_id):
312
        super(self.__class__, self)._run()
313
        self._run(server_id=server_id)
314

    
315

    
316
@command(server_cmds)
317
class server_firewall(_init_cyclades):
318
    """Manage server (VM) firewall profiles for public networks"""
319

    
320

    
321
@command(server_cmds)
322
class server_firewall_set(_init_cyclades, _optional_output_cmd):
323
    """Set the server (VM) firewall profile on VMs public network
324
    Values for profile:
325
    - DISABLED: Shutdown firewall
326
    - ENABLED: Firewall in normal mode
327
    - PROTECTED: Firewall in secure mode
328
    """
329

    
330
    @errors.generic.all
331
    @errors.cyclades.connection
332
    @errors.cyclades.server_id
333
    @errors.cyclades.firewall
334
    def _run(self, server_id, profile):
335
        self._optional_output(self.client.set_firewall_profile(
336
            server_id=int(server_id), profile=('%s' % profile).upper()))
337

    
338
    def main(self, server_id, profile):
339
        super(self.__class__, self)._run()
340
        self._run(server_id=server_id, profile=profile)
341

    
342

    
343
@command(server_cmds)
344
class server_firewall_get(_init_cyclades):
345
    """Get the server (VM) firewall profile for its public network"""
346

    
347
    @errors.generic.all
348
    @errors.cyclades.connection
349
    @errors.cyclades.server_id
350
    def _run(self, server_id):
351
        print(self.client.get_firewall_profile(server_id))
352

    
353
    def main(self, server_id):
354
        super(self.__class__, self)._run()
355
        self._run(server_id=server_id)
356

    
357

    
358
@command(server_cmds)
359
class server_addr(_init_cyclades, _optional_json):
360
    """List the addresses of all network interfaces on a server (VM)"""
361

    
362
    arguments = dict(
363
        enum=FlagArgument('Enumerate results', '--enumerate')
364
    )
365

    
366
    @errors.generic.all
367
    @errors.cyclades.connection
368
    @errors.cyclades.server_id
369
    def _run(self, server_id):
370
        reply = self.client.list_server_nics(int(server_id))
371
        self._print(
372
            reply, with_enumeration=self['enum'] and len(reply) > 1)
373

    
374
    def main(self, server_id):
375
        super(self.__class__, self)._run()
376
        self._run(server_id=server_id)
377

    
378

    
379
@command(server_cmds)
380
class server_metadata(_init_cyclades):
381
    """Manage Server metadata (key:value pairs of server attributes)"""
382

    
383

    
384
@command(server_cmds)
385
class server_metadata_list(_init_cyclades, _optional_json):
386
    """Get server metadata"""
387

    
388
    @errors.generic.all
389
    @errors.cyclades.connection
390
    @errors.cyclades.server_id
391
    @errors.cyclades.metadata
392
    def _run(self, server_id, key=''):
393
        self._print(
394
            self.client.get_server_metadata(int(server_id), key), print_dict)
395

    
396
    def main(self, server_id, key=''):
397
        super(self.__class__, self)._run()
398
        self._run(server_id=server_id, key=key)
399

    
400

    
401
@command(server_cmds)
402
class server_metadata_set(_init_cyclades, _optional_json):
403
    """Set / update server(VM) metadata
404
    Metadata should be given in key/value pairs in key=value format
405
    For example:
406
        /server metadata set <server id> key1=value1 key2=value2
407
    Old, unreferenced metadata will remain intact
408
    """
409

    
410
    @errors.generic.all
411
    @errors.cyclades.connection
412
    @errors.cyclades.server_id
413
    def _run(self, server_id, keyvals):
414
        assert keyvals, 'Please, add some metadata ( key=value)'
415
        metadata = dict()
416
        for keyval in keyvals:
417
            k, sep, v = keyval.partition('=')
418
            if sep and k:
419
                metadata[k] = v
420
            else:
421
                raiseCLIError(
422
                    'Invalid piece of metadata %s' % keyval,
423
                    importance=2, details=[
424
                        'Correct metadata format: key=val',
425
                        'For example:',
426
                        '/server metadata set <server id>'
427
                        'key1=value1 key2=value2'])
428
        self._print(
429
            self.client.update_server_metadata(int(server_id), **metadata),
430
            print_dict)
431

    
432
    def main(self, server_id, *key_equals_val):
433
        super(self.__class__, self)._run()
434
        self._run(server_id=server_id, keyvals=key_equals_val)
435

    
436

    
437
@command(server_cmds)
438
class server_metadata_delete(_init_cyclades, _optional_output_cmd):
439
    """Delete server (VM) metadata"""
440

    
441
    @errors.generic.all
442
    @errors.cyclades.connection
443
    @errors.cyclades.server_id
444
    @errors.cyclades.metadata
445
    def _run(self, server_id, key):
446
        self._optional_output(
447
            self.client.delete_server_metadata(int(server_id), key))
448

    
449
    def main(self, server_id, key):
450
        super(self.__class__, self)._run()
451
        self._run(server_id=server_id, key=key)
452

    
453

    
454
@command(server_cmds)
455
class server_stats(_init_cyclades, _optional_json):
456
    """Get server (VM) statistics"""
457

    
458
    @errors.generic.all
459
    @errors.cyclades.connection
460
    @errors.cyclades.server_id
461
    def _run(self, server_id):
462
        self._print(self.client.get_server_stats(int(server_id)), print_dict)
463

    
464
    def main(self, server_id):
465
        super(self.__class__, self)._run()
466
        self._run(server_id=server_id)
467

    
468

    
469
@command(server_cmds)
470
class server_wait(_init_cyclades):
471
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
472

    
473
    arguments = dict(
474
        progress_bar=ProgressBarArgument(
475
            'do not show progress bar',
476
            ('-N', '--no-progress-bar'),
477
            False
478
        )
479
    )
480

    
481
    @errors.generic.all
482
    @errors.cyclades.connection
483
    @errors.cyclades.server_id
484
    def _run(self, server_id, currect_status):
485
        (progress_bar, wait_cb) = self._safe_progress_bar(
486
            'Server %s still in %s mode' % (server_id, currect_status))
487

    
488
        try:
489
            new_mode = self.client.wait_server(
490
                server_id,
491
                currect_status,
492
                wait_cb=wait_cb)
493
        except Exception:
494
            self._safe_progress_bar_finish(progress_bar)
495
            raise
496
        finally:
497
            self._safe_progress_bar_finish(progress_bar)
498
        if new_mode:
499
            print('Server %s is now in %s mode' % (server_id, new_mode))
500
        else:
501
            raiseCLIError(None, 'Time out')
502

    
503
    def main(self, server_id, currect_status='BUILD'):
504
        super(self.__class__, self)._run()
505
        self._run(server_id=server_id, currect_status=currect_status)
506

    
507

    
508
@command(flavor_cmds)
509
class flavor_list(_init_cyclades, _optional_json):
510
    """List available hardware flavors"""
511

    
512
    arguments = dict(
513
        detail=FlagArgument('show detailed output', ('-l', '--details')),
514
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
515
        more=FlagArgument(
516
            'output results in pages (-n to set items per page, default 10)',
517
            '--more'),
518
        enum=FlagArgument('Enumerate results', '--enumerate')
519
    )
520

    
521
    @errors.generic.all
522
    @errors.cyclades.connection
523
    def _run(self):
524
        flavors = self.client.list_flavors(self['detail'])
525
        pg_size = 10 if self['more'] and not self['limit'] else self['limit']
526
        self._print(
527
            flavors,
528
            with_redundancy=self['detail'],
529
            page_size=pg_size,
530
            with_enumeration=self['enum'])
531

    
532
    def main(self):
533
        super(self.__class__, self)._run()
534
        self._run()
535

    
536

    
537
@command(flavor_cmds)
538
class flavor_info(_init_cyclades, _optional_json):
539
    """Detailed information on a hardware flavor
540
    To get a list of available flavors and flavor ids, try /flavor list
541
    """
542

    
543
    @errors.generic.all
544
    @errors.cyclades.connection
545
    @errors.cyclades.flavor_id
546
    def _run(self, flavor_id):
547
        self._print(
548
            self.client.get_flavor_details(int(flavor_id)), print_dict)
549

    
550
    def main(self, flavor_id):
551
        super(self.__class__, self)._run()
552
        self._run(flavor_id=flavor_id)
553

    
554

    
555
@command(network_cmds)
556
class network_info(_init_cyclades, _optional_json):
557
    """Detailed information on a network
558
    To get a list of available networks and network ids, try /network list
559
    """
560

    
561
    @errors.generic.all
562
    @errors.cyclades.connection
563
    @errors.cyclades.network_id
564
    def _run(self, network_id):
565
        network = self.client.get_network_details(int(network_id))
566
        self._print(network, print_dict, exclude=('id'))
567

    
568
    def main(self, network_id):
569
        super(self.__class__, self)._run()
570
        self._run(network_id=network_id)
571

    
572

    
573
@command(network_cmds)
574
class network_list(_init_cyclades, _optional_json):
575
    """List networks"""
576

    
577
    arguments = dict(
578
        detail=FlagArgument('show detailed output', ('-l', '--details')),
579
        limit=IntArgument('limit # of listed networks', ('-n', '--number')),
580
        more=FlagArgument(
581
            'output results in pages (-n to set items per page, default 10)',
582
            '--more'),
583
        enum=FlagArgument('Enumerate results', '--enumerate')
584
    )
585

    
586
    @errors.generic.all
587
    @errors.cyclades.connection
588
    def _run(self):
589
        networks = self.client.list_networks(self['detail'])
590
        kwargs = dict(with_enumeration=self['enum'])
591
        if self['more']:
592
            kwargs['page_size'] = self['limit'] or 10
593
        elif self['limit']:
594
            networks = networks[:self['limit']]
595
        self._print(networks, **kwargs)
596

    
597
    def main(self):
598
        super(self.__class__, self)._run()
599
        self._run()
600

    
601

    
602
@command(network_cmds)
603
class network_create(_init_cyclades, _optional_json):
604
    """Create an (unconnected) network"""
605

    
606
    arguments = dict(
607
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
608
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
609
        dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
610
        type=ValueArgument(
611
            'Valid network types are '
612
            'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
613
            '--with-type',
614
            default='MAC_FILTERED')
615
    )
616

    
617
    @errors.generic.all
618
    @errors.cyclades.connection
619
    @errors.cyclades.network_max
620
    def _run(self, name):
621
        self._print(self.client.create_network(
622
            name,
623
            cidr=self['cidr'],
624
            gateway=self['gateway'],
625
            dhcp=self['dhcp'],
626
            type=self['type']), print_dict)
627

    
628
    def main(self, name):
629
        super(self.__class__, self)._run()
630
        self._run(name)
631

    
632

    
633
@command(network_cmds)
634
class network_rename(_init_cyclades, _optional_output_cmd):
635
    """Set the name of a network"""
636

    
637
    @errors.generic.all
638
    @errors.cyclades.connection
639
    @errors.cyclades.network_id
640
    def _run(self, network_id, new_name):
641
        self._optional_output(
642
                self.client.update_network_name(int(network_id), new_name))
643

    
644
    def main(self, network_id, new_name):
645
        super(self.__class__, self)._run()
646
        self._run(network_id=network_id, new_name=new_name)
647

    
648

    
649
@command(network_cmds)
650
class network_delete(_init_cyclades, _optional_output_cmd):
651
    """Delete a network"""
652

    
653
    @errors.generic.all
654
    @errors.cyclades.connection
655
    @errors.cyclades.network_id
656
    @errors.cyclades.network_in_use
657
    def _run(self, network_id):
658
        self._optional_output(self.client.delete_network(int(network_id)))
659

    
660
    def main(self, network_id):
661
        super(self.__class__, self)._run()
662
        self._run(network_id=network_id)
663

    
664

    
665
@command(network_cmds)
666
class network_connect(_init_cyclades, _optional_output_cmd):
667
    """Connect a server to a network"""
668

    
669
    @errors.generic.all
670
    @errors.cyclades.connection
671
    @errors.cyclades.server_id
672
    @errors.cyclades.network_id
673
    def _run(self, server_id, network_id):
674
        self._optional_output(
675
                self.client.connect_server(int(server_id), int(network_id)))
676

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

    
681

    
682
@command(network_cmds)
683
class network_disconnect(_init_cyclades):
684
    """Disconnect a nic that connects a server to a network
685
    Nic ids are listed as "attachments" in detailed network information
686
    To get detailed network information: /network info <network id>
687
    """
688

    
689
    @errors.cyclades.nic_format
690
    def _server_id_from_nic(self, nic_id):
691
        return nic_id.split('-')[1]
692

    
693
    @errors.generic.all
694
    @errors.cyclades.connection
695
    @errors.cyclades.server_id
696
    @errors.cyclades.nic_id
697
    def _run(self, nic_id, server_id):
698
        num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
699
        if not num_of_disconnected:
700
            raise ClientError(
701
                'Network Interface %s not found on server %s' % (
702
                    nic_id,
703
                    server_id),
704
                status=404)
705
        print('Disconnected %s connections' % num_of_disconnected)
706

    
707
    def main(self, nic_id):
708
        super(self.__class__, self)._run()
709
        server_id = self._server_id_from_nic(nic_id=nic_id)
710
        self._run(nic_id=nic_id, server_id=server_id)