Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / network.py @ 69565935

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 io import StringIO
35
from pydoc import pager
36

    
37
from kamaki.cli import command
38
from kamaki.cli.command_tree import CommandTree
39
from kamaki.cli.errors import (
40
    CLIBaseUrlError, CLIInvalidArgument, raiseCLIError)
41
from kamaki.clients.cyclades import CycladesNetworkClient, ClientError
42
from kamaki.cli.argument import (
43
    FlagArgument, ValueArgument, RepeatableArgument, IntArgument)
44
from kamaki.cli.commands import _command_init, errors, addLogSettings
45
from kamaki.cli.commands import (
46
    _optional_output_cmd, _optional_json, _name_filter, _id_filter)
47
from kamaki.cli.commands.cyclades import _service_wait
48

    
49

    
50
network_cmds = CommandTree('network', 'Networking API network commands')
51
port_cmds = CommandTree('port', 'Networking API network commands')
52
subnet_cmds = CommandTree('subnet', 'Networking API network commands')
53
ip_cmds = CommandTree('ip', 'Networking API floatingip commands')
54
_commands = [network_cmds, port_cmds, subnet_cmds, ip_cmds]
55

    
56

    
57
about_authentication = '\nUser Authentication:\
58
    \n* to check authentication: /user authenticate\
59
    \n* to set authentication token: /config set cloud.<cloud>.token <token>'
60

    
61

    
62
class _port_wait(_service_wait):
63

    
64
    def _wait(self, port_id, current_status, timeout=60):
65
        super(_port_wait, self)._wait(
66
            'Port', port_id, self.client.wait_port, current_status,
67
            timeout=timeout)
68

    
69

    
70
class _init_network(_command_init):
71
    @errors.generic.all
72
    @addLogSettings
73
    def _run(self, service='network'):
74
        if getattr(self, 'cloud', None):
75
            base_url = self._custom_url(service) or self._custom_url(
76
                'network')
77
            if base_url:
78
                token = self._custom_token(service) or self._custom_token(
79
                    'network') or self.config.get_cloud('token')
80
                self.client = CycladesNetworkClient(
81
                  base_url=base_url, token=token)
82
                return
83
        else:
84
            self.cloud = 'default'
85
        if getattr(self, 'auth_base', False):
86
            network_endpoints = self.auth_base.get_service_endpoints(
87
                self._custom_type('network') or 'network',
88
                self._custom_version('network') or '')
89
            base_url = network_endpoints['publicURL']
90
            token = self.auth_base.token
91
            self.client = CycladesNetworkClient(base_url=base_url, token=token)
92
        else:
93
            raise CLIBaseUrlError(service='network')
94

    
95
    def _filter_by_user_id(self, nets):
96
        return [net for net in nets if net['user_id'] == self['user_id']] if (
97
            self['user_id']) else nets
98

    
99
    def main(self):
100
        self._run()
101

    
102

    
103
@command(network_cmds)
104
class network_list(_init_network, _optional_json, _name_filter, _id_filter):
105
    """List networks
106
    Use filtering arguments (e.g., --name-like) to manage long server lists
107
    """
108

    
109
    arguments = dict(
110
        detail=FlagArgument('show detailed output', ('-l', '--details')),
111
        more=FlagArgument(
112
            'output results in pages (-n to set items per page, default 10)',
113
            '--more'),
114
        user_id=ValueArgument(
115
            'show only networks belonging to user with this id', '--user-id')
116
    )
117

    
118
    @errors.generic.all
119
    @errors.cyclades.connection
120
    def _run(self):
121
        detail = bool(self['detail'] or self['user_id'])
122
        nets = self.client.list_networks(detail=detail)
123
        nets = self._filter_by_user_id(nets)
124
        nets = self._filter_by_name(nets)
125
        nets = self._filter_by_id(nets)
126
        if detail and not self['detail']:
127
            nets = [dict(
128
                id=n['id'], name=n['name'], links=n['links']) for n in nets]
129
        kwargs = dict()
130
        if self['more']:
131
            kwargs['out'] = StringIO()
132
            kwargs['title'] = ()
133
        self._print(nets, **kwargs)
134
        if self['more']:
135
            pager(kwargs['out'].getvalue())
136

    
137
    def main(self):
138
        super(self.__class__, self)._run()
139
        self._run()
140

    
141

    
142
@command(network_cmds)
143
class network_info(_init_network, _optional_json):
144
    """Get details about a network"""
145

    
146
    @errors.generic.all
147
    @errors.cyclades.connection
148
    @errors.cyclades.network_id
149
    def _run(self, network_id):
150
        net = self.client.get_network_details(network_id)
151
        self._print(net, self.print_dict)
152

    
153
    def main(self, network_id):
154
        super(self.__class__, self)._run()
155
        self._run(network_id=network_id)
156

    
157

    
158
class NetworkTypeArgument(ValueArgument):
159

    
160
    types = ('MAC_FILTERED', 'CUSTOM', 'IP_LESS_ROUTED', 'PHYSICAL_VLAN')
161

    
162
    @property
163
    def value(self):
164
        return getattr(self, '_value', self.types[0])
165

    
166
    @value.setter
167
    def value(self, new_value):
168
        if new_value and new_value.upper() in self.types:
169
            self._value = new_value.upper()
170
        elif new_value:
171
            raise CLIInvalidArgument(
172
                'Invalid network type %s' % new_value, details=[
173
                    'Valid types: %s' % ', '.join(self.types), ])
174

    
175

    
176
@command(network_cmds)
177
class network_create(_init_network, _optional_json):
178
    """Create a new network (default type: MAC_FILTERED)"""
179

    
180
    arguments = dict(
181
        name=ValueArgument('Network name', '--name'),
182
        shared=FlagArgument(
183
            'Make network shared (special privileges required)', '--shared'),
184
        network_type=NetworkTypeArgument(
185
            'Valid network types: %s' % (', '.join(NetworkTypeArgument.types)),
186
            '--type')
187
    )
188

    
189
    @errors.generic.all
190
    @errors.cyclades.connection
191
    @errors.cyclades.network_type
192
    def _run(self, network_type):
193
        net = self.client.create_network(
194
            network_type, name=self['name'], shared=self['shared'])
195
        self._print(net, self.print_dict)
196

    
197
    def main(self):
198
        super(self.__class__, self)._run()
199
        self._run(network_type=self['network_type'])
200

    
201

    
202
@command(network_cmds)
203
class network_delete(_init_network, _optional_output_cmd):
204
    """Delete a network"""
205

    
206
    @errors.generic.all
207
    @errors.cyclades.connection
208
    @errors.cyclades.network_id
209
    def _run(self, network_id):
210
        r = self.client.delete_network(network_id)
211
        self._optional_output(r)
212

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

    
217

    
218
@command(network_cmds)
219
class network_modify(_init_network, _optional_json):
220
    """Modify network attributes"""
221

    
222
    arguments = dict(new_name=ValueArgument('Rename the network', '--name'))
223
    required = ['new_name', ]
224

    
225
    @errors.generic.all
226
    @errors.cyclades.connection
227
    @errors.cyclades.network_id
228
    def _run(self, network_id):
229
        r = self.client.update_network(network_id, name=self['new_name'])
230
        self._print(r, self.print_dict)
231

    
232
    def main(self, network_id):
233
        super(self.__class__, self)._run()
234
        self._run(network_id=network_id)
235

    
236

    
237
@command(subnet_cmds)
238
class subnet_list(_init_network, _optional_json, _name_filter, _id_filter):
239
    """List subnets
240
    Use filtering arguments (e.g., --name-like) to manage long server lists
241
    """
242

    
243
    arguments = dict(
244
        detail=FlagArgument('show detailed output', ('-l', '--details')),
245
        more=FlagArgument(
246
            'output results in pages (-n to set items per page, default 10)',
247
            '--more')
248
    )
249

    
250
    @errors.generic.all
251
    @errors.cyclades.connection
252
    def _run(self):
253
        nets = self.client.list_subnets()
254
        nets = self._filter_by_name(nets)
255
        nets = self._filter_by_id(nets)
256
        if not self['detail']:
257
            nets = [dict(
258
                id=n['id'], name=n['name'], links=n['links']) for n in nets]
259
        kwargs = dict()
260
        if self['more']:
261
            kwargs['out'] = StringIO()
262
            kwargs['title'] = ()
263
        self._print(nets, **kwargs)
264
        if self['more']:
265
            pager(kwargs['out'].getvalue())
266

    
267
    def main(self):
268
        super(self.__class__, self)._run()
269
        self._run()
270

    
271

    
272
@command(subnet_cmds)
273
class subnet_info(_init_network, _optional_json):
274
    """Get details about a subnet"""
275

    
276
    @errors.generic.all
277
    @errors.cyclades.connection
278
    def _run(self, subnet_id):
279
        net = self.client.get_subnet_details(subnet_id)
280
        self._print(net, self.print_dict)
281

    
282
    def main(self, subnet_id):
283
        super(self.__class__, self)._run()
284
        self._run(subnet_id=subnet_id)
285

    
286

    
287
class AllocationPoolArgument(RepeatableArgument):
288

    
289
    @property
290
    def value(self):
291
        return super(AllocationPoolArgument, self).value or []
292

    
293
    @value.setter
294
    def value(self, new_pools):
295
        new_list = []
296
        for pool in new_pools:
297
            start, comma, end = pool.partition(',')
298
            if not (start and comma and end):
299
                raise CLIInvalidArgument(
300
                    'Invalid allocation pool argument %s' % pool, details=[
301
                    'Allocation values must be of the form:',
302
                    '  <start address>,<end address>'])
303
            new_list.append(dict(start=start, end=end))
304
        self._value = new_list
305

    
306

    
307
@command(subnet_cmds)
308
class subnet_create(_init_network, _optional_json):
309
    """Create a new subnet"""
310

    
311
    arguments = dict(
312
        name=ValueArgument('Subnet name', '--name'),
313
        allocation_pools=AllocationPoolArgument(
314
            'start_address,end_address of allocation pool (can be repeated)'
315
            ' e.g., --alloc-pool=123.45.67.1,123.45.67.8',
316
            '--alloc-pool'),
317
        gateway=ValueArgument('Gateway IP', '--gateway'),
318
        subnet_id=ValueArgument('The id for the subnet', '--id'),
319
        ipv6=FlagArgument('If set, IP version is set to 6, else 4', '--ipv6'),
320
        enable_dhcp=FlagArgument('Enable dhcp (default: off)', '--with-dhcp'),
321
        network_id=ValueArgument('Set the network ID', '--network-id'),
322
        cidr=ValueArgument('Set the CIDR', '--cidr')
323
    )
324
    required = ('network_id', 'cidr')
325

    
326
    @errors.generic.all
327
    @errors.cyclades.connection
328
    @errors.cyclades.network_id
329
    def _run(self, network_id, cidr):
330
        net = self.client.create_subnet(
331
            network_id, cidr,
332
            self['name'], self['allocation_pools'], self['gateway'],
333
            self['subnet_id'], self['ipv6'], self['enable_dhcp'])
334
        self._print(net, self.print_dict)
335

    
336
    def main(self):
337
        super(self.__class__, self)._run()
338
        self._run(network_id=self['network_id'], cidr=self['cidr'])
339

    
340

    
341
# @command(subnet_cmds)
342
# class subnet_delete(_init_network, _optional_output_cmd):
343
#     """Delete a subnet"""
344

    
345
#     @errors.generic.all
346
#     @errors.cyclades.connection
347
#     def _run(self, subnet_id):
348
#         r = self.client.delete_subnet(subnet_id)
349
#         self._optional_output(r)
350

    
351
#     def main(self, subnet_id):
352
#         super(self.__class__, self)._run()
353
#         self._run(subnet_id=subnet_id)
354

    
355

    
356
@command(subnet_cmds)
357
class subnet_modify(_init_network, _optional_json):
358
    """Modify the attributes of a subnet"""
359

    
360
    arguments = dict(
361
        new_name=ValueArgument('New name of the subnet', '--name')
362
    )
363
    required = ['new_name']
364

    
365
    @errors.generic.all
366
    @errors.cyclades.connection
367
    def _run(self, subnet_id):
368
        r = self.client.get_subnet_details(subnet_id)
369
        r = self.client.update_subnet(
370
            subnet_id, r['network_id'], name=self['new_name'])
371
        self._print(r, self.print_dict)
372

    
373
    def main(self, subnet_id):
374
        super(self.__class__, self)._run()
375
        self._run(subnet_id=subnet_id)
376

    
377

    
378
@command(port_cmds)
379
class port_list(_init_network, _optional_json, _name_filter, _id_filter):
380
    """List all ports"""
381

    
382
    arguments = dict(
383
        detail=FlagArgument('show detailed output', ('-l', '--details')),
384
        more=FlagArgument(
385
            'output results in pages (-n to set items per page, default 10)',
386
            '--more'),
387
        user_id=ValueArgument(
388
            'show only networks belonging to user with this id', '--user-id')
389
    )
390

    
391
    @errors.generic.all
392
    @errors.cyclades.connection
393
    def _run(self):
394
        detail = bool(self['detail'] or self['user_id'])
395
        ports = self.client.list_ports(detail=detail)
396
        ports = self._filter_by_user_id(ports)
397
        ports = self._filter_by_name(ports)
398
        ports = self._filter_by_id(ports)
399
        if detail and not self['detail']:
400
            ports = [dict(
401
                id=p['id'], name=p['name'], links=p['links']) for p in ports]
402
        kwargs = dict()
403
        if self['more']:
404
            kwargs['out'] = StringIO()
405
            kwargs['title'] = ()
406
        self._print(ports, **kwargs)
407
        if self['more']:
408
            pager(kwargs['out'].getvalue())
409

    
410
    def main(self):
411
        super(self.__class__, self)._run()
412
        self._run()
413

    
414

    
415
@command(port_cmds)
416
class port_info(_init_network, _optional_json):
417
    """Get details about a port"""
418

    
419
    @errors.generic.all
420
    @errors.cyclades.connection
421
    def _run(self, port_id):
422
        port = self.client.get_port_details(port_id)
423
        self._print(port, self.print_dict)
424

    
425
    def main(self, port_id):
426
        super(self.__class__, self)._run()
427
        self._run(port_id=port_id)
428

    
429

    
430
@command(port_cmds)
431
class port_delete(_init_network, _optional_output_cmd, _port_wait):
432
    """Delete a port (== disconnect server from network)"""
433

    
434
    arguments = dict(
435
        wait=FlagArgument('Wait port to be established', ('-w', '--wait'))
436
    )
437

    
438
    @errors.generic.all
439
    @errors.cyclades.connection
440
    def _run(self, port_id):
441
        if self['wait']:
442
            status = self.client.get_port_details(port_id)['status']
443
        r = self.client.delete_port(port_id)
444
        if self['wait']:
445
            try:
446
                self._wait(port_id, status)
447
            except ClientError as ce:
448
                if ce.status not in (404, ):
449
                    raise
450
                self.error('Port %s is deleted' % port_id)
451
        self._optional_output(r)
452

    
453
    def main(self, port_id):
454
        super(self.__class__, self)._run()
455
        self._run(port_id=port_id)
456

    
457

    
458
@command(port_cmds)
459
class port_modify(_init_network, _optional_json):
460
    """Modify the attributes of a port"""
461

    
462
    arguments = dict(new_name=ValueArgument('New name of the port', '--name'))
463
    required = ['new_name', ]
464

    
465
    @errors.generic.all
466
    @errors.cyclades.connection
467
    def _run(self, port_id):
468
        r = self.client.get_port_details(port_id)
469
        r = self.client.update_port(
470
            port_id, r['network_id'], name=self['new_name'])
471
        self._print(r, self.print_dict)
472

    
473
    def main(self, port_id):
474
        super(self.__class__, self)._run()
475
        self._run(port_id=port_id)
476

    
477

    
478
class _port_create(_init_network, _optional_json, _port_wait):
479

    
480
    def connect(self, network_id, device_id):
481
        fixed_ips = [dict(
482
            subnet_id=self['subnet_id'], ip_address=self['ip_address'])] if (
483
                self['subnet_id']) else None
484
        r = self.client.create_port(
485
            network_id, device_id,
486
            name=self['name'],
487
            security_groups=self['security_group_id'],
488
            fixed_ips=fixed_ips)
489
        if self['wait']:
490
            self._wait(r['id'], r['status'])
491
            r = self.client.get_port_details(r['id'])
492
        self._print([r])
493

    
494

    
495
@command(port_cmds)
496
class port_create(_port_create):
497
    """Create a new port (== connect server to network)"""
498

    
499
    arguments = dict(
500
        name=ValueArgument('A human readable name', '--name'),
501
        security_group_id=RepeatableArgument(
502
            'Add a security group id (can be repeated)',
503
            ('-g', '--security-group')),
504
        subnet_id=ValueArgument(
505
            'Subnet id for fixed ips (used with --ip-address)',
506
            '--subnet-id'),
507
        ip_address=ValueArgument(
508
            'IP address for subnet id (used with --subnet-id', '--ip-address'),
509
        network_id=ValueArgument('Set the network ID', '--network-id'),
510
        device_id=ValueArgument(
511
            'The device is either a virtual server or a virtual router',
512
            '--device-id'),
513
        wait=FlagArgument('Wait port to be established', ('-w', '--wait')),
514
    )
515
    required = ('network_id', 'device_id')
516

    
517
    @errors.generic.all
518
    @errors.cyclades.connection
519
    @errors.cyclades.network_id
520
    @errors.cyclades.server_id
521
    def _run(self, network_id, server_id):
522
        self.connect(network_id, server_id)
523

    
524
    def main(self):
525
        super(self.__class__, self)._run()
526
        self._run(network_id=self['network_id'], server_id=self['device_id'])
527

    
528

    
529
@command(port_cmds)
530
class port_wait(_init_network, _port_wait):
531
    """Wait for port to finish [ACTIVE, DOWN, BUILD, ERROR]"""
532

    
533
    arguments = dict(
534
        timeout=IntArgument(
535
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
536
    )
537

    
538
    @errors.generic.all
539
    @errors.cyclades.connection
540
    def _run(self, port_id, current_status):
541
        port = self.client.get_port_details(port_id)
542
        if port['status'].lower() == current_status.lower():
543
            self._wait(port_id, current_status, timeout=self['timeout'])
544
        else:
545
            self.error(
546
                'Port %s: Cannot wait for status %s, '
547
                'status is already %s' % (
548
                    port_id, current_status, port['status']))
549

    
550
    def main(self, port_id, current_status='BUILD'):
551
        super(self.__class__, self)._run()
552
        self._run(port_id=port_id, current_status=current_status)
553

    
554

    
555
@command(ip_cmds)
556
class ip_list(_init_network, _optional_json):
557
    """List reserved floating IPs"""
558

    
559
    @errors.generic.all
560
    @errors.cyclades.connection
561
    def _run(self):
562
        self._print(self.client.list_floatingips())
563

    
564
    def main(self):
565
        super(self.__class__, self)._run()
566
        self._run()
567

    
568

    
569
@command(ip_cmds)
570
class ip_info(_init_network, _optional_json):
571
    """Get details on a floating IP"""
572

    
573
    @errors.generic.all
574
    @errors.cyclades.connection
575
    def _run(self, ip_id):
576
        self._print(
577
            self.client.get_floatingip_details(ip_id), self.print_dict)
578

    
579
    def main(self, ip_id):
580
        super(self.__class__, self)._run()
581
        self._run(ip_id=ip_id)
582

    
583

    
584
@command(ip_cmds)
585
class ip_create(_init_network, _optional_json):
586
    """Reserve an IP on a network"""
587

    
588
    arguments = dict(
589
        network_id=ValueArgument(
590
            'The network to preserve the IP on', '--network-id'),
591
        ip_address=ValueArgument('Allocate a specific IP address', '--address')
592
    )
593
    required = ('network_id', )
594

    
595
    @errors.generic.all
596
    @errors.cyclades.connection
597
    @errors.cyclades.network_id
598
    def _run(self, network_id):
599
        self._print(
600
            self.client.create_floatingip(
601
                network_id, floating_ip_address=self['ip_address']),
602
            self.print_dict)
603

    
604
    def main(self):
605
        super(self.__class__, self)._run()
606
        self._run(network_id=self['network_id'])
607

    
608

    
609
@command(ip_cmds)
610
class ip_delete(_init_network, _optional_output_cmd):
611
    """Unreserve an IP (also delete the port, if attached)"""
612

    
613
    def _run(self, ip_id):
614
        self._optional_output(self.client.delete_floatingip(ip_id))
615

    
616
    def main(self, ip_id):
617
        super(self.__class__, self)._run()
618
        self._run(ip_id=ip_id)
619

    
620

    
621
#  Warn users for some importand changes
622

    
623
@command(network_cmds)
624
class network_connect(_port_create):
625
    """Connect a network with a device (server or router)"""
626

    
627
    arguments = dict(
628
        name=ValueArgument('A human readable name for the port', '--name'),
629
        security_group_id=RepeatableArgument(
630
            'Add a security group id (can be repeated)',
631
            ('-g', '--security-group')),
632
        subnet_id=ValueArgument(
633
            'Subnet id for fixed ips (used with --ip-address)',
634
            '--subnet-id'),
635
        ip_address=ValueArgument(
636
            'IP address for subnet id (used with --subnet-id', '--ip-address'),
637
        wait=FlagArgument('Wait network to connect', ('-w', '--wait')),
638
    )
639

    
640
    @errors.generic.all
641
    @errors.cyclades.connection
642
    @errors.cyclades.network_id
643
    @errors.cyclades.server_id
644
    def _run(self, network_id, server_id):
645
        self.error('Creating a port to connect network %s with device %s' % (
646
            network_id, server_id))
647
        self.connect(network_id, server_id)
648

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

    
653

    
654
@command(network_cmds)
655
class network_disconnect(_init_network, _port_wait, _optional_json):
656
    """Disconnnect a network from a device"""
657

    
658
    def _cyclades_client(self):
659
        auth = getattr(self, 'auth_base')
660
        endpoints = auth.get_service_endpoints('compute')
661
        URL = endpoints['publicURL']
662
        from kamaki.clients.cyclades import CycladesClient
663
        return CycladesClient(URL, self.client.token)
664

    
665
    arguments = dict(
666
        wait=FlagArgument('Wait network to disconnect', ('-w', '--wait'))
667
    )
668

    
669
    @errors.generic.all
670
    @errors.cyclades.connection
671
    @errors.cyclades.network_id
672
    @errors.cyclades.server_id
673
    def _run(self, network_id, server_id):
674
        vm = self._cyclades_client().get_server_details(server_id)
675
        ports = [port for port in vm['attachments'] if (
676
            port['network_id'] not in ('network_id', ))]
677
        if not ports:
678
            raiseCLIError('Network %s is not connected to device %s' % (
679
                network_id, server_id))
680
        for port in ports:
681
            if self['wait']:
682
                port['status'] = self.client.get_port_details(port['id'])[
683
                    'status']
684
            self.client.delete_port(port['id'])
685
            self.error('Deleting port %s:' % port['id'])
686
            self.print_dict(port)
687
            if self['wait']:
688
                try:
689
                    self._wait(port['id'], port['status'])
690
                except ClientError as ce:
691
                    if ce.status not in (404, ):
692
                        raise
693
                    self.error('Port %s is deleted' % port['id'])
694

    
695
    def main(self, network_id, device_id):
696
        super(self.__class__, self)._run()
697
        self._run(network_id=network_id, server_id=device_id)