b46954c9b410caadb7c0973dd02e0124154b7f91
[kamaki] / kamaki / cli / commands / network.py
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: [kamaki] ]user authenticate\
59     \n  to set authentication token: \
60     [kamaki] config set cloud.<CLOUD>.token <TOKEN>'
61
62
63 class _port_wait(_service_wait):
64
65     def _wait(self, port_id, current_status, timeout=60):
66         super(_port_wait, self)._wait(
67             'Port', port_id, self.client.wait_port, current_status,
68             timeout=timeout)
69
70
71 class _init_network(_command_init):
72     @errors.generic.all
73     @addLogSettings
74     def _run(self, service='network'):
75         if getattr(self, 'cloud', None):
76             base_url = self._custom_url(service) or self._custom_url(
77                 'network')
78             if base_url:
79                 token = self._custom_token(service) or self._custom_token(
80                     'network') or self.config.get_cloud('token')
81                 self.client = CycladesNetworkClient(
82                   base_url=base_url, token=token)
83                 return
84         else:
85             self.cloud = 'default'
86         if getattr(self, 'auth_base', False):
87             network_endpoints = self.auth_base.get_service_endpoints(
88                 self._custom_type('network') or 'network',
89                 self._custom_version('network') or '')
90             base_url = network_endpoints['publicURL']
91             token = self.auth_base.token
92             self.client = CycladesNetworkClient(base_url=base_url, token=token)
93         else:
94             raise CLIBaseUrlError(service='network')
95
96     def _filter_by_user_id(self, nets):
97         return [net for net in nets if net['user_id'] == self['user_id']] if (
98             self['user_id']) else nets
99
100     def main(self):
101         self._run()
102
103
104 @command(network_cmds)
105 class network_list(_init_network, _optional_json, _name_filter, _id_filter):
106     """List networks
107     Use filtering arguments (e.g., --name-like) to manage long server lists
108     """
109
110     arguments = dict(
111         detail=FlagArgument('show detailed output', ('-l', '--details')),
112         more=FlagArgument(
113             'output results in pages (-n to set items per page, default 10)',
114             '--more'),
115         user_id=ValueArgument(
116             'show only networks belonging to user with this id', '--user-id')
117     )
118
119     @errors.generic.all
120     @errors.cyclades.connection
121     def _run(self):
122         nets = self.client.list_networks(detail=True)
123         nets = self._filter_by_user_id(nets)
124         nets = self._filter_by_name(nets)
125         nets = self._filter_by_id(nets)
126         if not self['detail']:
127             nets = [dict(
128                 _0_id=n['id'],
129                 _1_name=n['name'],
130                 _2_public='( %s )' % 'public' if (
131                     n.get('public', None)) else 'private') for n in nets]
132             kwargs = dict(title=('_0_id', '_1_name', '_2_public'))
133         else:
134             kwargs = dict()
135         if self['more']:
136             kwargs['out'] = StringIO()
137             kwargs['title'] = ()
138         self._print(nets, **kwargs)
139         if self['more']:
140             pager(kwargs['out'].getvalue())
141
142     def main(self):
143         super(self.__class__, self)._run()
144         self._run()
145
146
147 @command(network_cmds)
148 class network_info(_init_network, _optional_json):
149     """Get details about a network"""
150
151     @errors.generic.all
152     @errors.cyclades.connection
153     @errors.cyclades.network_id
154     def _run(self, network_id):
155         net = self.client.get_network_details(network_id)
156         self._print(net, self.print_dict)
157
158     def main(self, network_id):
159         super(self.__class__, self)._run()
160         self._run(network_id=network_id)
161
162
163 class NetworkTypeArgument(ValueArgument):
164
165     types = ('MAC_FILTERED', 'CUSTOM', 'IP_LESS_ROUTED', 'PHYSICAL_VLAN')
166
167     @property
168     def value(self):
169         return getattr(self, '_value', self.types[0])
170
171     @value.setter
172     def value(self, new_value):
173         if new_value and new_value.upper() in self.types:
174             self._value = new_value.upper()
175         elif new_value:
176             raise CLIInvalidArgument(
177                 'Invalid network type %s' % new_value, details=[
178                     'Valid types: %s' % ', '.join(self.types), ])
179
180
181 @command(network_cmds)
182 class network_create(_init_network, _optional_json):
183     """Create a new network (default type: MAC_FILTERED)"""
184
185     arguments = dict(
186         name=ValueArgument('Network name', '--name'),
187         shared=FlagArgument(
188             'Make network shared (special privileges required)', '--shared'),
189         network_type=NetworkTypeArgument(
190             'Valid network types: %s' % (', '.join(NetworkTypeArgument.types)),
191             '--type')
192     )
193
194     @errors.generic.all
195     @errors.cyclades.connection
196     @errors.cyclades.network_type
197     def _run(self, network_type):
198         net = self.client.create_network(
199             network_type, name=self['name'], shared=self['shared'])
200         self._print(net, self.print_dict)
201
202     def main(self):
203         super(self.__class__, self)._run()
204         self._run(network_type=self['network_type'])
205
206
207 @command(network_cmds)
208 class network_delete(_init_network, _optional_output_cmd):
209     """Delete a network"""
210
211     @errors.generic.all
212     @errors.cyclades.connection
213     @errors.cyclades.network_id
214     def _run(self, network_id):
215         r = self.client.delete_network(network_id)
216         self._optional_output(r)
217
218     def main(self, network_id):
219         super(self.__class__, self)._run()
220         self._run(network_id=network_id)
221
222
223 @command(network_cmds)
224 class network_modify(_init_network, _optional_json):
225     """Modify network attributes"""
226
227     arguments = dict(new_name=ValueArgument('Rename the network', '--name'))
228     required = ['new_name', ]
229
230     @errors.generic.all
231     @errors.cyclades.connection
232     @errors.cyclades.network_id
233     def _run(self, network_id):
234         r = self.client.update_network(network_id, name=self['new_name'])
235         self._print(r, self.print_dict)
236
237     def main(self, network_id):
238         super(self.__class__, self)._run()
239         self._run(network_id=network_id)
240
241
242 @command(subnet_cmds)
243 class subnet_list(_init_network, _optional_json, _name_filter, _id_filter):
244     """List subnets
245     Use filtering arguments (e.g., --name-like) to manage long server lists
246     """
247
248     arguments = dict(
249         detail=FlagArgument('show detailed output', ('-l', '--details')),
250         more=FlagArgument(
251             'output results in pages (-n to set items per page, default 10)',
252             '--more')
253     )
254
255     @errors.generic.all
256     @errors.cyclades.connection
257     def _run(self):
258         nets = self.client.list_subnets()
259         nets = self._filter_by_name(nets)
260         nets = self._filter_by_id(nets)
261         if not self['detail']:
262             nets = [dict(
263                 _0_id=n['id'],
264                 _1_name=n['name'],
265                 _2_net='( of network %s )' % n['network_id']) for n in nets]
266             kwargs = dict(title=('_0_id', '_1_name', '_2_net'))
267         else:
268             kwargs = dict()
269         if self['more']:
270             kwargs['out'] = StringIO()
271             kwargs['title'] = ()
272         self._print(nets, **kwargs)
273         if self['more']:
274             pager(kwargs['out'].getvalue())
275
276     def main(self):
277         super(self.__class__, self)._run()
278         self._run()
279
280
281 @command(subnet_cmds)
282 class subnet_info(_init_network, _optional_json):
283     """Get details about a subnet"""
284
285     @errors.generic.all
286     @errors.cyclades.connection
287     def _run(self, subnet_id):
288         net = self.client.get_subnet_details(subnet_id)
289         self._print(net, self.print_dict)
290
291     def main(self, subnet_id):
292         super(self.__class__, self)._run()
293         self._run(subnet_id=subnet_id)
294
295
296 class AllocationPoolArgument(RepeatableArgument):
297
298     @property
299     def value(self):
300         return super(AllocationPoolArgument, self).value or []
301
302     @value.setter
303     def value(self, new_pools):
304         if not new_pools:
305             return
306         new_list = []
307         for pool in new_pools:
308             start, comma, end = pool.partition(',')
309             if not (start and comma and end):
310                 raise CLIInvalidArgument(
311                     'Invalid allocation pool argument %s' % pool, details=[
312                     'Allocation values must be of the form:',
313                     '  <start address>,<end address>'])
314             new_list.append(dict(start=start, end=end))
315         self._value = new_list
316
317
318 @command(subnet_cmds)
319 class subnet_create(_init_network, _optional_json):
320     """Create a new subnet"""
321
322     arguments = dict(
323         name=ValueArgument('Subnet name', '--name'),
324         allocation_pools=AllocationPoolArgument(
325             'start_address,end_address of allocation pool (can be repeated)'
326             ' e.g., --alloc-pool=123.45.67.1,123.45.67.8',
327             '--alloc-pool'),
328         gateway=ValueArgument('Gateway IP', '--gateway'),
329         subnet_id=ValueArgument('The id for the subnet', '--id'),
330         ipv6=FlagArgument('If set, IP version is set to 6, else 4', '--ipv6'),
331         enable_dhcp=FlagArgument('Enable dhcp (default: off)', '--with-dhcp'),
332         network_id=ValueArgument('Set the network ID', '--network-id'),
333         cidr=ValueArgument('Set the CIDR', '--cidr')
334     )
335     required = ('network_id', 'cidr')
336
337     @errors.generic.all
338     @errors.cyclades.connection
339     @errors.cyclades.network_id
340     def _run(self, network_id, cidr):
341         net = self.client.create_subnet(
342             network_id, cidr,
343             self['name'], self['allocation_pools'], self['gateway'],
344             self['subnet_id'], self['ipv6'], self['enable_dhcp'])
345         self._print(net, self.print_dict)
346
347     def main(self):
348         super(self.__class__, self)._run()
349         self._run(network_id=self['network_id'], cidr=self['cidr'])
350
351
352 # @command(subnet_cmds)
353 # class subnet_delete(_init_network, _optional_output_cmd):
354 #     """Delete a subnet"""
355
356 #     @errors.generic.all
357 #     @errors.cyclades.connection
358 #     def _run(self, subnet_id):
359 #         r = self.client.delete_subnet(subnet_id)
360 #         self._optional_output(r)
361
362 #     def main(self, subnet_id):
363 #         super(self.__class__, self)._run()
364 #         self._run(subnet_id=subnet_id)
365
366
367 @command(subnet_cmds)
368 class subnet_modify(_init_network, _optional_json):
369     """Modify the attributes of a subnet"""
370
371     arguments = dict(
372         new_name=ValueArgument('New name of the subnet', '--name')
373     )
374     required = ['new_name']
375
376     @errors.generic.all
377     @errors.cyclades.connection
378     def _run(self, subnet_id):
379         r = self.client.update_subnet(subnet_id, name=self['new_name'])
380         self._print(r, self.print_dict)
381
382     def main(self, subnet_id):
383         super(self.__class__, self)._run()
384         self._run(subnet_id=subnet_id)
385
386
387 @command(port_cmds)
388 class port_list(_init_network, _optional_json, _name_filter, _id_filter):
389     """List all ports"""
390
391     arguments = dict(
392         detail=FlagArgument('show detailed output', ('-l', '--details')),
393         more=FlagArgument(
394             'output results in pages (-n to set items per page, default 10)',
395             '--more'),
396         user_id=ValueArgument(
397             'show only networks belonging to user with this id', '--user-id')
398     )
399
400     @errors.generic.all
401     @errors.cyclades.connection
402     def _run(self):
403         detail = bool(self['detail'] or self['user_id'])
404         ports = self.client.list_ports(detail=detail)
405         ports = self._filter_by_user_id(ports)
406         ports = self._filter_by_name(ports)
407         ports = self._filter_by_id(ports)
408         if detail and not self['detail']:
409             ports = [dict(
410                 id=p['id'], name=p['name'], links=p['links']) for p in ports]
411         kwargs = dict()
412         if self['more']:
413             kwargs['out'] = StringIO()
414             kwargs['title'] = ()
415         self._print(ports, **kwargs)
416         if self['more']:
417             pager(kwargs['out'].getvalue())
418
419     def main(self):
420         super(self.__class__, self)._run()
421         self._run()
422
423
424 @command(port_cmds)
425 class port_info(_init_network, _optional_json):
426     """Get details about a port"""
427
428     @errors.generic.all
429     @errors.cyclades.connection
430     def _run(self, port_id):
431         port = self.client.get_port_details(port_id)
432         self._print(port, self.print_dict)
433
434     def main(self, port_id):
435         super(self.__class__, self)._run()
436         self._run(port_id=port_id)
437
438
439 @command(port_cmds)
440 class port_delete(_init_network, _optional_output_cmd, _port_wait):
441     """Delete a port (== disconnect server from network)"""
442
443     arguments = dict(
444         wait=FlagArgument('Wait port to be established', ('-w', '--wait'))
445     )
446
447     @errors.generic.all
448     @errors.cyclades.connection
449     def _run(self, port_id):
450         if self['wait']:
451             status = self.client.get_port_details(port_id)['status']
452         r = self.client.delete_port(port_id)
453         if self['wait']:
454             try:
455                 self._wait(port_id, status)
456             except ClientError as ce:
457                 if ce.status not in (404, ):
458                     raise
459                 self.error('Port %s is deleted' % port_id)
460         self._optional_output(r)
461
462     def main(self, port_id):
463         super(self.__class__, self)._run()
464         self._run(port_id=port_id)
465
466
467 @command(port_cmds)
468 class port_modify(_init_network, _optional_json):
469     """Modify the attributes of a port"""
470
471     arguments = dict(new_name=ValueArgument('New name of the port', '--name'))
472     required = ['new_name', ]
473
474     @errors.generic.all
475     @errors.cyclades.connection
476     def _run(self, port_id):
477         r = self.client.get_port_details(port_id)
478         r = self.client.update_port(
479             port_id, r['network_id'], name=self['new_name'])
480         self._print(r, self.print_dict)
481
482     def main(self, port_id):
483         super(self.__class__, self)._run()
484         self._run(port_id=port_id)
485
486
487 class PortStatusArgument(ValueArgument):
488
489     valid = ('BUILD', 'ACTIVE', 'DOWN', 'ERROR')
490
491     @property
492     def value(self):
493         return getattr(self, '_value', None)
494
495     @value.setter
496     def value(self, new_status):
497         if new_status:
498             new_status = new_status.upper()
499             if new_status in self.valid:
500                 raise CLIInvalidArgument(
501                     'Invalid argument %s' % new_status, details=[
502                     'Status valid values: %s'] % ', '.join(self.valid))
503             self._value = new_status
504
505
506 class _port_create(_init_network, _optional_json, _port_wait):
507
508     def connect(self, network_id, device_id):
509         fixed_ips = [dict(ip_address=self['ip_address'])] if (
510             self['ip_address']) else None
511         if fixed_ips and self['subnet_id']:
512             fixed_ips[0]['subnet_id'] = self['subnet_id']
513         r = self.client.create_port(
514             network_id, device_id,
515             name=self['name'],
516             security_groups=self['security_group_id'],
517             fixed_ips=fixed_ips)
518         if self['wait']:
519             self._wait(r['id'], r['status'])
520             r = self.client.get_port_details(r['id'])
521         self._print([r])
522
523
524 @command(port_cmds)
525 class port_create(_port_create):
526     """Create a new port (== connect server to network)"""
527
528     arguments = dict(
529         name=ValueArgument('A human readable name', '--name'),
530         security_group_id=RepeatableArgument(
531             'Add a security group id (can be repeated)',
532             ('-g', '--security-group')),
533         subnet_id=ValueArgument(
534             'Subnet id for fixed ips (used with --ip-address)',
535             '--subnet-id'),
536         ip_address=ValueArgument(
537             'IP address for subnet id', '--ip-address'),
538         network_id=ValueArgument('Set the network ID', '--network-id'),
539         device_id=ValueArgument(
540             'The device is either a virtual server or a virtual router',
541             '--device-id'),
542         wait=FlagArgument('Wait port to be established', ('-w', '--wait')),
543     )
544     required = ('network_id', 'device_id')
545
546     @errors.generic.all
547     @errors.cyclades.connection
548     @errors.cyclades.network_id
549     @errors.cyclades.server_id
550     def _run(self, network_id, server_id):
551         self.connect(network_id, server_id)
552
553     def main(self):
554         super(self.__class__, self)._run()
555         self._run(network_id=self['network_id'], server_id=self['device_id'])
556
557
558 @command(port_cmds)
559 class port_wait(_init_network, _port_wait):
560     """Wait for port to finish [ACTIVE, DOWN, BUILD, ERROR]"""
561
562     arguments = dict(
563         current_status=PortStatusArgument(
564             'Wait while in this status', '--status'),
565         timeout=IntArgument(
566             'Wait limit in seconds (default: 60)', '--timeout', default=60)
567     )
568
569     @errors.generic.all
570     @errors.cyclades.connection
571     def _run(self, port_id, current_status):
572         port = self.client.get_port_details(port_id)
573         if port['status'].lower() == current_status.lower():
574             self._wait(port_id, current_status, timeout=self['timeout'])
575         else:
576             self.error(
577                 'Port %s: Cannot wait for status %s, '
578                 'status is already %s' % (
579                     port_id, current_status, port['status']))
580
581     def main(self, port_id):
582         super(self.__class__, self)._run()
583         current_status = self['current_status'] or self.arguments[
584             'current_status'].valid[0]
585         self._run(port_id=port_id, current_status=current_status)
586
587
588 @command(ip_cmds)
589 class ip_list(_init_network, _optional_json):
590     """List reserved floating IPs"""
591
592     @errors.generic.all
593     @errors.cyclades.connection
594     def _run(self):
595         self._print(self.client.list_floatingips())
596
597     def main(self):
598         super(self.__class__, self)._run()
599         self._run()
600
601
602 @command(ip_cmds)
603 class ip_info(_init_network, _optional_json):
604     """Get details on a floating IP"""
605
606     @errors.generic.all
607     @errors.cyclades.connection
608     def _run(self, ip_id):
609         self._print(
610             self.client.get_floatingip_details(ip_id), self.print_dict)
611
612     def main(self, ip_id):
613         super(self.__class__, self)._run()
614         self._run(ip_id=ip_id)
615
616
617 @command(ip_cmds)
618 class ip_create(_init_network, _optional_json):
619     """Reserve an IP on a network"""
620
621     arguments = dict(
622         network_id=ValueArgument(
623             'The network to preserve the IP on', '--network-id'),
624         ip_address=ValueArgument('Allocate an IP address', '--address')
625     )
626     required = ('network_id', )
627
628     @errors.generic.all
629     @errors.cyclades.connection
630     @errors.cyclades.network_id
631     def _run(self, network_id):
632         self._print(
633             self.client.create_floatingip(
634                 network_id, floating_ip_address=self['ip_address']),
635             self.print_dict)
636
637     def main(self):
638         super(self.__class__, self)._run()
639         self._run(network_id=self['network_id'])
640
641
642 @command(ip_cmds)
643 class ip_delete(_init_network, _optional_output_cmd):
644     """Unreserve an IP (also delete the port, if attached)"""
645
646     def _run(self, ip_id):
647         self._optional_output(self.client.delete_floatingip(ip_id))
648
649     def main(self, ip_id):
650         super(self.__class__, self)._run()
651         self._run(ip_id=ip_id)
652
653
654 @command(ip_cmds)
655 class ip_attach(_port_create):
656     """Attach an IP on a virtual server"""
657
658     arguments = dict(
659         name=ValueArgument('A human readable name for the port', '--name'),
660         security_group_id=RepeatableArgument(
661             'Add a security group id (can be repeated)',
662             ('-g', '--security-group')),
663         subnet_id=ValueArgument('Subnet id', '--subnet-id'),
664         wait=FlagArgument('Wait IP to be attached', ('-w', '--wait')),
665         server_id=ValueArgument(
666             'Server to attach to this IP', '--server-id')
667     )
668     required = ('server_id', )
669
670     @errors.generic.all
671     @errors.cyclades.connection
672     @errors.cyclades.server_id
673     def _run(self, ip_or_ip_id, server_id):
674         netid = None
675         for ip in self.client.list_floatingips():
676             if ip_or_ip_id in (ip['floating_ip_address'], ip['id']):
677                 netid = ip['floating_network_id']
678                 iparg = ValueArgument(parsed_name='--ip')
679                 iparg.value = ip['floating_ip_address']
680                 self.arguments['ip_address'] = iparg
681                 break
682         if netid:
683             self.error('Creating a port to attach IP %s to server %s' % (
684                 ip_or_ip_id, server_id))
685             self.connect(netid, server_id)
686         else:
687             raiseCLIError(
688                 '%s does not match any reserved IPs or IP ids' % ip_or_ip_id,
689                 details=[
690                     'To reserve an IP:', '  [kamaki] ip create',
691                     'To see all reserved IPs:', '  [kamaki] ip list'])
692
693     def main(self, ip_or_ip_id):
694         super(self.__class__, self)._run()
695         self._run(ip_or_ip_id=ip_or_ip_id, server_id=self['server_id'])
696
697
698 @command(ip_cmds)
699 class ip_detach(_init_network, _port_wait, _optional_json):
700     """Detach an IP from a virtual server"""
701
702     arguments = dict(
703         wait=FlagArgument('Wait network to disconnect', ('-w', '--wait')),
704     )
705
706     @errors.generic.all
707     @errors.cyclades.connection
708     def _run(self, ip_or_ip_id):
709         for ip in self.client.list_floatingips():
710             if ip_or_ip_id in (ip['floating_ip_address'], ip['id']):
711                 if not ip['port_id']:
712                     raiseCLIError('IP %s is not attached' % ip_or_ip_id)
713                 self.error('Deleting port %s:' % ip['port_id'])
714                 self.client.delete_port(ip['port_id'])
715                 if self['wait']:
716                     port_status = self.client.get_port_details(ip['port_id'])[
717                         'status']
718                     try:
719                         self._wait(ip['port_id'], port_status)
720                     except ClientError as ce:
721                         if ce.status not in (404, ):
722                             raise
723                         self.error('Port %s is deleted' % ip['port_id'])
724                 return
725         raiseCLIError('IP or IP id %s not found' % ip_or_ip_id)
726
727     def main(self, ip_or_ip_id):
728         super(self.__class__, self)._run()
729         self._run(ip_or_ip_id)
730
731
732 #  Warn users for some importand changes
733
734 @command(network_cmds)
735 class network_connect(_port_create):
736     """Connect a network with a device (server or router)"""
737
738     arguments = dict(
739         name=ValueArgument('A human readable name for the port', '--name'),
740         security_group_id=RepeatableArgument(
741             'Add a security group id (can be repeated)',
742             ('-g', '--security-group')),
743         subnet_id=ValueArgument(
744             'Subnet id for fixed ips (used with --ip-address)',
745             '--subnet-id'),
746         ip_address=ValueArgument(
747             'IP address for subnet id (used with --subnet-id', '--ip-address'),
748         wait=FlagArgument('Wait network to connect', ('-w', '--wait')),
749         device_id=RepeatableArgument(
750             'Connect this device to the network (can be repeated)',
751             '--device-id')
752     )
753     required = ('device_id', )
754
755     @errors.generic.all
756     @errors.cyclades.connection
757     @errors.cyclades.network_id
758     @errors.cyclades.server_id
759     def _run(self, network_id, server_id):
760         self.error('Creating a port to connect network %s with device %s' % (
761             network_id, server_id))
762         self.connect(network_id, server_id)
763
764     def main(self, network_id):
765         super(self.__class__, self)._run()
766         for sid in self['device_id']:
767             self._run(network_id=network_id, server_id=sid)
768
769
770 @command(network_cmds)
771 class network_disconnect(_init_network, _port_wait, _optional_json):
772     """Disconnect a network from a device"""
773
774     def _cyclades_client(self):
775         auth = getattr(self, 'auth_base')
776         endpoints = auth.get_service_endpoints('compute')
777         URL = endpoints['publicURL']
778         from kamaki.clients.cyclades import CycladesClient
779         return CycladesClient(URL, self.client.token)
780
781     arguments = dict(
782         wait=FlagArgument('Wait network to disconnect', ('-w', '--wait')),
783         device_id=RepeatableArgument(
784             'Disconnect device from the network (can be repeated)',
785             '--device-id')
786     )
787     required = ('device_id', )
788
789     @errors.generic.all
790     @errors.cyclades.connection
791     @errors.cyclades.network_id
792     @errors.cyclades.server_id
793     def _run(self, network_id, server_id):
794         vm = self._cyclades_client().get_server_details(server_id)
795         ports = [port for port in vm['attachments'] if (
796             port['network_id'] in (network_id, ))]
797         if not ports:
798             raiseCLIError('Network %s is not connected to device %s' % (
799                 network_id, server_id))
800         for port in ports:
801             if self['wait']:
802                 port['status'] = self.client.get_port_details(port['id'])[
803                     'status']
804             self.client.delete_port(port['id'])
805             self.error('Deleting port %s:' % port['id'])
806             self.print_dict(port)
807             if self['wait']:
808                 try:
809                     self._wait(port['id'], port['status'])
810                 except ClientError as ce:
811                     if ce.status not in (404, ):
812                         raise
813                     self.error('Port %s is deleted' % port['id'])
814
815     def main(self, network_id):
816         super(self.__class__, self)._run()
817         for sid in self['device_id']:
818             self._run(network_id=network_id, server_id=sid)