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,
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
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]
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>'
63 port_states = ('BUILD', 'ACTIVE', 'DOWN', 'ERROR')
66 class _port_wait(_service_wait):
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,
74 class _init_network(_command_init):
77 def _run(self, service='network'):
78 if getattr(self, 'cloud', None):
79 base_url = self._custom_url(service) or self._custom_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)
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)
97 raise CLIBaseUrlError(service='network')
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
107 @command(network_cmds)
108 class network_list(_init_network, _optional_json, _name_filter, _id_filter):
110 Use filtering arguments (e.g., --name-like) to manage long server lists
114 detail=FlagArgument('show detailed output', ('-l', '--details')),
116 'output results in pages (-n to set items per page, default 10)',
118 user_id=ValueArgument(
119 'show only networks belonging to user with this id', '--user-id')
123 @errors.cyclades.connection
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']:
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'))
139 kwargs['out'] = StringIO()
141 self._print(nets, **kwargs)
143 pager(kwargs['out'].getvalue())
146 super(self.__class__, self)._run()
150 @command(network_cmds)
151 class network_info(_init_network, _optional_json):
152 """Get details about a network"""
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)
161 def main(self, network_id):
162 super(self.__class__, self)._run()
163 self._run(network_id=network_id)
166 class NetworkTypeArgument(ValueArgument):
168 types = ('MAC_FILTERED', 'CUSTOM', 'IP_LESS_ROUTED', 'PHYSICAL_VLAN')
172 return getattr(self, '_value', self.types[0])
175 def value(self, new_value):
176 if new_value and new_value.upper() in self.types:
177 self._value = new_value.upper()
179 raise CLIInvalidArgument(
180 'Invalid network type %s' % new_value, details=[
181 'Valid types: %s' % ', '.join(self.types), ])
184 @command(network_cmds)
185 class network_create(_init_network, _optional_json):
186 """Create a new network (default type: MAC_FILTERED)"""
189 name=ValueArgument('Network name', '--name'),
191 'Make network shared (special privileges required)', '--shared'),
192 network_type=NetworkTypeArgument(
193 'Valid network types: %s' % (', '.join(NetworkTypeArgument.types)),
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)
206 super(self.__class__, self)._run()
207 self._run(network_type=self['network_type'])
210 @command(network_cmds)
211 class network_delete(_init_network, _optional_output_cmd):
212 """Delete a network"""
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)
221 def main(self, network_id):
222 super(self.__class__, self)._run()
223 self._run(network_id=network_id)
226 @command(network_cmds)
227 class network_modify(_init_network, _optional_json):
228 """Modify network attributes"""
230 arguments = dict(new_name=ValueArgument('Rename the network', '--name'))
231 required = ['new_name', ]
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)
240 def main(self, network_id):
241 super(self.__class__, self)._run()
242 self._run(network_id=network_id)
245 @command(subnet_cmds)
246 class subnet_list(_init_network, _optional_json, _name_filter, _id_filter):
248 Use filtering arguments (e.g., --name-like) to manage long server lists
252 detail=FlagArgument('show detailed output', ('-l', '--details')),
254 'output results in pages (-n to set items per page, default 10)',
259 @errors.cyclades.connection
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']:
268 _2_net='( of network %s )' % n['network_id']) for n in nets]
269 kwargs = dict(title=('_0_id', '_1_name', '_2_net'))
273 kwargs['out'] = StringIO()
275 self._print(nets, **kwargs)
277 pager(kwargs['out'].getvalue())
280 super(self.__class__, self)._run()
284 @command(subnet_cmds)
285 class subnet_info(_init_network, _optional_json):
286 """Get details about a subnet"""
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)
294 def main(self, subnet_id):
295 super(self.__class__, self)._run()
296 self._run(subnet_id=subnet_id)
299 class AllocationPoolArgument(RepeatableArgument):
303 return super(AllocationPoolArgument, self).value or []
306 def value(self, new_pools):
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
321 @command(subnet_cmds)
322 class subnet_create(_init_network, _optional_json):
323 """Create a new subnet"""
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',
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')
338 required = ('network_id', 'cidr')
341 @errors.cyclades.connection
342 @errors.cyclades.network_id
343 def _run(self, network_id, cidr):
344 net = self.client.create_subnet(
346 self['name'], self['allocation_pools'], self['gateway'],
347 self['subnet_id'], self['ipv6'], self['enable_dhcp'])
348 self._print(net, self.print_dict)
351 super(self.__class__, self)._run()
352 self._run(network_id=self['network_id'], cidr=self['cidr'])
355 # @command(subnet_cmds)
356 # class subnet_delete(_init_network, _optional_output_cmd):
357 # """Delete a subnet"""
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)
365 # def main(self, subnet_id):
366 # super(self.__class__, self)._run()
367 # self._run(subnet_id=subnet_id)
370 @command(subnet_cmds)
371 class subnet_modify(_init_network, _optional_json):
372 """Modify the attributes of a subnet"""
375 new_name=ValueArgument('New name of the subnet', '--name')
377 required = ['new_name']
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)
385 def main(self, subnet_id):
386 super(self.__class__, self)._run()
387 self._run(subnet_id=subnet_id)
391 class port_list(_init_network, _optional_json, _name_filter, _id_filter):
395 detail=FlagArgument('show detailed output', ('-l', '--details')),
397 'output results in pages (-n to set items per page, default 10)',
399 user_id=ValueArgument(
400 'show only networks belonging to user with this id', '--user-id')
404 @errors.cyclades.connection
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']:
413 id=p['id'], name=p['name'], links=p['links']) for p in ports]
416 kwargs['out'] = StringIO()
418 self._print(ports, **kwargs)
420 pager(kwargs['out'].getvalue())
423 super(self.__class__, self)._run()
428 class port_info(_init_network, _optional_json):
429 """Get details about a port"""
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)
437 def main(self, port_id):
438 super(self.__class__, self)._run()
439 self._run(port_id=port_id)
443 class port_delete(_init_network, _optional_output_cmd, _port_wait):
444 """Delete a port (== disconnect server from network)"""
447 wait=FlagArgument('Wait port to be established', ('-w', '--wait'))
451 @errors.cyclades.connection
452 def _run(self, port_id):
454 status = self.client.get_port_details(port_id)['status']
455 r = self.client.delete_port(port_id)
458 self._wait(port_id, status)
459 except ClientError as ce:
460 if ce.status not in (404, ):
462 self.error('Port %s is deleted' % port_id)
463 self._optional_output(r)
465 def main(self, port_id):
466 super(self.__class__, self)._run()
467 self._run(port_id=port_id)
471 class port_modify(_init_network, _optional_json):
472 """Modify the attributes of a port"""
474 arguments = dict(new_name=ValueArgument('New name of the port', '--name'))
475 required = ['new_name', ]
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)
485 def main(self, port_id):
486 super(self.__class__, self)._run()
487 self._run(port_id=port_id)
490 class _port_create(_init_network, _optional_json, _port_wait):
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,
500 security_groups=self['security_group_id'],
503 self._wait(r['id'], r['status'])
504 r = self.client.get_port_details(r['id'])
509 class port_create(_port_create):
510 """Create a new port (== connect server to network)"""
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)',
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',
526 wait=FlagArgument('Wait port to be established', ('-w', '--wait')),
528 required = ('network_id', 'device_id')
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)
538 super(self.__class__, self)._run()
539 self._run(network_id=self['network_id'], server_id=self['device_id'])
543 class port_wait(_init_network, _port_wait):
544 """Wait for port to finish (default: BUILD)"""
547 port_status=StatusArgument(
548 'Wait while in this status (%s, default: %s)' % (
549 ', '.join(port_states), port_states[0]),
551 valid_states=port_states),
553 'Wait limit in seconds (default: 60)', '--timeout', default=60)
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'])
564 'Port %s: Cannot wait for status %s, '
565 'status is already %s' % (
566 port_id, port_status, port['status']))
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)
575 class ip_list(_init_network, _optional_json):
576 """List reserved floating IPs"""
579 @errors.cyclades.connection
581 self._print(self.client.list_floatingips())
584 super(self.__class__, self)._run()
589 class ip_info(_init_network, _optional_json):
590 """Get details on a floating IP"""
593 @errors.cyclades.connection
594 def _run(self, ip_id):
596 self.client.get_floatingip_details(ip_id), self.print_dict)
598 def main(self, ip_id):
599 super(self.__class__, self)._run()
600 self._run(ip_id=ip_id)
604 class ip_create(_init_network, _optional_json):
605 """Reserve an IP on a network"""
608 network_id=ValueArgument(
609 'The network to preserve the IP on', '--network-id'),
610 ip_address=ValueArgument('Allocate an IP address', '--address')
614 @errors.cyclades.connection
617 self.client.create_floatingip(
618 self['network_id'], floating_ip_address=self['ip_address']),
622 super(self.__class__, self)._run()
627 class ip_delete(_init_network, _optional_output_cmd):
628 """Unreserve an IP (also delete the port, if attached)"""
630 def _run(self, ip_id):
631 self._optional_output(self.client.delete_floatingip(ip_id))
633 def main(self, ip_id):
634 super(self.__class__, self)._run()
635 self._run(ip_id=ip_id)
639 class ip_attach(_port_create):
640 """Attach an IP on a virtual server"""
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')
652 required = ('server_id', )
655 @errors.cyclades.connection
656 @errors.cyclades.server_id
657 def _run(self, ip_or_ip_id, server_id):
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
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)
672 '%s does not match any reserved IPs or IP ids' % ip_or_ip_id,
674 'To reserve an IP:', ' [kamaki] ip create',
675 'To see all reserved IPs:', ' [kamaki] ip list'])
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'])
683 class ip_detach(_init_network, _port_wait, _optional_json):
684 """Detach an IP from a virtual server"""
687 wait=FlagArgument('Wait network to disconnect', ('-w', '--wait')),
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'])
700 port_status = self.client.get_port_details(ip['port_id'])[
703 self._wait(ip['port_id'], port_status)
704 except ClientError as ce:
705 if ce.status not in (404, ):
707 self.error('Port %s is deleted' % ip['port_id'])
709 raiseCLIError('IP or IP id %s not found' % ip_or_ip_id)
711 def main(self, ip_or_ip_id):
712 super(self.__class__, self)._run()
713 self._run(ip_or_ip_id)
716 # Warn users for some importand changes
718 @command(network_cmds)
719 class network_connect(_port_create):
720 """Connect a network with a device (server or router)"""
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)',
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)',
737 required = ('device_id', )
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)
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)
754 @command(network_cmds)
755 class network_disconnect(_init_network, _port_wait, _optional_json):
756 """Disconnect a network from a device"""
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)
766 wait=FlagArgument('Wait network to disconnect', ('-w', '--wait')),
767 device_id=RepeatableArgument(
768 'Disconnect device from the network (can be repeated)',
771 required = ('device_id', )
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, ))]
782 raiseCLIError('Network %s is not connected to device %s' % (
783 network_id, server_id))
786 port['status'] = self.client.get_port_details(port['id'])[
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))
793 self._wait(port['id'], port['status'])
794 except ClientError as ce:
795 if ce.status not in (404, ):
797 self.error('Port %s is deleted' % port['id'])
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)