1 # Copyright 2011-2013 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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.
34 from io import StringIO
35 from pydoc import pager
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
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]
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>'
63 class _port_wait(_service_wait):
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,
71 class _init_network(_command_init):
74 def _run(self, service='network'):
75 if getattr(self, 'cloud', None):
76 base_url = self._custom_url(service) or self._custom_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)
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)
94 raise CLIBaseUrlError(service='network')
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
104 @command(network_cmds)
105 class network_list(_init_network, _optional_json, _name_filter, _id_filter):
107 Use filtering arguments (e.g., --name-like) to manage long server lists
111 detail=FlagArgument('show detailed output', ('-l', '--details')),
113 'output results in pages (-n to set items per page, default 10)',
115 user_id=ValueArgument(
116 'show only networks belonging to user with this id', '--user-id')
120 @errors.cyclades.connection
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']:
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'))
136 kwargs['out'] = StringIO()
138 self._print(nets, **kwargs)
140 pager(kwargs['out'].getvalue())
143 super(self.__class__, self)._run()
147 @command(network_cmds)
148 class network_info(_init_network, _optional_json):
149 """Get details about a network"""
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)
158 def main(self, network_id):
159 super(self.__class__, self)._run()
160 self._run(network_id=network_id)
163 class NetworkTypeArgument(ValueArgument):
165 types = ('MAC_FILTERED', 'CUSTOM', 'IP_LESS_ROUTED', 'PHYSICAL_VLAN')
169 return getattr(self, '_value', self.types[0])
172 def value(self, new_value):
173 if new_value and new_value.upper() in self.types:
174 self._value = new_value.upper()
176 raise CLIInvalidArgument(
177 'Invalid network type %s' % new_value, details=[
178 'Valid types: %s' % ', '.join(self.types), ])
181 @command(network_cmds)
182 class network_create(_init_network, _optional_json):
183 """Create a new network (default type: MAC_FILTERED)"""
186 name=ValueArgument('Network name', '--name'),
188 'Make network shared (special privileges required)', '--shared'),
189 network_type=NetworkTypeArgument(
190 'Valid network types: %s' % (', '.join(NetworkTypeArgument.types)),
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)
203 super(self.__class__, self)._run()
204 self._run(network_type=self['network_type'])
207 @command(network_cmds)
208 class network_delete(_init_network, _optional_output_cmd):
209 """Delete a network"""
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)
218 def main(self, network_id):
219 super(self.__class__, self)._run()
220 self._run(network_id=network_id)
223 @command(network_cmds)
224 class network_modify(_init_network, _optional_json):
225 """Modify network attributes"""
227 arguments = dict(new_name=ValueArgument('Rename the network', '--name'))
228 required = ['new_name', ]
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)
237 def main(self, network_id):
238 super(self.__class__, self)._run()
239 self._run(network_id=network_id)
242 @command(subnet_cmds)
243 class subnet_list(_init_network, _optional_json, _name_filter, _id_filter):
245 Use filtering arguments (e.g., --name-like) to manage long server lists
249 detail=FlagArgument('show detailed output', ('-l', '--details')),
251 'output results in pages (-n to set items per page, default 10)',
256 @errors.cyclades.connection
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']:
265 _2_net='( of network %s )' % n['network_id']) for n in nets]
266 kwargs = dict(title=('_0_id', '_1_name', '_2_net'))
270 kwargs['out'] = StringIO()
272 self._print(nets, **kwargs)
274 pager(kwargs['out'].getvalue())
277 super(self.__class__, self)._run()
281 @command(subnet_cmds)
282 class subnet_info(_init_network, _optional_json):
283 """Get details about a subnet"""
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)
291 def main(self, subnet_id):
292 super(self.__class__, self)._run()
293 self._run(subnet_id=subnet_id)
296 class AllocationPoolArgument(RepeatableArgument):
300 return super(AllocationPoolArgument, self).value or []
303 def value(self, new_pools):
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
318 @command(subnet_cmds)
319 class subnet_create(_init_network, _optional_json):
320 """Create a new subnet"""
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',
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')
335 required = ('network_id', 'cidr')
338 @errors.cyclades.connection
339 @errors.cyclades.network_id
340 def _run(self, network_id, cidr):
341 net = self.client.create_subnet(
343 self['name'], self['allocation_pools'], self['gateway'],
344 self['subnet_id'], self['ipv6'], self['enable_dhcp'])
345 self._print(net, self.print_dict)
348 super(self.__class__, self)._run()
349 self._run(network_id=self['network_id'], cidr=self['cidr'])
352 # @command(subnet_cmds)
353 # class subnet_delete(_init_network, _optional_output_cmd):
354 # """Delete a subnet"""
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)
362 # def main(self, subnet_id):
363 # super(self.__class__, self)._run()
364 # self._run(subnet_id=subnet_id)
367 @command(subnet_cmds)
368 class subnet_modify(_init_network, _optional_json):
369 """Modify the attributes of a subnet"""
372 new_name=ValueArgument('New name of the subnet', '--name')
374 required = ['new_name']
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)
382 def main(self, subnet_id):
383 super(self.__class__, self)._run()
384 self._run(subnet_id=subnet_id)
388 class port_list(_init_network, _optional_json, _name_filter, _id_filter):
392 detail=FlagArgument('show detailed output', ('-l', '--details')),
394 'output results in pages (-n to set items per page, default 10)',
396 user_id=ValueArgument(
397 'show only networks belonging to user with this id', '--user-id')
401 @errors.cyclades.connection
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']:
410 id=p['id'], name=p['name'], links=p['links']) for p in ports]
413 kwargs['out'] = StringIO()
415 self._print(ports, **kwargs)
417 pager(kwargs['out'].getvalue())
420 super(self.__class__, self)._run()
425 class port_info(_init_network, _optional_json):
426 """Get details about a port"""
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)
434 def main(self, port_id):
435 super(self.__class__, self)._run()
436 self._run(port_id=port_id)
440 class port_delete(_init_network, _optional_output_cmd, _port_wait):
441 """Delete a port (== disconnect server from network)"""
444 wait=FlagArgument('Wait port to be established', ('-w', '--wait'))
448 @errors.cyclades.connection
449 def _run(self, port_id):
451 status = self.client.get_port_details(port_id)['status']
452 r = self.client.delete_port(port_id)
455 self._wait(port_id, status)
456 except ClientError as ce:
457 if ce.status not in (404, ):
459 self.error('Port %s is deleted' % port_id)
460 self._optional_output(r)
462 def main(self, port_id):
463 super(self.__class__, self)._run()
464 self._run(port_id=port_id)
468 class port_modify(_init_network, _optional_json):
469 """Modify the attributes of a port"""
471 arguments = dict(new_name=ValueArgument('New name of the port', '--name'))
472 required = ['new_name', ]
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)
482 def main(self, port_id):
483 super(self.__class__, self)._run()
484 self._run(port_id=port_id)
487 class PortStatusArgument(ValueArgument):
489 valid = ('BUILD', 'ACTIVE', 'DOWN', 'ERROR')
493 return getattr(self, '_value', None)
496 def value(self, 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
506 class _port_create(_init_network, _optional_json, _port_wait):
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,
516 security_groups=self['security_group_id'],
519 self._wait(r['id'], r['status'])
520 r = self.client.get_port_details(r['id'])
525 class port_create(_port_create):
526 """Create a new port (== connect server to network)"""
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)',
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',
542 wait=FlagArgument('Wait port to be established', ('-w', '--wait')),
544 required = ('network_id', 'device_id')
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)
554 super(self.__class__, self)._run()
555 self._run(network_id=self['network_id'], server_id=self['device_id'])
559 class port_wait(_init_network, _port_wait):
560 """Wait for port to finish [ACTIVE, DOWN, BUILD, ERROR]"""
563 current_status=PortStatusArgument(
564 'Wait while in this status', '--status'),
566 'Wait limit in seconds (default: 60)', '--timeout', default=60)
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'])
577 'Port %s: Cannot wait for status %s, '
578 'status is already %s' % (
579 port_id, current_status, port['status']))
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)
589 class ip_list(_init_network, _optional_json):
590 """List reserved floating IPs"""
593 @errors.cyclades.connection
595 self._print(self.client.list_floatingips())
598 super(self.__class__, self)._run()
603 class ip_info(_init_network, _optional_json):
604 """Get details on a floating IP"""
607 @errors.cyclades.connection
608 def _run(self, ip_id):
610 self.client.get_floatingip_details(ip_id), self.print_dict)
612 def main(self, ip_id):
613 super(self.__class__, self)._run()
614 self._run(ip_id=ip_id)
618 class ip_create(_init_network, _optional_json):
619 """Reserve an IP on a network"""
622 network_id=ValueArgument(
623 'The network to preserve the IP on', '--network-id'),
624 ip_address=ValueArgument('Allocate an IP address', '--address')
626 required = ('network_id', )
629 @errors.cyclades.connection
630 @errors.cyclades.network_id
631 def _run(self, network_id):
633 self.client.create_floatingip(
634 network_id, floating_ip_address=self['ip_address']),
638 super(self.__class__, self)._run()
639 self._run(network_id=self['network_id'])
643 class ip_delete(_init_network, _optional_output_cmd):
644 """Unreserve an IP (also delete the port, if attached)"""
646 def _run(self, ip_id):
647 self._optional_output(self.client.delete_floatingip(ip_id))
649 def main(self, ip_id):
650 super(self.__class__, self)._run()
651 self._run(ip_id=ip_id)
655 class ip_attach(_port_create):
656 """Attach an IP on a virtual server"""
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')
668 required = ('server_id', )
671 @errors.cyclades.connection
672 @errors.cyclades.server_id
673 def _run(self, ip_or_ip_id, server_id):
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
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)
688 '%s does not match any reserved IPs or IP ids' % ip_or_ip_id,
690 'To reserve an IP:', ' [kamaki] ip create',
691 'To see all reserved IPs:', ' [kamaki] ip list'])
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'])
699 class ip_detach(_init_network, _port_wait, _optional_json):
700 """Detach an IP from a virtual server"""
703 wait=FlagArgument('Wait network to disconnect', ('-w', '--wait')),
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'])
716 port_status = self.client.get_port_details(ip['port_id'])[
719 self._wait(ip['port_id'], port_status)
720 except ClientError as ce:
721 if ce.status not in (404, ):
723 self.error('Port %s is deleted' % ip['port_id'])
725 raiseCLIError('IP or IP id %s not found' % ip_or_ip_id)
727 def main(self, ip_or_ip_id):
728 super(self.__class__, self)._run()
729 self._run(ip_or_ip_id)
732 # Warn users for some importand changes
734 @command(network_cmds)
735 class network_connect(_port_create):
736 """Connect a network with a device (server or router)"""
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)',
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)',
753 required = ('device_id', )
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)
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)
770 @command(network_cmds)
771 class network_disconnect(_init_network, _port_wait, _optional_json):
772 """Disconnect a network from a device"""
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)
782 wait=FlagArgument('Wait network to disconnect', ('-w', '--wait')),
783 device_id=RepeatableArgument(
784 'Disconnect device from the network (can be repeated)',
787 required = ('device_id', )
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, ))]
798 raiseCLIError('Network %s is not connected to device %s' % (
799 network_id, server_id))
802 port['status'] = self.client.get_port_details(port['id'])[
804 self.client.delete_port(port['id'])
805 self.error('Deleting port %s:' % port['id'])
806 self.print_dict(port)
809 self._wait(port['id'], port['status'])
810 except ClientError as ce:
811 if ce.status not in (404, ):
813 self.error('Port %s is deleted' % port['id'])
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)