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