Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 7ba195e5

History | View | Annotate | Download (23.6 kB)

1
# Copyright 2011-2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from kamaki.cli import command
35
from kamaki.cli.command_tree import CommandTree
36
from kamaki.cli.utils import print_dict, remove_from_items
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
        if not (self['detail'] or self['json_output']):
122
            remove_from_items(servers, 'links')
123

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

    
131
    def main(self):
132
        super(self.__class__, self)._run()
133
        self._run()
134

    
135

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

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

    
152
    def main(self, server_id):
153
        super(self.__class__, self)._run()
154
        self._run(server_id=server_id)
155

    
156

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

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

    
190

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

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

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

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

    
219

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

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

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

    
237

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

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

    
248
    def main(self, server_id):
249
        super(self.__class__, self)._run()
250
        self._run(server_id=server_id)
251

    
252

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

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

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

    
268
    def main(self, server_id):
269
        super(self.__class__, self)._run()
270
        self._run(server_id=server_id)
271

    
272

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

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

    
283
    def main(self, server_id):
284
        super(self.__class__, self)._run()
285
        self._run(server_id=server_id)
286

    
287

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

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

    
298
    def main(self, server_id):
299
        super(self.__class__, self)._run()
300
        self._run(server_id=server_id)
301

    
302

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

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

    
319
    def main(self, server_id):
320
        super(self.__class__, self)._run()
321
        self._run(server_id=server_id)
322

    
323

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

    
328

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

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

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

    
350

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

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

    
361
    def main(self, server_id):
362
        super(self.__class__, self)._run()
363
        self._run(server_id=server_id)
364

    
365

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

    
370
    arguments = dict(
371
        enum=FlagArgument('Enumerate results', '--enumerate')
372
    )
373

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

    
382
    def main(self, server_id):
383
        super(self.__class__, self)._run()
384
        self._run(server_id=server_id)
385

    
386

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

    
391

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

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

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

    
408

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

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

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

    
443

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

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

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

    
460

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

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

    
471
    def main(self, server_id):
472
        super(self.__class__, self)._run()
473
        self._run(server_id=server_id)
474

    
475

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

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

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

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

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

    
514

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

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

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

    
541
    def main(self):
542
        super(self.__class__, self)._run()
543
        self._run()
544

    
545

    
546
@command(flavor_cmds)
547
class flavor_info(_init_cyclades, _optional_json):
548
    """Detailed information on a hardware flavor
549
    To get a list of available flavors and flavor ids, try /flavor list
550
    """
551

    
552
    @errors.generic.all
553
    @errors.cyclades.connection
554
    @errors.cyclades.flavor_id
555
    def _run(self, flavor_id):
556
        self._print(
557
            self.client.get_flavor_details(int(flavor_id)), print_dict)
558

    
559
    def main(self, flavor_id):
560
        super(self.__class__, self)._run()
561
        self._run(flavor_id=flavor_id)
562

    
563

    
564
@command(network_cmds)
565
class network_info(_init_cyclades, _optional_json):
566
    """Detailed information on a network
567
    To get a list of available networks and network ids, try /network list
568
    """
569

    
570
    @errors.generic.all
571
    @errors.cyclades.connection
572
    @errors.cyclades.network_id
573
    def _run(self, network_id):
574
        network = self.client.get_network_details(int(network_id))
575
        self._print(network, print_dict, exclude=('id'))
576

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

    
581

    
582
@command(network_cmds)
583
class network_list(_init_cyclades, _optional_json):
584
    """List networks"""
585

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

    
595
    @errors.generic.all
596
    @errors.cyclades.connection
597
    def _run(self):
598
        networks = self.client.list_networks(self['detail'])
599
        if not (self['detail'] or self['json_output']):
600
            remove_from_items(networks, 'links')
601
        kwargs = dict(with_enumeration=self['enum'])
602
        if self['more']:
603
            kwargs['page_size'] = self['limit'] or 10
604
        elif self['limit']:
605
            networks = networks[:self['limit']]
606
        self._print(networks, **kwargs)
607

    
608
    def main(self):
609
        super(self.__class__, self)._run()
610
        self._run()
611

    
612

    
613
@command(network_cmds)
614
class network_create(_init_cyclades, _optional_json):
615
    """Create an (unconnected) network"""
616

    
617
    arguments = dict(
618
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
619
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
620
        dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
621
        type=ValueArgument(
622
            'Valid network types are '
623
            'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
624
            '--with-type',
625
            default='MAC_FILTERED')
626
    )
627

    
628
    @errors.generic.all
629
    @errors.cyclades.connection
630
    @errors.cyclades.network_max
631
    def _run(self, name):
632
        self._print(self.client.create_network(
633
            name,
634
            cidr=self['cidr'],
635
            gateway=self['gateway'],
636
            dhcp=self['dhcp'],
637
            type=self['type']), print_dict)
638

    
639
    def main(self, name):
640
        super(self.__class__, self)._run()
641
        self._run(name)
642

    
643

    
644
@command(network_cmds)
645
class network_rename(_init_cyclades, _optional_output_cmd):
646
    """Set the name of a network"""
647

    
648
    @errors.generic.all
649
    @errors.cyclades.connection
650
    @errors.cyclades.network_id
651
    def _run(self, network_id, new_name):
652
        self._optional_output(
653
                self.client.update_network_name(int(network_id), new_name))
654

    
655
    def main(self, network_id, new_name):
656
        super(self.__class__, self)._run()
657
        self._run(network_id=network_id, new_name=new_name)
658

    
659

    
660
@command(network_cmds)
661
class network_delete(_init_cyclades, _optional_output_cmd):
662
    """Delete a network"""
663

    
664
    @errors.generic.all
665
    @errors.cyclades.connection
666
    @errors.cyclades.network_id
667
    @errors.cyclades.network_in_use
668
    def _run(self, network_id):
669
        self._optional_output(self.client.delete_network(int(network_id)))
670

    
671
    def main(self, network_id):
672
        super(self.__class__, self)._run()
673
        self._run(network_id=network_id)
674

    
675

    
676
@command(network_cmds)
677
class network_connect(_init_cyclades, _optional_output_cmd):
678
    """Connect a server to a network"""
679

    
680
    @errors.generic.all
681
    @errors.cyclades.connection
682
    @errors.cyclades.server_id
683
    @errors.cyclades.network_id
684
    def _run(self, server_id, network_id):
685
        self._optional_output(
686
                self.client.connect_server(int(server_id), int(network_id)))
687

    
688
    def main(self, server_id, network_id):
689
        super(self.__class__, self)._run()
690
        self._run(server_id=server_id, network_id=network_id)
691

    
692

    
693
@command(network_cmds)
694
class network_disconnect(_init_cyclades):
695
    """Disconnect a nic that connects a server to a network
696
    Nic ids are listed as "attachments" in detailed network information
697
    To get detailed network information: /network info <network id>
698
    """
699

    
700
    @errors.cyclades.nic_format
701
    def _server_id_from_nic(self, nic_id):
702
        return nic_id.split('-')[1]
703

    
704
    @errors.generic.all
705
    @errors.cyclades.connection
706
    @errors.cyclades.server_id
707
    @errors.cyclades.nic_id
708
    def _run(self, nic_id, server_id):
709
        num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
710
        if not num_of_disconnected:
711
            raise ClientError(
712
                'Network Interface %s not found on server %s' % (
713
                    nic_id,
714
                    server_id),
715
                status=404)
716
        print('Disconnected %s connections' % num_of_disconnected)
717

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