Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 92e089e4

History | View | Annotate | Download (23.2 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, addLogSettings
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 cloud.<cloud>.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
    @addLogSettings
71
    def _run(self, service='compute'):
72
        if getattr(self, 'cloud', None):
73
            base_url = self._custom_url(service)\
74
                or self._custom_url('cyclades')
75
            if base_url:
76
                token = self._custom_token(service)\
77
                    or self._custom_token('cyclades')\
78
                    or self.config.get_cloud('token')
79
                self.client = CycladesClient(
80
                    base_url=base_url, token=token)
81
                return
82
        else:
83
            self.cloud = 'default'
84
        if getattr(self, 'auth_base', False):
85
            cyclades_endpoints = self.auth_base.get_service_endpoints(
86
                self._custom_type('cyclades') or 'compute',
87
                self._custom_version('cyclades') or '')
88
            base_url = cyclades_endpoints['publicURL']
89
            token = self.auth_base.token
90
            self.client = CycladesClient(base_url=base_url, token=token)
91
        else:
92
            raise CLIBaseUrlError(service='cyclades')
93

    
94
    def main(self):
95
        self._run()
96

    
97

    
98
@command(server_cmds)
99
class server_list(_init_cyclades, _optional_json):
100
    """List Virtual Machines accessible by user"""
101

    
102
    __doc__ += about_authentication
103

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

    
116
    @errors.generic.all
117
    @errors.cyclades.connection
118
    @errors.cyclades.date
119
    def _run(self):
120
        servers = self.client.list_servers(self['detail'], self['since'])
121

    
122
        kwargs = dict(with_enumeration=self['enum'])
123
        if self['more']:
124
            kwargs['page_size'] = self['limit'] if self['limit'] else 10
125
        elif self['limit']:
126
            servers = servers[:self['limit']]
127
        self._print(servers, **kwargs)
128

    
129
    def main(self):
130
        super(self.__class__, self)._run()
131
        self._run()
132

    
133

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

    
144
    @errors.generic.all
145
    @errors.cyclades.connection
146
    @errors.cyclades.server_id
147
    def _run(self, server_id):
148
        self._print(self.client.get_server_details(server_id), print_dict)
149

    
150
    def main(self, server_id):
151
        super(self.__class__, self)._run()
152
        self._run(server_id=server_id)
153

    
154

    
155
class PersonalityArgument(KeyValueArgument):
156
    @property
157
    def value(self):
158
        return self._value if hasattr(self, '_value') else []
159

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

    
188

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

    
198
    arguments = dict(
199
        personality=PersonalityArgument(
200
            (80 * ' ').join(howto_personality), ('-p', '--personality'))
201
    )
202

    
203
    @errors.generic.all
204
    @errors.cyclades.connection
205
    @errors.plankton.id
206
    @errors.cyclades.flavor_id
207
    def _run(self, name, flavor_id, image_id):
208
        self._print(
209
            self.client.create_server(
210
                name, int(flavor_id), image_id, self['personality']),
211
            print_dict)
212

    
213
    def main(self, name, flavor_id, image_id):
214
        super(self.__class__, self)._run()
215
        self._run(name=name, flavor_id=flavor_id, image_id=image_id)
216

    
217

    
218
@command(server_cmds)
219
class server_rename(_init_cyclades, _optional_output_cmd):
220
    """Set/update a server (VM) name
221
    VM names are not unique, therefore multiple servers may share the same name
222
    """
223

    
224
    @errors.generic.all
225
    @errors.cyclades.connection
226
    @errors.cyclades.server_id
227
    def _run(self, server_id, new_name):
228
        self._optional_output(
229
            self.client.update_server_name(int(server_id), new_name))
230

    
231
    def main(self, server_id, new_name):
232
        super(self.__class__, self)._run()
233
        self._run(server_id=server_id, new_name=new_name)
234

    
235

    
236
@command(server_cmds)
237
class server_delete(_init_cyclades, _optional_output_cmd):
238
    """Delete a server (VM)"""
239

    
240
    @errors.generic.all
241
    @errors.cyclades.connection
242
    @errors.cyclades.server_id
243
    def _run(self, server_id):
244
            self._optional_output(self.client.delete_server(int(server_id)))
245

    
246
    def main(self, server_id):
247
        super(self.__class__, self)._run()
248
        self._run(server_id=server_id)
249

    
250

    
251
@command(server_cmds)
252
class server_reboot(_init_cyclades, _optional_output_cmd):
253
    """Reboot a server (VM)"""
254

    
255
    arguments = dict(
256
        hard=FlagArgument('perform a hard reboot', ('-f', '--force'))
257
    )
258

    
259
    @errors.generic.all
260
    @errors.cyclades.connection
261
    @errors.cyclades.server_id
262
    def _run(self, server_id):
263
        self._optional_output(
264
            self.client.reboot_server(int(server_id), self['hard']))
265

    
266
    def main(self, server_id):
267
        super(self.__class__, self)._run()
268
        self._run(server_id=server_id)
269

    
270

    
271
@command(server_cmds)
272
class server_start(_init_cyclades, _optional_output_cmd):
273
    """Start an existing server (VM)"""
274

    
275
    @errors.generic.all
276
    @errors.cyclades.connection
277
    @errors.cyclades.server_id
278
    def _run(self, server_id):
279
        self._optional_output(self.client.start_server(int(server_id)))
280

    
281
    def main(self, server_id):
282
        super(self.__class__, self)._run()
283
        self._run(server_id=server_id)
284

    
285

    
286
@command(server_cmds)
287
class server_shutdown(_init_cyclades, _optional_output_cmd):
288
    """Shutdown an active server (VM)"""
289

    
290
    @errors.generic.all
291
    @errors.cyclades.connection
292
    @errors.cyclades.server_id
293
    def _run(self, server_id):
294
        self._optional_output(self.client.shutdown_server(int(server_id)))
295

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

    
300

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

    
310
    @errors.generic.all
311
    @errors.cyclades.connection
312
    @errors.cyclades.server_id
313
    def _run(self, server_id):
314
        self._print(
315
            self.client.get_server_console(int(server_id)), print_dict)
316

    
317
    def main(self, server_id):
318
        super(self.__class__, self)._run()
319
        self._run(server_id=server_id)
320

    
321

    
322
@command(server_cmds)
323
class server_firewall(_init_cyclades):
324
    """Manage server (VM) firewall profiles for public networks"""
325

    
326

    
327
@command(server_cmds)
328
class server_firewall_set(_init_cyclades, _optional_output_cmd):
329
    """Set the server (VM) firewall profile on VMs public network
330
    Values for profile:
331
    - DISABLED: Shutdown firewall
332
    - ENABLED: Firewall in normal mode
333
    - PROTECTED: Firewall in secure mode
334
    """
335

    
336
    @errors.generic.all
337
    @errors.cyclades.connection
338
    @errors.cyclades.server_id
339
    @errors.cyclades.firewall
340
    def _run(self, server_id, profile):
341
        self._optional_output(self.client.set_firewall_profile(
342
            server_id=int(server_id), profile=('%s' % profile).upper()))
343

    
344
    def main(self, server_id, profile):
345
        super(self.__class__, self)._run()
346
        self._run(server_id=server_id, profile=profile)
347

    
348

    
349
@command(server_cmds)
350
class server_firewall_get(_init_cyclades):
351
    """Get the server (VM) firewall profile for its public network"""
352

    
353
    @errors.generic.all
354
    @errors.cyclades.connection
355
    @errors.cyclades.server_id
356
    def _run(self, server_id):
357
        print(self.client.get_firewall_profile(server_id))
358

    
359
    def main(self, server_id):
360
        super(self.__class__, self)._run()
361
        self._run(server_id=server_id)
362

    
363

    
364
@command(server_cmds)
365
class server_addr(_init_cyclades, _optional_json):
366
    """List the addresses of all network interfaces on a server (VM)"""
367

    
368
    arguments = dict(
369
        enum=FlagArgument('Enumerate results', '--enumerate')
370
    )
371

    
372
    @errors.generic.all
373
    @errors.cyclades.connection
374
    @errors.cyclades.server_id
375
    def _run(self, server_id):
376
        reply = self.client.list_server_nics(int(server_id))
377
        self._print(
378
            reply, with_enumeration=self['enum'] and len(reply) > 1)
379

    
380
    def main(self, server_id):
381
        super(self.__class__, self)._run()
382
        self._run(server_id=server_id)
383

    
384

    
385
@command(server_cmds)
386
class server_metadata(_init_cyclades):
387
    """Manage Server metadata (key:value pairs of server attributes)"""
388

    
389

    
390
@command(server_cmds)
391
class server_metadata_list(_init_cyclades, _optional_json):
392
    """Get server metadata"""
393

    
394
    @errors.generic.all
395
    @errors.cyclades.connection
396
    @errors.cyclades.server_id
397
    @errors.cyclades.metadata
398
    def _run(self, server_id, key=''):
399
        self._print(
400
            self.client.get_server_metadata(int(server_id), key), print_dict)
401

    
402
    def main(self, server_id, key=''):
403
        super(self.__class__, self)._run()
404
        self._run(server_id=server_id, key=key)
405

    
406

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

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

    
437
    def main(self, server_id, *key_equals_val):
438
        super(self.__class__, self)._run()
439
        self._run(server_id=server_id, keyvals=key_equals_val)
440

    
441

    
442
@command(server_cmds)
443
class server_metadata_delete(_init_cyclades, _optional_output_cmd):
444
    """Delete server (VM) metadata"""
445

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

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

    
458

    
459
@command(server_cmds)
460
class server_stats(_init_cyclades, _optional_json):
461
    """Get server (VM) statistics"""
462

    
463
    @errors.generic.all
464
    @errors.cyclades.connection
465
    @errors.cyclades.server_id
466
    def _run(self, server_id):
467
        self._print(self.client.get_server_stats(int(server_id)), print_dict)
468

    
469
    def main(self, server_id):
470
        super(self.__class__, self)._run()
471
        self._run(server_id=server_id)
472

    
473

    
474
@command(server_cmds)
475
class server_wait(_init_cyclades):
476
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
477

    
478
    arguments = dict(
479
        progress_bar=ProgressBarArgument(
480
            'do not show progress bar',
481
            ('-N', '--no-progress-bar'),
482
            False
483
        )
484
    )
485

    
486
    @errors.generic.all
487
    @errors.cyclades.connection
488
    @errors.cyclades.server_id
489
    def _run(self, server_id, currect_status):
490
        (progress_bar, wait_cb) = self._safe_progress_bar(
491
            'Server %s still in %s mode' % (server_id, currect_status))
492

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

    
508
    def main(self, server_id, currect_status='BUILD'):
509
        super(self.__class__, self)._run()
510
        self._run(server_id=server_id, currect_status=currect_status)
511

    
512

    
513
@command(flavor_cmds)
514
class flavor_list(_init_cyclades, _optional_json):
515
    """List available hardware flavors"""
516

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

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

    
537
    def main(self):
538
        super(self.__class__, self)._run()
539
        self._run()
540

    
541

    
542
@command(flavor_cmds)
543
class flavor_info(_init_cyclades, _optional_json):
544
    """Detailed information on a hardware flavor
545
    To get a list of available flavors and flavor ids, try /flavor list
546
    """
547

    
548
    @errors.generic.all
549
    @errors.cyclades.connection
550
    @errors.cyclades.flavor_id
551
    def _run(self, flavor_id):
552
        self._print(
553
            self.client.get_flavor_details(int(flavor_id)), print_dict)
554

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

    
559

    
560
@command(network_cmds)
561
class network_info(_init_cyclades, _optional_json):
562
    """Detailed information on a network
563
    To get a list of available networks and network ids, try /network list
564
    """
565

    
566
    @errors.generic.all
567
    @errors.cyclades.connection
568
    @errors.cyclades.network_id
569
    def _run(self, network_id):
570
        network = self.client.get_network_details(int(network_id))
571
        self._print(network, print_dict, exclude=('id'))
572

    
573
    def main(self, network_id):
574
        super(self.__class__, self)._run()
575
        self._run(network_id=network_id)
576

    
577

    
578
@command(network_cmds)
579
class network_list(_init_cyclades, _optional_json):
580
    """List networks"""
581

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

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

    
602
    def main(self):
603
        super(self.__class__, self)._run()
604
        self._run()
605

    
606

    
607
@command(network_cmds)
608
class network_create(_init_cyclades, _optional_json):
609
    """Create an (unconnected) network"""
610

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

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

    
633
    def main(self, name):
634
        super(self.__class__, self)._run()
635
        self._run(name)
636

    
637

    
638
@command(network_cmds)
639
class network_rename(_init_cyclades, _optional_output_cmd):
640
    """Set the name of a network"""
641

    
642
    @errors.generic.all
643
    @errors.cyclades.connection
644
    @errors.cyclades.network_id
645
    def _run(self, network_id, new_name):
646
        self._optional_output(
647
                self.client.update_network_name(int(network_id), new_name))
648

    
649
    def main(self, network_id, new_name):
650
        super(self.__class__, self)._run()
651
        self._run(network_id=network_id, new_name=new_name)
652

    
653

    
654
@command(network_cmds)
655
class network_delete(_init_cyclades, _optional_output_cmd):
656
    """Delete a network"""
657

    
658
    @errors.generic.all
659
    @errors.cyclades.connection
660
    @errors.cyclades.network_id
661
    @errors.cyclades.network_in_use
662
    def _run(self, network_id):
663
        self._optional_output(self.client.delete_network(int(network_id)))
664

    
665
    def main(self, network_id):
666
        super(self.__class__, self)._run()
667
        self._run(network_id=network_id)
668

    
669

    
670
@command(network_cmds)
671
class network_connect(_init_cyclades, _optional_output_cmd):
672
    """Connect a server to a network"""
673

    
674
    @errors.generic.all
675
    @errors.cyclades.connection
676
    @errors.cyclades.server_id
677
    @errors.cyclades.network_id
678
    def _run(self, server_id, network_id):
679
        self._optional_output(
680
                self.client.connect_server(int(server_id), int(network_id)))
681

    
682
    def main(self, server_id, network_id):
683
        super(self.__class__, self)._run()
684
        self._run(server_id=server_id, network_id=network_id)
685

    
686

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

    
694
    @errors.cyclades.nic_format
695
    def _server_id_from_nic(self, nic_id):
696
        return nic_id.split('-')[1]
697

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

    
712
    def main(self, nic_id):
713
        super(self.__class__, self)._run()
714
        server_id = self._server_id_from_nic(nic_id=nic_id)
715
        self._run(nic_id=nic_id, server_id=server_id)