Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / network.py @ 17cfc2f0

History | View | Annotate | Download (21.3 kB)

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
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.utils import filter_dicts_by_dict
48
from kamaki.cli.commands.cyclades import _service_wait
49

    
50

    
51
network_cmds = CommandTree('network', 'Networking API network commands')
52
port_cmds = CommandTree('port', 'Networking API network commands')
53
subnet_cmds = CommandTree('subnet', 'Networking API network commands')
54
ip_cmds = CommandTree('ip', 'Networking 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: /user authenticate\
60
    \n* to set authentication token: /config set cloud.<cloud>.token <token>'
61

    
62

    
63
class _port_wait(_service_wait):
64

    
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,
68
            timeout=timeout)
69

    
70

    
71
class _init_network(_command_init):
72
    @errors.generic.all
73
    @addLogSettings
74
    def _run(self, service='network'):
75
        if getattr(self, 'cloud', None):
76
            base_url = self._custom_url(service) or self._custom_url(
77
                'network')
78
            if base_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)
83
                return
84
        else:
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)
93
        else:
94
            raise CLIBaseUrlError(service='network')
95

    
96
    def main(self):
97
        self._run()
98

    
99

    
100
@command(network_cmds)
101
class network_list(_init_network, _optional_json, _name_filter, _id_filter):
102
    """List networks
103
    Use filtering arguments (e.g., --name-like) to manage long server lists
104
    """
105

    
106
    arguments = dict(
107
        detail=FlagArgument('show detailed output', ('-l', '--details')),
108
        more=FlagArgument(
109
            'output results in pages (-n to set items per page, default 10)',
110
            '--more'),
111
        user_id=ValueArgument(
112
            'show only networks belonging to user with this id', '--user-id')
113
    )
114

    
115
    def _filter_by_user_id(self, nets):
116
        return filter_dicts_by_dict(nets, dict(user_id=self['user_id'])) if (
117
            self['user_id']) else nets
118

    
119
    @errors.generic.all
120
    @errors.cyclades.connection
121
    def _run(self):
122
        nets = self.client.list_networks()
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']:
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 = ('CUSTOM', 'MAC_FILTERED', 'IP_LESS_ROUTED', 'PHYSICAL_VLAN')
161

    
162
    @property
163
    def value(self):
164
        return getattr(self, '_value', None)
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"""
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
    required = ('network_type', )
189

    
190
    @errors.generic.all
191
    @errors.cyclades.connection
192
    @errors.cyclades.network_type
193
    def _run(self, network_type):
194
        net = self.client.create_network(
195
            network_type, name=self['name'], shared=self['shared'])
196
        self._print(net, self.print_dict)
197

    
198
    def main(self):
199
        super(self.__class__, self)._run()
200
        self._run(network_type=self['network_type'])
201

    
202

    
203
@command(network_cmds)
204
class network_delete(_init_network, _optional_output_cmd):
205
    """Delete a network"""
206

    
207
    @errors.generic.all
208
    @errors.cyclades.connection
209
    @errors.cyclades.network_id
210
    def _run(self, network_id):
211
        r = self.client.delete_network(network_id)
212
        self._optional_output(r)
213

    
214
    def main(self, network_id):
215
        super(self.__class__, self)._run()
216
        self._run(network_id=network_id)
217

    
218

    
219
@command(network_cmds)
220
class network_modify(_init_network, _optional_json):
221
    """Modify network attributes"""
222

    
223
    arguments = dict(new_name=ValueArgument('Rename the network', '--name'))
224
    required = ['new_name', ]
225

    
226
    @errors.generic.all
227
    @errors.cyclades.connection
228
    @errors.cyclades.network_id
229
    def _run(self, network_id):
230
        r = self.client.update_network(network_id, name=self['new_name'])
231
        self._print(r, self.print_dict)
232

    
233
    def main(self, network_id):
234
        super(self.__class__, self)._run()
235
        self._run(network_id=network_id)
236

    
237

    
238
@command(subnet_cmds)
239
class subnet_list(_init_network, _optional_json, _name_filter, _id_filter):
240
    """List subnets
241
    Use filtering arguments (e.g., --name-like) to manage long server lists
242
    """
243

    
244
    arguments = dict(
245
        detail=FlagArgument('show detailed output', ('-l', '--details')),
246
        more=FlagArgument(
247
            'output results in pages (-n to set items per page, default 10)',
248
            '--more')
249
    )
250

    
251
    @errors.generic.all
252
    @errors.cyclades.connection
253
    def _run(self):
254
        nets = self.client.list_subnets()
255
        nets = self._filter_by_name(nets)
256
        nets = self._filter_by_id(nets)
257
        if not self['detail']:
258
            nets = [dict(
259
                id=n['id'], name=n['name'], links=n['links']) for n in nets]
260
        kwargs = dict()
261
        if self['more']:
262
            kwargs['out'] = StringIO()
263
            kwargs['title'] = ()
264
        self._print(nets, **kwargs)
265
        if self['more']:
266
            pager(kwargs['out'].getvalue())
267

    
268
    def main(self):
269
        super(self.__class__, self)._run()
270
        self._run()
271

    
272

    
273
@command(subnet_cmds)
274
class subnet_info(_init_network, _optional_json):
275
    """Get details about a subnet"""
276

    
277
    @errors.generic.all
278
    @errors.cyclades.connection
279
    def _run(self, subnet_id):
280
        net = self.client.get_subnet_details(subnet_id)
281
        self._print(net, self.print_dict)
282

    
283
    def main(self, subnet_id):
284
        super(self.__class__, self)._run()
285
        self._run(subnet_id=subnet_id)
286

    
287

    
288
class AllocationPoolArgument(RepeatableArgument):
289

    
290
    @property
291
    def value(self):
292
        return super(AllocationPoolArgument, self).value or []
293

    
294
    @value.setter
295
    def value(self, new_pools):
296
        new_list = []
297
        for pool in new_pools:
298
            start, comma, end = pool.partition(',')
299
            if not (start and comma and end):
300
                raise CLIInvalidArgument(
301
                    'Invalid allocation pool argument %s' % pool, details=[
302
                    'Allocation values must be of the form:',
303
                    '  <start address>,<end address>'])
304
            new_list.append(dict(start=start, end=end))
305
        self._value = new_list
306

    
307

    
308
@command(subnet_cmds)
309
class subnet_create(_init_network, _optional_json):
310
    """Create a new subnet"""
311

    
312
    arguments = dict(
313
        name=ValueArgument('Subnet name', '--name'),
314
        allocation_pools=AllocationPoolArgument(
315
            'start_address,end_address of allocation pool (can be repeated)'
316
            ' e.g., --alloc-pool=123.45.67.1,123.45.67.8',
317
            '--alloc-pool'),
318
        gateway=ValueArgument('Gateway IP', '--gateway'),
319
        subnet_id=ValueArgument('The id for the subnet', '--id'),
320
        ipv6=FlagArgument('If set, IP version is set to 6, else 4', '--ipv6'),
321
        enable_dhcp=FlagArgument('Enable dhcp (default: off)', '--with-dhcp'),
322
        network_id=ValueArgument('Set the network ID', '--network-id'),
323
        cidr=ValueArgument('Set the CIDR', '--cidr')
324
    )
325
    required = ('network_id', 'cidr')
326

    
327
    @errors.generic.all
328
    @errors.cyclades.connection
329
    @errors.cyclades.network_id
330
    def _run(self, network_id, cidr):
331
        net = self.client.create_subnet(
332
            network_id, cidr,
333
            self['name'], self['allocation_pools'], self['gateway'],
334
            self['subnet_id'], self['ipv6'], self['enable_dhcp'])
335
        self._print(net, self.print_dict)
336

    
337
    def main(self):
338
        super(self.__class__, self)._run()
339
        self._run(network_id=self['network_id'], cidr=self['cidr'])
340

    
341

    
342
# @command(subnet_cmds)
343
# class subnet_delete(_init_network, _optional_output_cmd):
344
#     """Delete a subnet"""
345

    
346
#     @errors.generic.all
347
#     @errors.cyclades.connection
348
#     def _run(self, subnet_id):
349
#         r = self.client.delete_subnet(subnet_id)
350
#         self._optional_output(r)
351

    
352
#     def main(self, subnet_id):
353
#         super(self.__class__, self)._run()
354
#         self._run(subnet_id=subnet_id)
355

    
356

    
357
@command(subnet_cmds)
358
class subnet_modify(_init_network, _optional_json):
359
    """Modify the attributes of a subnet"""
360

    
361
    arguments = dict(
362
        new_name=ValueArgument('New name of the subnet', '--name')
363
    )
364
    required = ['new_name']
365

    
366
    @errors.generic.all
367
    @errors.cyclades.connection
368
    def _run(self, subnet_id):
369
        r = self.client.get_subnet_details(subnet_id)
370
        r = self.client.update_subnet(
371
            subnet_id, r['network_id'], name=self['new_name'])
372
        self._print(r, self.print_dict)
373

    
374
    def main(self, subnet_id):
375
        super(self.__class__, self)._run()
376
        self._run(subnet_id=subnet_id)
377

    
378

    
379
@command(port_cmds)
380
class port_list(_init_network, _optional_json):
381
    """List all ports"""
382

    
383
    @errors.generic.all
384
    @errors.cyclades.connection
385
    def _run(self):
386
        net = self.client.list_ports()
387
        self._print(net)
388

    
389
    def main(self):
390
        super(self.__class__, self)._run()
391
        self._run()
392

    
393

    
394
@command(port_cmds)
395
class port_info(_init_network, _optional_json):
396
    """Get details about a port"""
397

    
398
    @errors.generic.all
399
    @errors.cyclades.connection
400
    def _run(self, port_id):
401
        net = self.client.get_port_details(port_id)
402
        self._print(net, self.print_dict)
403

    
404
    def main(self, port_id):
405
        super(self.__class__, self)._run()
406
        self._run(port_id=port_id)
407

    
408

    
409
@command(port_cmds)
410
class port_delete(_init_network, _optional_output_cmd, _port_wait):
411
    """Delete a port (== disconnect server from network)"""
412

    
413
    arguments = dict(
414
        wait=FlagArgument('Wait port to be established', ('-w', '--wait'))
415
    )
416

    
417
    @errors.generic.all
418
    @errors.cyclades.connection
419
    def _run(self, port_id):
420
        r = self.client.delete_port(port_id)
421
        if self['wait']:
422
            self._wait(r['id'], r['status'])
423
        self._optional_output(r)
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_modify(_init_network, _optional_json):
432
    """Modify the attributes of a port"""
433

    
434
    arguments = dict(new_name=ValueArgument('New name of the port', '--name'))
435
    required = ['new_name', ]
436

    
437
    @errors.generic.all
438
    @errors.cyclades.connection
439
    def _run(self, port_id):
440
        r = self.client.get_port_details(port_id)
441
        r = self.client.update_port(
442
            port_id, r['network_id'], name=self['new_name'])
443
        self._print(r, self.print_dict)
444

    
445
    def main(self, port_id):
446
        super(self.__class__, self)._run()
447
        self._run(port_id=port_id)
448

    
449

    
450
class _port_create(_init_network, _optional_json, _port_wait):
451

    
452
    def connect(self, network_id, device_id):
453
        fixed_ips = [dict(
454
            subnet_id=self['subnet_id'], ip_address=self['ip_address'])] if (
455
                self['subnet_id']) else None
456
        r = self.client.create_port(
457
            network_id, device_id,
458
            name=self['name'],
459
            security_groups=self['security_group_id'],
460
            fixed_ips=fixed_ips)
461
        if self['wait']:
462
            self._wait(r['id'], r['status'])
463
        self._print(r, self.print_dict)
464

    
465

    
466
@command(port_cmds)
467
class port_create(_init_network):
468
    """Create a new port (== connect server to network)"""
469

    
470
    arguments = dict(
471
        name=ValueArgument('A human readable name', '--name'),
472
        security_group_id=RepeatableArgument(
473
            'Add a security group id (can be repeated)',
474
            ('-g', '--security-group')),
475
        subnet_id=ValueArgument(
476
            'Subnet id for fixed ips (used with --ip-address)',
477
            '--subnet-id'),
478
        ip_address=ValueArgument(
479
            'IP address for subnet id (used with --subnet-id', '--ip-address'),
480
        network_id=ValueArgument('Set the network ID', '--network-id'),
481
        device_id=ValueArgument(
482
            'The device is either a virtual server or a virtual router',
483
            '--device-id'),
484
        wait=FlagArgument('Wait port to be established', ('-w', '--wait')),
485
    )
486
    required = ('network_id', 'device_id')
487

    
488
    @errors.generic.all
489
    @errors.cyclades.connection
490
    @errors.cyclades.network_id
491
    @errors.cyclades.server_id
492
    def _run(self, network_id, server_id):
493
        self.connect(network_id, server_id)
494

    
495
    def main(self):
496
        super(self.__class__, self)._run()
497
        self._run(network_id=self['network_id'], server_id=self['device_id'])
498

    
499

    
500
@command(port_cmds)
501
class port_wait(_init_network, _port_wait):
502
    """Wait for port to finish [ACTIVE, DOWN, BUILD, ERROR]"""
503

    
504
    arguments = dict(
505
        timeout=IntArgument(
506
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
507
    )
508

    
509
    @errors.generic.all
510
    @errors.cyclades.connection
511
    def _run(self, port_id, current_status):
512
        port = self.client.get_port_details(port_id)
513
        if port['status'].lower() == current_status.lower():
514
            self._wait(port_id, current_status, timeout=self['timeout'])
515
        else:
516
            self.error(
517
                'Port %s: Cannot wait for status %s, '
518
                'status is already %s' % (
519
                    port_id, current_status, port['status']))
520

    
521
    def main(self, port_id, current_status='BUILD'):
522
        super(self.__class__, self)._run()
523
        self._run(port_id=port_id, current_status=current_status)
524

    
525

    
526
@command(ip_cmds)
527
class ip_list(_init_network, _optional_json):
528
    """List reserved floating IPs"""
529

    
530
    @errors.generic.all
531
    @errors.cyclades.connection
532
    def _run(self):
533
        self._print(self.client.list_floatingips())
534

    
535
    def main(self):
536
        super(self.__class__, self)._run()
537
        self._run()
538

    
539

    
540
@command(ip_cmds)
541
class ip_info(_init_network, _optional_json):
542
    """Get details on a floating IP"""
543

    
544
    @errors.generic.all
545
    @errors.cyclades.connection
546
    def _run(self, ip_id):
547
        self._print(
548
            self.client.get_floatingip_details(ip_id), self.print_dict)
549

    
550
    def main(self, ip_id):
551
        super(self.__class__, self)._run()
552
        self._run(ip_id=ip_id)
553

    
554

    
555
@command(ip_cmds)
556
class ip_create(_init_network, _optional_json):
557
    """Reserve an IP on a network"""
558

    
559
    arguments = dict(
560
        network_id=ValueArgument(
561
            'The network to preserve the IP on', '--network-id'),
562
        ip_address=ValueArgument('Allocate a specific IP address', '--address')
563
    )
564
    required = ('network_id', )
565

    
566
    @errors.generic.all
567
    @errors.cyclades.connection
568
    @errors.cyclades.network_id
569
    def _run(self, network_id):
570
        self._print(
571
            self.client.create_floatingip(
572
                network_id, floating_ip_address=self['ip_address']),
573
            self.print_dict)
574

    
575
    def main(self):
576
        super(self.__class__, self)._run()
577
        self._run(network_id=self['network_id'])
578

    
579

    
580
@command(ip_cmds)
581
class ip_delete(_init_network, _optional_output_cmd):
582
    """Unreserve an IP (also delete the port, if attached)"""
583

    
584
    def _run(self, ip_id):
585
        self._optional_output(self.client.delete_floatingip(ip_id))
586

    
587
    def main(self, ip_id):
588
        super(self.__class__, self)._run()
589
        self._run(ip_id=ip_id)
590

    
591

    
592
#  Warn users for some importand changes
593

    
594
@command(network_cmds)
595
class network_connect(_port_create):
596
    """Connect a network with a device (server or router)"""
597

    
598
    arguments = dict(
599
        name=ValueArgument('A human readable name for the port', '--name'),
600
        security_group_id=RepeatableArgument(
601
            'Add a security group id (can be repeated)',
602
            ('-g', '--security-group')),
603
        subnet_id=ValueArgument(
604
            'Subnet id for fixed ips (used with --ip-address)',
605
            '--subnet-id'),
606
        ip_address=ValueArgument(
607
            'IP address for subnet id (used with --subnet-id', '--ip-address'),
608
        wait=FlagArgument('Wait network to connect', ('-w', '--wait')),
609
    )
610

    
611
    @errors.generic.all
612
    @errors.cyclades.connection
613
    @errors.cyclades.network_id
614
    @errors.cyclades.server_id
615
    def _run(self, network_id, server_id):
616
        self.connect(network_id, server_id)
617

    
618
    def main(self, network_id, device_id):
619
        super(self.__class__, self)._run()
620
        self._run(network_id=network_id, device_id=device_id)
621

    
622

    
623
@command(network_cmds)
624
class network_disconnect(_init_network, _port_wait, _optional_json):
625
    """Disconnnect a network from a device"""
626

    
627
    def _cyclades_client(self):
628
        auth = getattr(self, 'auth_base')
629
        endpoints = auth.get_service_endpoints('compute')
630
        URL = endpoints['publicURL']
631
        from kamaki.clients.cyclades import CycladesClient
632
        return CycladesClient(URL, self.client.token)
633

    
634
    arguments = dict(
635
        wait=FlagArgument('Wait network to disconnect', ('-w', '--wait'))
636
    )
637

    
638
    @errors.generic.all
639
    @errors.cyclades.connection
640
    @errors.cyclades.network_id
641
    @errors.cyclades.server_id
642
    def _run(self, network_id, device_id):
643
        vm = self._cyclades_client().get_server_details(device_id)
644
        nets = [net for net in vm['attachments'] if net['network_id'] not in (
645
            'network_id', )]
646
        if not nets:
647
            raiseCLIError('Network %s is not connected to device %s' % (
648
                network_id, device_id))
649
        for net in nets:
650
            self.client.port_delete(net['id'])
651
            self.error('Deleting this connection:')
652
            self.print_dict(net)
653
            if self['wait']:
654
                self._wait(net['id'], net['status'])
655

    
656
    def main(self, network_id, device_id):
657
        super(self.__class__, self)._run()
658
        self._run(network_id=network_id, device_id=device_id)