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: /user authenticate\
59 \n* to set authentication token: /config set cloud.<cloud>.token <token>'
62 class _port_wait(_service_wait):
64 def _wait(self, port_id, current_status, timeout=60):
65 super(_port_wait, self)._wait(
66 'Port', port_id, self.client.wait_port, current_status,
70 class _init_network(_command_init):
73 def _run(self, service='network'):
74 if getattr(self, 'cloud', None):
75 base_url = self._custom_url(service) or self._custom_url(
78 token = self._custom_token(service) or self._custom_token(
79 'network') or self.config.get_cloud('token')
80 self.client = CycladesNetworkClient(
81 base_url=base_url, token=token)
84 self.cloud = 'default'
85 if getattr(self, 'auth_base', False):
86 network_endpoints = self.auth_base.get_service_endpoints(
87 self._custom_type('network') or 'network',
88 self._custom_version('network') or '')
89 base_url = network_endpoints['publicURL']
90 token = self.auth_base.token
91 self.client = CycladesNetworkClient(base_url=base_url, token=token)
93 raise CLIBaseUrlError(service='network')
95 def _filter_by_user_id(self, nets):
96 return [net for net in nets if net['user_id'] == self['user_id']] if (
97 self['user_id']) else nets
103 @command(network_cmds)
104 class network_list(_init_network, _optional_json, _name_filter, _id_filter):
106 Use filtering arguments (e.g., --name-like) to manage long server lists
110 detail=FlagArgument('show detailed output', ('-l', '--details')),
112 'output results in pages (-n to set items per page, default 10)',
114 user_id=ValueArgument(
115 'show only networks belonging to user with this id', '--user-id')
119 @errors.cyclades.connection
121 detail = bool(self['detail'] or self['user_id'])
122 nets = self.client.list_networks(detail=detail)
123 nets = self._filter_by_user_id(nets)
124 nets = self._filter_by_name(nets)
125 nets = self._filter_by_id(nets)
126 if detail and not self['detail']:
128 id=n['id'], name=n['name'], links=n['links']) for n in nets]
131 kwargs['out'] = StringIO()
133 self._print(nets, **kwargs)
135 pager(kwargs['out'].getvalue())
138 super(self.__class__, self)._run()
142 @command(network_cmds)
143 class network_info(_init_network, _optional_json):
144 """Get details about a network"""
147 @errors.cyclades.connection
148 @errors.cyclades.network_id
149 def _run(self, network_id):
150 net = self.client.get_network_details(network_id)
151 self._print(net, self.print_dict)
153 def main(self, network_id):
154 super(self.__class__, self)._run()
155 self._run(network_id=network_id)
158 class NetworkTypeArgument(ValueArgument):
160 types = ('MAC_FILTERED', 'CUSTOM', 'IP_LESS_ROUTED', 'PHYSICAL_VLAN')
164 return getattr(self, '_value', self.types[0])
167 def value(self, new_value):
168 if new_value and new_value.upper() in self.types:
169 self._value = new_value.upper()
171 raise CLIInvalidArgument(
172 'Invalid network type %s' % new_value, details=[
173 'Valid types: %s' % ', '.join(self.types), ])
176 @command(network_cmds)
177 class network_create(_init_network, _optional_json):
178 """Create a new network (default type: MAC_FILTERED)"""
181 name=ValueArgument('Network name', '--name'),
183 'Make network shared (special privileges required)', '--shared'),
184 network_type=NetworkTypeArgument(
185 'Valid network types: %s' % (', '.join(NetworkTypeArgument.types)),
190 @errors.cyclades.connection
191 @errors.cyclades.network_type
192 def _run(self, network_type):
193 net = self.client.create_network(
194 network_type, name=self['name'], shared=self['shared'])
195 self._print(net, self.print_dict)
198 super(self.__class__, self)._run()
199 self._run(network_type=self['network_type'])
202 @command(network_cmds)
203 class network_delete(_init_network, _optional_output_cmd):
204 """Delete a network"""
207 @errors.cyclades.connection
208 @errors.cyclades.network_id
209 def _run(self, network_id):
210 r = self.client.delete_network(network_id)
211 self._optional_output(r)
213 def main(self, network_id):
214 super(self.__class__, self)._run()
215 self._run(network_id=network_id)
218 @command(network_cmds)
219 class network_modify(_init_network, _optional_json):
220 """Modify network attributes"""
222 arguments = dict(new_name=ValueArgument('Rename the network', '--name'))
223 required = ['new_name', ]
226 @errors.cyclades.connection
227 @errors.cyclades.network_id
228 def _run(self, network_id):
229 r = self.client.update_network(network_id, name=self['new_name'])
230 self._print(r, self.print_dict)
232 def main(self, network_id):
233 super(self.__class__, self)._run()
234 self._run(network_id=network_id)
237 @command(subnet_cmds)
238 class subnet_list(_init_network, _optional_json, _name_filter, _id_filter):
240 Use filtering arguments (e.g., --name-like) to manage long server lists
244 detail=FlagArgument('show detailed output', ('-l', '--details')),
246 'output results in pages (-n to set items per page, default 10)',
251 @errors.cyclades.connection
253 nets = self.client.list_subnets()
254 nets = self._filter_by_name(nets)
255 nets = self._filter_by_id(nets)
256 if not self['detail']:
258 id=n['id'], name=n['name'], links=n['links']) for n in nets]
261 kwargs['out'] = StringIO()
263 self._print(nets, **kwargs)
265 pager(kwargs['out'].getvalue())
268 super(self.__class__, self)._run()
272 @command(subnet_cmds)
273 class subnet_info(_init_network, _optional_json):
274 """Get details about a subnet"""
277 @errors.cyclades.connection
278 def _run(self, subnet_id):
279 net = self.client.get_subnet_details(subnet_id)
280 self._print(net, self.print_dict)
282 def main(self, subnet_id):
283 super(self.__class__, self)._run()
284 self._run(subnet_id=subnet_id)
287 class AllocationPoolArgument(RepeatableArgument):
291 return super(AllocationPoolArgument, self).value or []
294 def value(self, new_pools):
296 for pool in new_pools:
297 start, comma, end = pool.partition(',')
298 if not (start and comma and end):
299 raise CLIInvalidArgument(
300 'Invalid allocation pool argument %s' % pool, details=[
301 'Allocation values must be of the form:',
302 ' <start address>,<end address>'])
303 new_list.append(dict(start=start, end=end))
304 self._value = new_list
307 @command(subnet_cmds)
308 class subnet_create(_init_network, _optional_json):
309 """Create a new subnet"""
312 name=ValueArgument('Subnet name', '--name'),
313 allocation_pools=AllocationPoolArgument(
314 'start_address,end_address of allocation pool (can be repeated)'
315 ' e.g., --alloc-pool=123.45.67.1,123.45.67.8',
317 gateway=ValueArgument('Gateway IP', '--gateway'),
318 subnet_id=ValueArgument('The id for the subnet', '--id'),
319 ipv6=FlagArgument('If set, IP version is set to 6, else 4', '--ipv6'),
320 enable_dhcp=FlagArgument('Enable dhcp (default: off)', '--with-dhcp'),
321 network_id=ValueArgument('Set the network ID', '--network-id'),
322 cidr=ValueArgument('Set the CIDR', '--cidr')
324 required = ('network_id', 'cidr')
327 @errors.cyclades.connection
328 @errors.cyclades.network_id
329 def _run(self, network_id, cidr):
330 net = self.client.create_subnet(
332 self['name'], self['allocation_pools'], self['gateway'],
333 self['subnet_id'], self['ipv6'], self['enable_dhcp'])
334 self._print(net, self.print_dict)
337 super(self.__class__, self)._run()
338 self._run(network_id=self['network_id'], cidr=self['cidr'])
341 # @command(subnet_cmds)
342 # class subnet_delete(_init_network, _optional_output_cmd):
343 # """Delete a subnet"""
345 # @errors.generic.all
346 # @errors.cyclades.connection
347 # def _run(self, subnet_id):
348 # r = self.client.delete_subnet(subnet_id)
349 # self._optional_output(r)
351 # def main(self, subnet_id):
352 # super(self.__class__, self)._run()
353 # self._run(subnet_id=subnet_id)
356 @command(subnet_cmds)
357 class subnet_modify(_init_network, _optional_json):
358 """Modify the attributes of a subnet"""
361 new_name=ValueArgument('New name of the subnet', '--name')
363 required = ['new_name']
366 @errors.cyclades.connection
367 def _run(self, subnet_id):
368 r = self.client.get_subnet_details(subnet_id)
369 r = self.client.update_subnet(
370 subnet_id, r['network_id'], name=self['new_name'])
371 self._print(r, self.print_dict)
373 def main(self, subnet_id):
374 super(self.__class__, self)._run()
375 self._run(subnet_id=subnet_id)
379 class port_list(_init_network, _optional_json, _name_filter, _id_filter):
383 detail=FlagArgument('show detailed output', ('-l', '--details')),
385 'output results in pages (-n to set items per page, default 10)',
387 user_id=ValueArgument(
388 'show only networks belonging to user with this id', '--user-id')
392 @errors.cyclades.connection
394 detail = bool(self['detail'] or self['user_id'])
395 ports = self.client.list_ports(detail=detail)
396 ports = self._filter_by_user_id(ports)
397 ports = self._filter_by_name(ports)
398 ports = self._filter_by_id(ports)
399 if detail and not self['detail']:
401 id=p['id'], name=p['name'], links=p['links']) for p in ports]
404 kwargs['out'] = StringIO()
406 self._print(ports, **kwargs)
408 pager(kwargs['out'].getvalue())
411 super(self.__class__, self)._run()
416 class port_info(_init_network, _optional_json):
417 """Get details about a port"""
420 @errors.cyclades.connection
421 def _run(self, port_id):
422 port = self.client.get_port_details(port_id)
423 self._print(port, self.print_dict)
425 def main(self, port_id):
426 super(self.__class__, self)._run()
427 self._run(port_id=port_id)
431 class port_delete(_init_network, _optional_output_cmd, _port_wait):
432 """Delete a port (== disconnect server from network)"""
435 wait=FlagArgument('Wait port to be established', ('-w', '--wait'))
439 @errors.cyclades.connection
440 def _run(self, port_id):
442 status = self.client.get_port_details(port_id)['status']
443 r = self.client.delete_port(port_id)
446 self._wait(port_id, status)
447 except ClientError as ce:
448 if ce.status not in (404, ):
450 self.error('Port %s is deleted' % port_id)
451 self._optional_output(r)
453 def main(self, port_id):
454 super(self.__class__, self)._run()
455 self._run(port_id=port_id)
459 class port_modify(_init_network, _optional_json):
460 """Modify the attributes of a port"""
462 arguments = dict(new_name=ValueArgument('New name of the port', '--name'))
463 required = ['new_name', ]
466 @errors.cyclades.connection
467 def _run(self, port_id):
468 r = self.client.get_port_details(port_id)
469 r = self.client.update_port(
470 port_id, r['network_id'], name=self['new_name'])
471 self._print(r, self.print_dict)
473 def main(self, port_id):
474 super(self.__class__, self)._run()
475 self._run(port_id=port_id)
478 class _port_create(_init_network, _optional_json, _port_wait):
480 def connect(self, network_id, device_id):
482 subnet_id=self['subnet_id'], ip_address=self['ip_address'])] if (
483 self['subnet_id']) else None
484 r = self.client.create_port(
485 network_id, device_id,
487 security_groups=self['security_group_id'],
490 self._wait(r['id'], r['status'])
491 r = self.client.get_port_details(r['id'])
496 class port_create(_port_create):
497 """Create a new port (== connect server to network)"""
500 name=ValueArgument('A human readable name', '--name'),
501 security_group_id=RepeatableArgument(
502 'Add a security group id (can be repeated)',
503 ('-g', '--security-group')),
504 subnet_id=ValueArgument(
505 'Subnet id for fixed ips (used with --ip-address)',
507 ip_address=ValueArgument(
508 'IP address for subnet id (used with --subnet-id', '--ip-address'),
509 network_id=ValueArgument('Set the network ID', '--network-id'),
510 device_id=ValueArgument(
511 'The device is either a virtual server or a virtual router',
513 wait=FlagArgument('Wait port to be established', ('-w', '--wait')),
515 required = ('network_id', 'device_id')
518 @errors.cyclades.connection
519 @errors.cyclades.network_id
520 @errors.cyclades.server_id
521 def _run(self, network_id, server_id):
522 self.connect(network_id, server_id)
525 super(self.__class__, self)._run()
526 self._run(network_id=self['network_id'], server_id=self['device_id'])
530 class port_wait(_init_network, _port_wait):
531 """Wait for port to finish [ACTIVE, DOWN, BUILD, ERROR]"""
535 'Wait limit in seconds (default: 60)', '--timeout', default=60)
539 @errors.cyclades.connection
540 def _run(self, port_id, current_status):
541 port = self.client.get_port_details(port_id)
542 if port['status'].lower() == current_status.lower():
543 self._wait(port_id, current_status, timeout=self['timeout'])
546 'Port %s: Cannot wait for status %s, '
547 'status is already %s' % (
548 port_id, current_status, port['status']))
550 def main(self, port_id, current_status='BUILD'):
551 super(self.__class__, self)._run()
552 self._run(port_id=port_id, current_status=current_status)
556 class ip_list(_init_network, _optional_json):
557 """List reserved floating IPs"""
560 @errors.cyclades.connection
562 self._print(self.client.list_floatingips())
565 super(self.__class__, self)._run()
570 class ip_info(_init_network, _optional_json):
571 """Get details on a floating IP"""
574 @errors.cyclades.connection
575 def _run(self, ip_id):
577 self.client.get_floatingip_details(ip_id), self.print_dict)
579 def main(self, ip_id):
580 super(self.__class__, self)._run()
581 self._run(ip_id=ip_id)
585 class ip_create(_init_network, _optional_json):
586 """Reserve an IP on a network"""
589 network_id=ValueArgument(
590 'The network to preserve the IP on', '--network-id'),
591 ip_address=ValueArgument('Allocate a specific IP address', '--address')
593 required = ('network_id', )
596 @errors.cyclades.connection
597 @errors.cyclades.network_id
598 def _run(self, network_id):
600 self.client.create_floatingip(
601 network_id, floating_ip_address=self['ip_address']),
605 super(self.__class__, self)._run()
606 self._run(network_id=self['network_id'])
610 class ip_delete(_init_network, _optional_output_cmd):
611 """Unreserve an IP (also delete the port, if attached)"""
613 def _run(self, ip_id):
614 self._optional_output(self.client.delete_floatingip(ip_id))
616 def main(self, ip_id):
617 super(self.__class__, self)._run()
618 self._run(ip_id=ip_id)
621 # Warn users for some importand changes
623 @command(network_cmds)
624 class network_connect(_port_create):
625 """Connect a network with a device (server or router)"""
628 name=ValueArgument('A human readable name for the port', '--name'),
629 security_group_id=RepeatableArgument(
630 'Add a security group id (can be repeated)',
631 ('-g', '--security-group')),
632 subnet_id=ValueArgument(
633 'Subnet id for fixed ips (used with --ip-address)',
635 ip_address=ValueArgument(
636 'IP address for subnet id (used with --subnet-id', '--ip-address'),
637 wait=FlagArgument('Wait network to connect', ('-w', '--wait')),
641 @errors.cyclades.connection
642 @errors.cyclades.network_id
643 @errors.cyclades.server_id
644 def _run(self, network_id, server_id):
645 self.error('Creating a port to connect network %s with device %s' % (
646 network_id, server_id))
647 self.connect(network_id, server_id)
649 def main(self, network_id, device_id):
650 super(self.__class__, self)._run()
651 self._run(network_id=network_id, server_id=device_id)
654 @command(network_cmds)
655 class network_disconnect(_init_network, _port_wait, _optional_json):
656 """Disconnnect a network from a device"""
658 def _cyclades_client(self):
659 auth = getattr(self, 'auth_base')
660 endpoints = auth.get_service_endpoints('compute')
661 URL = endpoints['publicURL']
662 from kamaki.clients.cyclades import CycladesClient
663 return CycladesClient(URL, self.client.token)
666 wait=FlagArgument('Wait network to disconnect', ('-w', '--wait'))
670 @errors.cyclades.connection
671 @errors.cyclades.network_id
672 @errors.cyclades.server_id
673 def _run(self, network_id, server_id):
674 vm = self._cyclades_client().get_server_details(server_id)
675 ports = [port for port in vm['attachments'] if (
676 port['network_id'] not in ('network_id', ))]
678 raiseCLIError('Network %s is not connected to device %s' % (
679 network_id, server_id))
682 port['status'] = self.client.get_port_details(port['id'])[
684 self.client.delete_port(port['id'])
685 self.error('Deleting port %s:' % port['id'])
686 self.print_dict(port)
689 self._wait(port['id'], port['status'])
690 except ClientError as ce:
691 if ce.status not in (404, ):
693 self.error('Port %s is deleted' % port['id'])
695 def main(self, network_id, device_id):
696 super(self.__class__, self)._run()
697 self._run(network_id=network_id, server_id=device_id)