Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (23 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('compute', 'url')\
81
                or self.config.get('cyclades', 'url')
82
        if not base_url:
83
            raise CLIBaseUrlError(service='cyclades')
84

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

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

    
92

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

    
97
    __doc__ += about_authentication
98

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

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

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

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

    
128

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

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

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

    
149

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

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

    
183

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

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

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

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

    
212

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

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

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

    
230

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

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

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

    
245

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

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

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

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

    
265

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

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

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

    
280

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

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

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

    
295

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

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

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

    
316

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

    
321

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

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

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

    
343

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

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

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

    
358

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

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

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

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

    
379

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

    
384

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

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

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

    
401

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

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

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

    
437

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

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

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

    
454

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

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

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

    
469

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

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

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

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

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

    
508

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

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

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

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

    
537

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

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

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

    
555

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

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

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

    
573

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

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

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

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

    
602

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

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

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

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

    
633

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

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

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

    
649

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

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

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

    
665

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

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

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

    
682

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

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

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

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