da5ca7e3eb98727500d319a1ed0d632e409951c0
[kamaki] / kamaki / cli / commands / network.py
1 # Copyright 2011-2013 GRNET S.A. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6 #
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10 #
11 #   2. Redistributions in binary form must reproduce the above
12 #      copyright notice, this list of conditions and the following
13 #      disclaimer in the documentation and/or other materials
14 #      provided with the distribution.
15 #
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28 #
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
33
34 from io import StringIO
35 from pydoc import pager
36
37 from kamaki.cli import command
38 from kamaki.cli.command_tree import CommandTree
39 from kamaki.cli.errors import (
40     CLIBaseUrlError, CLIInvalidArgument, raiseCLIError)
41 from kamaki.clients.cyclades import CycladesNetworkClient, ClientError
42 from kamaki.cli.argument import (
43     FlagArgument, ValueArgument, RepeatableArgument, IntArgument)
44 from kamaki.cli.commands import _command_init, errors, addLogSettings
45 from kamaki.cli.commands import (
46     _optional_output_cmd, _optional_json, _name_filter, _id_filter)
47 from kamaki.cli.commands.cyclades import _service_wait
48
49
50 network_cmds = CommandTree('network', 'Networking API network commands')
51 port_cmds = CommandTree('port', 'Networking API network commands')
52 subnet_cmds = CommandTree('subnet', 'Networking API network commands')
53 ip_cmds = CommandTree('ip', 'Networking API floatingip commands')
54 _commands = [network_cmds, port_cmds, subnet_cmds, ip_cmds]
55
56
57 about_authentication = '\nUser Authentication:\
58     \n* to check authentication: /user authenticate\
59     \n* to set authentication token: /config set cloud.<cloud>.token <token>'
60
61
62 class _port_wait(_service_wait):
63
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,
67             timeout=timeout)
68
69
70 class _init_network(_command_init):
71     @errors.generic.all
72     @addLogSettings
73     def _run(self, service='network'):
74         if getattr(self, 'cloud', None):
75             base_url = self._custom_url(service) or self._custom_url(
76                 'network')
77             if base_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)
82                 return
83         else:
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)
92         else:
93             raise CLIBaseUrlError(service='network')
94
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
98
99     def main(self):
100         self._run()
101
102
103 @command(network_cmds)
104 class network_list(_init_network, _optional_json, _name_filter, _id_filter):
105     """List networks
106     Use filtering arguments (e.g., --name-like) to manage long server lists
107     """
108
109     arguments = dict(
110         detail=FlagArgument('show detailed output', ('-l', '--details')),
111         more=FlagArgument(
112             'output results in pages (-n to set items per page, default 10)',
113             '--more'),
114         user_id=ValueArgument(
115             'show only networks belonging to user with this id', '--user-id')
116     )
117
118     @errors.generic.all
119     @errors.cyclades.connection
120     def _run(self):
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']:
127             nets = [dict(
128                 id=n['id'], name=n['name'], links=n['links']) for n in nets]
129         kwargs = dict()
130         if self['more']:
131             kwargs['out'] = StringIO()
132             kwargs['title'] = ()
133         self._print(nets, **kwargs)
134         if self['more']:
135             pager(kwargs['out'].getvalue())
136
137     def main(self):
138         super(self.__class__, self)._run()
139         self._run()
140
141
142 @command(network_cmds)
143 class network_info(_init_network, _optional_json):
144     """Get details about a network"""
145
146     @errors.generic.all
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)
152
153     def main(self, network_id):
154         super(self.__class__, self)._run()
155         self._run(network_id=network_id)
156
157
158 class NetworkTypeArgument(ValueArgument):
159
160     types = ('MAC_FILTERED', 'CUSTOM', 'IP_LESS_ROUTED', 'PHYSICAL_VLAN')
161
162     @property
163     def value(self):
164         return getattr(self, '_value', self.types[0])
165
166     @value.setter
167     def value(self, new_value):
168         if new_value and new_value.upper() in self.types:
169             self._value = new_value.upper()
170         elif new_value:
171             raise CLIInvalidArgument(
172                 'Invalid network type %s' % new_value, details=[
173                     'Valid types: %s' % ', '.join(self.types), ])
174
175
176 @command(network_cmds)
177 class network_create(_init_network, _optional_json):
178     """Create a new network (default type: MAC_FILTERED)"""
179
180     arguments = dict(
181         name=ValueArgument('Network name', '--name'),
182         shared=FlagArgument(
183             'Make network shared (special privileges required)', '--shared'),
184         network_type=NetworkTypeArgument(
185             'Valid network types: %s' % (', '.join(NetworkTypeArgument.types)),
186             '--type')
187     )
188
189     @errors.generic.all
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)
196
197     def main(self):
198         super(self.__class__, self)._run()
199         self._run(network_type=self['network_type'])
200
201
202 @command(network_cmds)
203 class network_delete(_init_network, _optional_output_cmd):
204     """Delete a network"""
205
206     @errors.generic.all
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)
212
213     def main(self, network_id):
214         super(self.__class__, self)._run()
215         self._run(network_id=network_id)
216
217
218 @command(network_cmds)
219 class network_modify(_init_network, _optional_json):
220     """Modify network attributes"""
221
222     arguments = dict(new_name=ValueArgument('Rename the network', '--name'))
223     required = ['new_name', ]
224
225     @errors.generic.all
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)
231
232     def main(self, network_id):
233         super(self.__class__, self)._run()
234         self._run(network_id=network_id)
235
236
237 @command(subnet_cmds)
238 class subnet_list(_init_network, _optional_json, _name_filter, _id_filter):
239     """List subnets
240     Use filtering arguments (e.g., --name-like) to manage long server lists
241     """
242
243     arguments = dict(
244         detail=FlagArgument('show detailed output', ('-l', '--details')),
245         more=FlagArgument(
246             'output results in pages (-n to set items per page, default 10)',
247             '--more')
248     )
249
250     @errors.generic.all
251     @errors.cyclades.connection
252     def _run(self):
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']:
257             nets = [dict(
258                 id=n['id'], name=n['name'], links=n['links']) for n in nets]
259         kwargs = dict()
260         if self['more']:
261             kwargs['out'] = StringIO()
262             kwargs['title'] = ()
263         self._print(nets, **kwargs)
264         if self['more']:
265             pager(kwargs['out'].getvalue())
266
267     def main(self):
268         super(self.__class__, self)._run()
269         self._run()
270
271
272 @command(subnet_cmds)
273 class subnet_info(_init_network, _optional_json):
274     """Get details about a subnet"""
275
276     @errors.generic.all
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)
281
282     def main(self, subnet_id):
283         super(self.__class__, self)._run()
284         self._run(subnet_id=subnet_id)
285
286
287 class AllocationPoolArgument(RepeatableArgument):
288
289     @property
290     def value(self):
291         return super(AllocationPoolArgument, self).value or []
292
293     @value.setter
294     def value(self, new_pools):
295         new_list = []
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
305
306
307 @command(subnet_cmds)
308 class subnet_create(_init_network, _optional_json):
309     """Create a new subnet"""
310
311     arguments = dict(
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',
316             '--alloc-pool'),
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')
323     )
324     required = ('network_id', 'cidr')
325
326     @errors.generic.all
327     @errors.cyclades.connection
328     @errors.cyclades.network_id
329     def _run(self, network_id, cidr):
330         net = self.client.create_subnet(
331             network_id, cidr,
332             self['name'], self['allocation_pools'], self['gateway'],
333             self['subnet_id'], self['ipv6'], self['enable_dhcp'])
334         self._print(net, self.print_dict)
335
336     def main(self):
337         super(self.__class__, self)._run()
338         self._run(network_id=self['network_id'], cidr=self['cidr'])
339
340
341 # @command(subnet_cmds)
342 # class subnet_delete(_init_network, _optional_output_cmd):
343 #     """Delete a subnet"""
344
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)
350
351 #     def main(self, subnet_id):
352 #         super(self.__class__, self)._run()
353 #         self._run(subnet_id=subnet_id)
354
355
356 @command(subnet_cmds)
357 class subnet_modify(_init_network, _optional_json):
358     """Modify the attributes of a subnet"""
359
360     arguments = dict(
361         new_name=ValueArgument('New name of the subnet', '--name')
362     )
363     required = ['new_name']
364
365     @errors.generic.all
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)
372
373     def main(self, subnet_id):
374         super(self.__class__, self)._run()
375         self._run(subnet_id=subnet_id)
376
377
378 @command(port_cmds)
379 class port_list(_init_network, _optional_json, _name_filter, _id_filter):
380     """List all ports"""
381
382     arguments = dict(
383         detail=FlagArgument('show detailed output', ('-l', '--details')),
384         more=FlagArgument(
385             'output results in pages (-n to set items per page, default 10)',
386             '--more'),
387         user_id=ValueArgument(
388             'show only networks belonging to user with this id', '--user-id')
389     )
390
391     @errors.generic.all
392     @errors.cyclades.connection
393     def _run(self):
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']:
400             ports = [dict(
401                 id=p['id'], name=p['name'], links=p['links']) for p in ports]
402         kwargs = dict()
403         if self['more']:
404             kwargs['out'] = StringIO()
405             kwargs['title'] = ()
406         self._print(ports, **kwargs)
407         if self['more']:
408             pager(kwargs['out'].getvalue())
409
410     def main(self):
411         super(self.__class__, self)._run()
412         self._run()
413
414
415 @command(port_cmds)
416 class port_info(_init_network, _optional_json):
417     """Get details about a port"""
418
419     @errors.generic.all
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)
424
425     def main(self, port_id):
426         super(self.__class__, self)._run()
427         self._run(port_id=port_id)
428
429
430 @command(port_cmds)
431 class port_delete(_init_network, _optional_output_cmd, _port_wait):
432     """Delete a port (== disconnect server from network)"""
433
434     arguments = dict(
435         wait=FlagArgument('Wait port to be established', ('-w', '--wait'))
436     )
437
438     @errors.generic.all
439     @errors.cyclades.connection
440     def _run(self, port_id):
441         if self['wait']:
442             status = self.client.get_port_details(port_id)['status']
443         r = self.client.delete_port(port_id)
444         if self['wait']:
445             try:
446                 self._wait(port_id, status)
447             except ClientError as ce:
448                 if ce.status not in (404, ):
449                     raise
450                 self.error('Port %s is deleted' % port_id)
451         self._optional_output(r)
452
453     def main(self, port_id):
454         super(self.__class__, self)._run()
455         self._run(port_id=port_id)
456
457
458 @command(port_cmds)
459 class port_modify(_init_network, _optional_json):
460     """Modify the attributes of a port"""
461
462     arguments = dict(new_name=ValueArgument('New name of the port', '--name'))
463     required = ['new_name', ]
464
465     @errors.generic.all
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)
472
473     def main(self, port_id):
474         super(self.__class__, self)._run()
475         self._run(port_id=port_id)
476
477
478 class _port_create(_init_network, _optional_json, _port_wait):
479
480     def connect(self, network_id, device_id):
481         fixed_ips = [dict(
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,
486             name=self['name'],
487             security_groups=self['security_group_id'],
488             fixed_ips=fixed_ips)
489         if self['wait']:
490             self._wait(r['id'], r['status'])
491             r = self.client.get_port_details(r['id'])
492         self._print([r])
493
494
495 @command(port_cmds)
496 class port_create(_port_create):
497     """Create a new port (== connect server to network)"""
498
499     arguments = dict(
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)',
506             '--subnet-id'),
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',
512             '--device-id'),
513         wait=FlagArgument('Wait port to be established', ('-w', '--wait')),
514     )
515     required = ('network_id', 'device_id')
516
517     @errors.generic.all
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)
523
524     def main(self):
525         super(self.__class__, self)._run()
526         self._run(network_id=self['network_id'], server_id=self['device_id'])
527
528
529 @command(port_cmds)
530 class port_wait(_init_network, _port_wait):
531     """Wait for port to finish [ACTIVE, DOWN, BUILD, ERROR]"""
532
533     arguments = dict(
534         timeout=IntArgument(
535             'Wait limit in seconds (default: 60)', '--timeout', default=60)
536     )
537
538     @errors.generic.all
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'])
544         else:
545             self.error(
546                 'Port %s: Cannot wait for status %s, '
547                 'status is already %s' % (
548                     port_id, current_status, port['status']))
549
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)
553
554
555 @command(ip_cmds)
556 class ip_list(_init_network, _optional_json):
557     """List reserved floating IPs"""
558
559     @errors.generic.all
560     @errors.cyclades.connection
561     def _run(self):
562         self._print(self.client.list_floatingips())
563
564     def main(self):
565         super(self.__class__, self)._run()
566         self._run()
567
568
569 @command(ip_cmds)
570 class ip_info(_init_network, _optional_json):
571     """Get details on a floating IP"""
572
573     @errors.generic.all
574     @errors.cyclades.connection
575     def _run(self, ip_id):
576         self._print(
577             self.client.get_floatingip_details(ip_id), self.print_dict)
578
579     def main(self, ip_id):
580         super(self.__class__, self)._run()
581         self._run(ip_id=ip_id)
582
583
584 @command(ip_cmds)
585 class ip_create(_init_network, _optional_json):
586     """Reserve an IP on a network"""
587
588     arguments = dict(
589         network_id=ValueArgument(
590             'The network to preserve the IP on', '--network-id'),
591         ip_address=ValueArgument('Allocate a specific IP address', '--address')
592     )
593     required = ('network_id', )
594
595     @errors.generic.all
596     @errors.cyclades.connection
597     @errors.cyclades.network_id
598     def _run(self, network_id):
599         self._print(
600             self.client.create_floatingip(
601                 network_id, floating_ip_address=self['ip_address']),
602             self.print_dict)
603
604     def main(self):
605         super(self.__class__, self)._run()
606         self._run(network_id=self['network_id'])
607
608
609 @command(ip_cmds)
610 class ip_delete(_init_network, _optional_output_cmd):
611     """Unreserve an IP (also delete the port, if attached)"""
612
613     def _run(self, ip_id):
614         self._optional_output(self.client.delete_floatingip(ip_id))
615
616     def main(self, ip_id):
617         super(self.__class__, self)._run()
618         self._run(ip_id=ip_id)
619
620
621 #  Warn users for some importand changes
622
623 @command(network_cmds)
624 class network_connect(_port_create):
625     """Connect a network with a device (server or router)"""
626
627     arguments = dict(
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)',
634             '--subnet-id'),
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')),
638     )
639
640     @errors.generic.all
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)
648
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)
652
653
654 @command(network_cmds)
655 class network_disconnect(_init_network, _port_wait, _optional_json):
656     """Disconnnect a network from a device"""
657
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)
664
665     arguments = dict(
666         wait=FlagArgument('Wait network to disconnect', ('-w', '--wait'))
667     )
668
669     @errors.generic.all
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', ))]
677         if not ports:
678             raiseCLIError('Network %s is not connected to device %s' % (
679                 network_id, server_id))
680         for port in ports:
681             if self['wait']:
682                 port['status'] = self.client.get_port_details(port['id'])[
683                     'status']
684             self.client.delete_port(port['id'])
685             self.error('Deleting port %s:' % port['id'])
686             self.print_dict(port)
687             if self['wait']:
688                 try:
689                     self._wait(port['id'], port['status'])
690                 except ClientError as ce:
691                     if ce.status not in (404, ):
692                         raise
693                     self.error('Port %s is deleted' % port['id'])
694
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)