Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ e9c73313

History | View | Annotate | Download (40.4 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 base64 import b64encode
35
from os.path import exists
36
from io import StringIO
37
from pydoc import pager
38

    
39
from kamaki.cli import command
40
from kamaki.cli.command_tree import CommandTree
41
from kamaki.cli.utils import remove_from_items, filter_dicts_by_dict
42
from kamaki.cli.errors import raiseCLIError, CLISyntaxError, CLIBaseUrlError
43
from kamaki.clients.cyclades import CycladesClient, ClientError
44
from kamaki.cli.argument import FlagArgument, ValueArgument, KeyValueArgument
45
from kamaki.cli.argument import ProgressBarArgument, DateArgument, IntArgument
46
from kamaki.cli.commands import _command_init, errors, addLogSettings
47
from kamaki.cli.commands import (
48
    _optional_output_cmd, _optional_json, _name_filter, _id_filter)
49

    
50

    
51
server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
52
flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
53
network_cmds = CommandTree('network', 'Cyclades/Compute API network commands')
54
_commands = [server_cmds, flavor_cmds, network_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
howto_personality = [
62
    'Defines a file to be injected to virtual servers file system.',
63
    'syntax:  PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
64
    '  PATH: local file to be injected (relative or absolute)',
65
    '  SERVER_PATH: destination location inside server Image',
66
    '  OWNER: virtual servers user id of the remote destination file',
67
    '  GROUP: virtual servers group id or name of the destination file',
68
    '  MODEL: permition in octal (e.g., 0777 or o+rwx)']
69

    
70

    
71
class _service_wait(object):
72

    
73
    wait_arguments = dict(
74
        progress_bar=ProgressBarArgument(
75
            'do not show progress bar', ('-N', '--no-progress-bar'), False)
76
    )
77

    
78
    def _wait(
79
            self, service, service_id, status_method, current_status,
80
            timeout=True):
81
        (progress_bar, wait_cb) = self._safe_progress_bar(
82
            '%s %s: status is still %s' % (
83
                service, service_id, current_status),
84
            timeout=timeout)
85

    
86
        try:
87
            new_mode = status_method(
88
                service_id, current_status, wait_cb=wait_cb)
89
            if new_mode:
90
                self.error('%s %s: status is now %s' % (
91
                    service, service_id, new_mode))
92
            else:
93
                self.error('%s %s: (timeout) status is still %s' % (
94
                    service, service_id, current_status))
95
        except KeyboardInterrupt:
96
            self.error('\n- canceled')
97
        finally:
98
            self._safe_progress_bar_finish(progress_bar)
99

    
100

    
101
class _server_wait(_service_wait):
102

    
103
    def _wait(self, server_id, current_status):
104
        super(_server_wait, self)._wait(
105
            'Server', server_id, self.client.wait_server, current_status,
106
            timeout=(current_status not in ('BUILD', )))
107

    
108

    
109
class _network_wait(_service_wait):
110

    
111
    def _wait(self, net_id, current_status):
112
        super(_network_wait, self)._wait(
113
            'Network', net_id, self.client.wait_network, current_status)
114

    
115

    
116
class _init_cyclades(_command_init):
117
    @errors.generic.all
118
    @addLogSettings
119
    def _run(self, service='compute'):
120
        if getattr(self, 'cloud', None):
121
            base_url = self._custom_url(service) or self._custom_url(
122
                'cyclades')
123
            if base_url:
124
                token = self._custom_token(service) or self._custom_token(
125
                    'cyclades') or self.config.get_cloud('token')
126
                self.client = CycladesClient(base_url=base_url, token=token)
127
                return
128
        else:
129
            self.cloud = 'default'
130
        if getattr(self, 'auth_base', False):
131
            cyclades_endpoints = self.auth_base.get_service_endpoints(
132
                self._custom_type('cyclades') or 'compute',
133
                self._custom_version('cyclades') or '')
134
            base_url = cyclades_endpoints['publicURL']
135
            token = self.auth_base.token
136
            self.client = CycladesClient(base_url=base_url, token=token)
137
        else:
138
            raise CLIBaseUrlError(service='cyclades')
139

    
140
    def main(self):
141
        self._run()
142

    
143

    
144
@command(server_cmds)
145
class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
146
    """List virtual servers accessible by user
147
    Use filtering arguments (e.g., --name-like) to manage long server lists
148
    """
149

    
150
    PERMANENTS = ('id', 'name')
151

    
152
    arguments = dict(
153
        detail=FlagArgument('show detailed output', ('-l', '--details')),
154
        since=DateArgument(
155
            'show only items since date (\' d/m/Y H:M:S \')',
156
            '--since'),
157
        limit=IntArgument(
158
            'limit number of listed virtual servers', ('-n', '--number')),
159
        more=FlagArgument(
160
            'output results in pages (-n to set items per page, default 10)',
161
            '--more'),
162
        enum=FlagArgument('Enumerate results', '--enumerate'),
163
        flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
164
        image_id=ValueArgument('filter by image id', ('--image-id')),
165
        user_id=ValueArgument('filter by user id', ('--user-id')),
166
        user_name=ValueArgument('filter by user name', ('--user-name')),
167
        status=ValueArgument(
168
            'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
169
            ('--status')),
170
        meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
171
        meta_like=KeyValueArgument(
172
            'print only if in key=value, the value is part of actual value',
173
            ('--metadata-like')),
174
    )
175

    
176
    def _add_user_name(self, servers):
177
        uuids = self._uuids2usernames(list(set(
178
                [srv['user_id'] for srv in servers] +
179
                [srv['tenant_id'] for srv in servers])))
180
        for srv in servers:
181
            srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
182
            srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
183
        return servers
184

    
185
    def _apply_common_filters(self, servers):
186
        common_filters = dict()
187
        if self['status']:
188
            common_filters['status'] = self['status']
189
        if self['user_id'] or self['user_name']:
190
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
191
            common_filters['user_id'] = uuid
192
        return filter_dicts_by_dict(servers, common_filters)
193

    
194
    def _filter_by_image(self, servers):
195
        iid = self['image_id']
196
        return [srv for srv in servers if srv['image']['id'] == iid]
197

    
198
    def _filter_by_flavor(self, servers):
199
        fid = self['flavor_id']
200
        return [srv for srv in servers if (
201
            '%s' % srv['image']['id'] == '%s' % fid)]
202

    
203
    def _filter_by_metadata(self, servers):
204
        new_servers = []
205
        for srv in servers:
206
            if not 'metadata' in srv:
207
                continue
208
            meta = [dict(srv['metadata'])]
209
            if self['meta']:
210
                meta = filter_dicts_by_dict(meta, self['meta'])
211
            if meta and self['meta_like']:
212
                meta = filter_dicts_by_dict(
213
                    meta, self['meta_like'], exact_match=False)
214
            if meta:
215
                new_servers.append(srv)
216
        return new_servers
217

    
218
    @errors.generic.all
219
    @errors.cyclades.connection
220
    @errors.cyclades.date
221
    def _run(self):
222
        withimage = bool(self['image_id'])
223
        withflavor = bool(self['flavor_id'])
224
        withmeta = bool(self['meta'] or self['meta_like'])
225
        withcommons = bool(
226
            self['status'] or self['user_id'] or self['user_name'])
227
        detail = self['detail'] or (
228
            withimage or withflavor or withmeta or withcommons)
229
        servers = self.client.list_servers(detail, self['since'])
230

    
231
        servers = self._filter_by_name(servers)
232
        servers = self._filter_by_id(servers)
233
        servers = self._apply_common_filters(servers)
234
        if withimage:
235
            servers = self._filter_by_image(servers)
236
        if withflavor:
237
            servers = self._filter_by_flavor(servers)
238
        if withmeta:
239
            servers = self._filter_by_metadata(servers)
240

    
241
        if self['detail'] and not self['json_output']:
242
            servers = self._add_user_name(servers)
243
        elif not (self['detail'] or self['json_output']):
244
            remove_from_items(servers, 'links')
245
        if detail and not self['detail']:
246
            for srv in servers:
247
                for key in set(srv).difference(self.PERMANENTS):
248
                    srv.pop(key)
249
        kwargs = dict(with_enumeration=self['enum'])
250
        if self['more']:
251
            kwargs['out'] = StringIO()
252
            kwargs['title'] = ()
253
        if self['limit']:
254
            servers = servers[:self['limit']]
255
        self._print(servers, **kwargs)
256
        if self['more']:
257
            pager(kwargs['out'].getvalue())
258

    
259
    def main(self):
260
        super(self.__class__, self)._run()
261
        self._run()
262

    
263

    
264
@command(server_cmds)
265
class server_info(_init_cyclades, _optional_json):
266
    """Detailed information on a Virtual Machine
267
    Contains:
268
    - name, id, status, create/update dates
269
    - network interfaces
270
    - metadata (e.g., os, superuser) and diagnostics
271
    - hardware flavor and os image ids
272
    """
273

    
274
    @errors.generic.all
275
    @errors.cyclades.connection
276
    @errors.cyclades.server_id
277
    def _run(self, server_id):
278
        vm = self.client.get_server_details(server_id)
279
        uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
280
        vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
281
        vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
282
        self._print(vm, self.print_dict)
283

    
284
    def main(self, server_id):
285
        super(self.__class__, self)._run()
286
        self._run(server_id=server_id)
287

    
288

    
289
class PersonalityArgument(KeyValueArgument):
290
    @property
291
    def value(self):
292
        return self._value if hasattr(self, '_value') else []
293

    
294
    @value.setter
295
    def value(self, newvalue):
296
        if newvalue == self.default:
297
            return self.value
298
        self._value = []
299
        for i, terms in enumerate(newvalue):
300
            termlist = terms.split(',')
301
            if len(termlist) > 5:
302
                msg = 'Wrong number of terms (should be 1 to 5)'
303
                raiseCLIError(CLISyntaxError(msg), details=howto_personality)
304
            path = termlist[0]
305
            if not exists(path):
306
                raiseCLIError(
307
                    None,
308
                    '--personality: File %s does not exist' % path,
309
                    importance=1, details=howto_personality)
310
            self._value.append(dict(path=path))
311
            with open(path) as f:
312
                self._value[i]['contents'] = b64encode(f.read())
313
            try:
314
                self._value[i]['path'] = termlist[1]
315
                self._value[i]['owner'] = termlist[2]
316
                self._value[i]['group'] = termlist[3]
317
                self._value[i]['mode'] = termlist[4]
318
            except IndexError:
319
                pass
320

    
321

    
322
@command(server_cmds)
323
class server_create(_init_cyclades, _optional_json, _server_wait):
324
    """Create a server (aka Virtual Machine)
325
    Parameters:
326
    - name: (single quoted text)
327
    - flavor id: Hardware flavor. Pick one from: /flavor list
328
    - image id: OS images. Pick one from: /image list
329
    """
330

    
331
    arguments = dict(
332
        personality=PersonalityArgument(
333
            (80 * ' ').join(howto_personality), ('-p', '--personality')),
334
        wait=FlagArgument('Wait server to build', ('-w', '--wait'))
335
    )
336

    
337
    @errors.generic.all
338
    @errors.cyclades.connection
339
    @errors.plankton.id
340
    @errors.cyclades.flavor_id
341
    def _run(self, name, flavor_id, image_id):
342
        r = self.client.create_server(
343
            name, int(flavor_id), image_id, personality=self['personality'])
344
        usernames = self._uuids2usernames([r['user_id'], r['tenant_id']])
345
        r['user_id'] += ' (%s)' % usernames[r['user_id']]
346
        r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
347
        self._print(r, self.print_dict)
348
        if self['wait']:
349
            self._wait(r['id'], r['status'])
350

    
351
    def main(self, name, flavor_id, image_id):
352
        super(self.__class__, self)._run()
353
        self._run(name=name, flavor_id=flavor_id, image_id=image_id)
354

    
355

    
356
@command(server_cmds)
357
class server_rename(_init_cyclades, _optional_output_cmd):
358
    """Set/update a virtual server name
359
    virtual server names are not unique, therefore multiple servers may share
360
    the same name
361
    """
362

    
363
    @errors.generic.all
364
    @errors.cyclades.connection
365
    @errors.cyclades.server_id
366
    def _run(self, server_id, new_name):
367
        self._optional_output(
368
            self.client.update_server_name(int(server_id), new_name))
369

    
370
    def main(self, server_id, new_name):
371
        super(self.__class__, self)._run()
372
        self._run(server_id=server_id, new_name=new_name)
373

    
374

    
375
@command(server_cmds)
376
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
377
    """Delete a virtual server"""
378

    
379
    arguments = dict(
380
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
381
    )
382

    
383
    @errors.generic.all
384
    @errors.cyclades.connection
385
    @errors.cyclades.server_id
386
    def _run(self, server_id):
387
            status = 'DELETED'
388
            if self['wait']:
389
                details = self.client.get_server_details(server_id)
390
                status = details['status']
391

    
392
            r = self.client.delete_server(int(server_id))
393
            self._optional_output(r)
394

    
395
            if self['wait']:
396
                self._wait(server_id, status)
397

    
398
    def main(self, server_id):
399
        super(self.__class__, self)._run()
400
        self._run(server_id=server_id)
401

    
402

    
403
@command(server_cmds)
404
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
405
    """Reboot a virtual server"""
406

    
407
    arguments = dict(
408
        hard=FlagArgument(
409
            'perform a hard reboot (deprecated)', ('-f', '--force')),
410
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
411
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
412
    )
413

    
414
    @errors.generic.all
415
    @errors.cyclades.connection
416
    @errors.cyclades.server_id
417
    def _run(self, server_id):
418
        hard_reboot = self['hard']
419
        if hard_reboot:
420
            self.error(
421
                'WARNING: -f/--force will be deprecated in version 0.12\n'
422
                '\tIn the future, please use --type=hard instead')
423
        if self['type']:
424
            if self['type'].lower() in ('soft', ):
425
                hard_reboot = False
426
            elif self['type'].lower() in ('hard', ):
427
                hard_reboot = True
428
            else:
429
                raise CLISyntaxError(
430
                    'Invalid reboot type %s' % self['type'],
431
                    importance=2, details=[
432
                        '--type values are either SOFT (default) or HARD'])
433

    
434
        r = self.client.reboot_server(int(server_id), hard_reboot)
435
        self._optional_output(r)
436

    
437
        if self['wait']:
438
            self._wait(server_id, 'REBOOT')
439

    
440
    def main(self, server_id):
441
        super(self.__class__, self)._run()
442
        self._run(server_id=server_id)
443

    
444

    
445
@command(server_cmds)
446
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
447
    """Start an existing virtual server"""
448

    
449
    arguments = dict(
450
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
451
    )
452

    
453
    @errors.generic.all
454
    @errors.cyclades.connection
455
    @errors.cyclades.server_id
456
    def _run(self, server_id):
457
        status = 'ACTIVE'
458
        if self['wait']:
459
            details = self.client.get_server_details(server_id)
460
            status = details['status']
461
            if status in ('ACTIVE', ):
462
                return
463

    
464
        r = self.client.start_server(int(server_id))
465
        self._optional_output(r)
466

    
467
        if self['wait']:
468
            self._wait(server_id, status)
469

    
470
    def main(self, server_id):
471
        super(self.__class__, self)._run()
472
        self._run(server_id=server_id)
473

    
474

    
475
@command(server_cmds)
476
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
477
    """Shutdown an active virtual server"""
478

    
479
    arguments = dict(
480
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
481
    )
482

    
483
    @errors.generic.all
484
    @errors.cyclades.connection
485
    @errors.cyclades.server_id
486
    def _run(self, server_id):
487
        status = 'STOPPED'
488
        if self['wait']:
489
            details = self.client.get_server_details(server_id)
490
            status = details['status']
491
            if status in ('STOPPED', ):
492
                return
493

    
494
        r = self.client.shutdown_server(int(server_id))
495
        self._optional_output(r)
496

    
497
        if self['wait']:
498
            self._wait(server_id, status)
499

    
500
    def main(self, server_id):
501
        super(self.__class__, self)._run()
502
        self._run(server_id=server_id)
503

    
504

    
505
@command(server_cmds)
506
class server_console(_init_cyclades, _optional_json):
507
    """Get a VNC console to access an existing virtual server
508
    Console connection information provided (at least):
509
    - host: (url or address) a VNC host
510
    - port: (int) the gateway to enter virtual server on host
511
    - password: for VNC authorization
512
    """
513

    
514
    @errors.generic.all
515
    @errors.cyclades.connection
516
    @errors.cyclades.server_id
517
    def _run(self, server_id):
518
        self._print(
519
            self.client.get_server_console(int(server_id)), self.print_dict)
520

    
521
    def main(self, server_id):
522
        super(self.__class__, self)._run()
523
        self._run(server_id=server_id)
524

    
525

    
526
@command(server_cmds)
527
class server_resize(_init_cyclades, _optional_output_cmd):
528
    """Set a different flavor for an existing server
529
    To get server ids and flavor ids:
530
    /server list
531
    /flavor list
532
    """
533

    
534
    @errors.generic.all
535
    @errors.cyclades.connection
536
    @errors.cyclades.server_id
537
    @errors.cyclades.flavor_id
538
    def _run(self, server_id, flavor_id):
539
        self._optional_output(self.client.resize_server(server_id, flavor_id))
540

    
541
    def main(self, server_id, flavor_id):
542
        super(self.__class__, self)._run()
543
        self._run(server_id=server_id, flavor_id=flavor_id)
544

    
545

    
546
@command(server_cmds)
547
class server_firewall(_init_cyclades):
548
    """Manage virtual server firewall profiles for public networks"""
549

    
550

    
551
@command(server_cmds)
552
class server_firewall_set(_init_cyclades, _optional_output_cmd):
553
    """Set the firewall profile on virtual server public network
554
    Values for profile:
555
    - DISABLED: Shutdown firewall
556
    - ENABLED: Firewall in normal mode
557
    - PROTECTED: Firewall in secure mode
558
    """
559

    
560
    @errors.generic.all
561
    @errors.cyclades.connection
562
    @errors.cyclades.server_id
563
    @errors.cyclades.firewall
564
    def _run(self, server_id, profile):
565
        self._optional_output(self.client.set_firewall_profile(
566
            server_id=int(server_id), profile=('%s' % profile).upper()))
567

    
568
    def main(self, server_id, profile):
569
        super(self.__class__, self)._run()
570
        self._run(server_id=server_id, profile=profile)
571

    
572

    
573
@command(server_cmds)
574
class server_firewall_get(_init_cyclades):
575
    """Get the firewall profile for a virtual servers' public network"""
576

    
577
    @errors.generic.all
578
    @errors.cyclades.connection
579
    @errors.cyclades.server_id
580
    def _run(self, server_id):
581
        self.writeln(self.client.get_firewall_profile(server_id))
582

    
583
    def main(self, server_id):
584
        super(self.__class__, self)._run()
585
        self._run(server_id=server_id)
586

    
587

    
588
@command(server_cmds)
589
class server_addr(_init_cyclades, _optional_json):
590
    """List the addresses of all network interfaces on a virtual server"""
591

    
592
    arguments = dict(
593
        enum=FlagArgument('Enumerate results', '--enumerate')
594
    )
595

    
596
    @errors.generic.all
597
    @errors.cyclades.connection
598
    @errors.cyclades.server_id
599
    def _run(self, server_id):
600
        reply = self.client.list_server_nics(int(server_id))
601
        self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
602

    
603
    def main(self, server_id):
604
        super(self.__class__, self)._run()
605
        self._run(server_id=server_id)
606

    
607

    
608
@command(server_cmds)
609
class server_metadata(_init_cyclades):
610
    """Manage Server metadata (key:value pairs of server attributes)"""
611

    
612

    
613
@command(server_cmds)
614
class server_metadata_list(_init_cyclades, _optional_json):
615
    """Get server metadata"""
616

    
617
    @errors.generic.all
618
    @errors.cyclades.connection
619
    @errors.cyclades.server_id
620
    @errors.cyclades.metadata
621
    def _run(self, server_id, key=''):
622
        self._print(
623
            self.client.get_server_metadata(int(server_id), key),
624
            self.print_dict)
625

    
626
    def main(self, server_id, key=''):
627
        super(self.__class__, self)._run()
628
        self._run(server_id=server_id, key=key)
629

    
630

    
631
@command(server_cmds)
632
class server_metadata_set(_init_cyclades, _optional_json):
633
    """Set / update virtual server metadata
634
    Metadata should be given in key/value pairs in key=value format
635
    For example: /server metadata set <server id> key1=value1 key2=value2
636
    Old, unreferenced metadata will remain intact
637
    """
638

    
639
    @errors.generic.all
640
    @errors.cyclades.connection
641
    @errors.cyclades.server_id
642
    def _run(self, server_id, keyvals):
643
        assert keyvals, 'Please, add some metadata ( key=value)'
644
        metadata = dict()
645
        for keyval in keyvals:
646
            k, sep, v = keyval.partition('=')
647
            if sep and k:
648
                metadata[k] = v
649
            else:
650
                raiseCLIError(
651
                    'Invalid piece of metadata %s' % keyval,
652
                    importance=2, details=[
653
                        'Correct metadata format: key=val',
654
                        'For example:',
655
                        '/server metadata set <server id>'
656
                        'key1=value1 key2=value2'])
657
        self._print(
658
            self.client.update_server_metadata(int(server_id), **metadata),
659
            self.print_dict)
660

    
661
    def main(self, server_id, *key_equals_val):
662
        super(self.__class__, self)._run()
663
        self._run(server_id=server_id, keyvals=key_equals_val)
664

    
665

    
666
@command(server_cmds)
667
class server_metadata_delete(_init_cyclades, _optional_output_cmd):
668
    """Delete virtual server metadata"""
669

    
670
    @errors.generic.all
671
    @errors.cyclades.connection
672
    @errors.cyclades.server_id
673
    @errors.cyclades.metadata
674
    def _run(self, server_id, key):
675
        self._optional_output(
676
            self.client.delete_server_metadata(int(server_id), key))
677

    
678
    def main(self, server_id, key):
679
        super(self.__class__, self)._run()
680
        self._run(server_id=server_id, key=key)
681

    
682

    
683
@command(server_cmds)
684
class server_stats(_init_cyclades, _optional_json):
685
    """Get virtual server statistics"""
686

    
687
    @errors.generic.all
688
    @errors.cyclades.connection
689
    @errors.cyclades.server_id
690
    def _run(self, server_id):
691
        self._print(
692
            self.client.get_server_stats(int(server_id)), self.print_dict)
693

    
694
    def main(self, server_id):
695
        super(self.__class__, self)._run()
696
        self._run(server_id=server_id)
697

    
698

    
699
@command(server_cmds)
700
class server_wait(_init_cyclades, _server_wait):
701
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
702

    
703
    @errors.generic.all
704
    @errors.cyclades.connection
705
    @errors.cyclades.server_id
706
    def _run(self, server_id, current_status):
707
        r = self.client.get_server_details(server_id)
708
        if r['status'].lower() == current_status.lower():
709
            self._wait(server_id, current_status)
710
        else:
711
            self.error(
712
                'Server %s: Cannot wait for status %s, '
713
                'status is already %s' % (
714
                    server_id, current_status, r['status']))
715

    
716
    def main(self, server_id, current_status='BUILD'):
717
        super(self.__class__, self)._run()
718
        self._run(server_id=server_id, current_status=current_status)
719

    
720

    
721
@command(flavor_cmds)
722
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
723
    """List available hardware flavors"""
724

    
725
    PERMANENTS = ('id', 'name')
726

    
727
    arguments = dict(
728
        detail=FlagArgument('show detailed output', ('-l', '--details')),
729
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
730
        more=FlagArgument(
731
            'output results in pages (-n to set items per page, default 10)',
732
            '--more'),
733
        enum=FlagArgument('Enumerate results', '--enumerate'),
734
        ram=ValueArgument('filter by ram', ('--ram')),
735
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
736
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
737
        disk_template=ValueArgument(
738
            'filter by disk_templace', ('--disk-template'))
739
    )
740

    
741
    def _apply_common_filters(self, flavors):
742
        common_filters = dict()
743
        if self['ram']:
744
            common_filters['ram'] = self['ram']
745
        if self['vcpus']:
746
            common_filters['vcpus'] = self['vcpus']
747
        if self['disk']:
748
            common_filters['disk'] = self['disk']
749
        if self['disk_template']:
750
            common_filters['SNF:disk_template'] = self['disk_template']
751
        return filter_dicts_by_dict(flavors, common_filters)
752

    
753
    @errors.generic.all
754
    @errors.cyclades.connection
755
    def _run(self):
756
        withcommons = self['ram'] or self['vcpus'] or (
757
            self['disk'] or self['disk_template'])
758
        detail = self['detail'] or withcommons
759
        flavors = self.client.list_flavors(detail)
760
        flavors = self._filter_by_name(flavors)
761
        flavors = self._filter_by_id(flavors)
762
        if withcommons:
763
            flavors = self._apply_common_filters(flavors)
764
        if not (self['detail'] or self['json_output']):
765
            remove_from_items(flavors, 'links')
766
        if detail and not self['detail']:
767
            for flv in flavors:
768
                for key in set(flv).difference(self.PERMANENTS):
769
                    flv.pop(key)
770
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
771
        self._print(
772
            flavors,
773
            with_redundancy=self['detail'], with_enumeration=self['enum'],
774
            **kwargs)
775
        if self['more']:
776
            pager(kwargs['out'].getvalue())
777

    
778
    def main(self):
779
        super(self.__class__, self)._run()
780
        self._run()
781

    
782

    
783
@command(flavor_cmds)
784
class flavor_info(_init_cyclades, _optional_json):
785
    """Detailed information on a hardware flavor
786
    To get a list of available flavors and flavor ids, try /flavor list
787
    """
788

    
789
    @errors.generic.all
790
    @errors.cyclades.connection
791
    @errors.cyclades.flavor_id
792
    def _run(self, flavor_id):
793
        self._print(
794
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
795

    
796
    def main(self, flavor_id):
797
        super(self.__class__, self)._run()
798
        self._run(flavor_id=flavor_id)
799

    
800

    
801
def _add_name(self, net):
802
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
803
        if user_id:
804
            uuids.append(user_id)
805
        if tenant_id:
806
            uuids.append(tenant_id)
807
        if uuids:
808
            usernames = self._uuids2usernames(uuids)
809
            if user_id:
810
                net['user_id'] += ' (%s)' % usernames[user_id]
811
            if tenant_id:
812
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]
813

    
814

    
815
@command(network_cmds)
816
class network_info(_init_cyclades, _optional_json):
817
    """Detailed information on a network
818
    To get a list of available networks and network ids, try /network list
819
    """
820

    
821
    @errors.generic.all
822
    @errors.cyclades.connection
823
    @errors.cyclades.network_id
824
    def _run(self, network_id):
825
        network = self.client.get_network_details(int(network_id))
826
        _add_name(self, network)
827
        self._print(network, self.print_dict, exclude=('id'))
828

    
829
    def main(self, network_id):
830
        super(self.__class__, self)._run()
831
        self._run(network_id=network_id)
832

    
833

    
834
@command(network_cmds)
835
class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
836
    """List networks"""
837

    
838
    PERMANENTS = ('id', 'name')
839

    
840
    arguments = dict(
841
        detail=FlagArgument('show detailed output', ('-l', '--details')),
842
        limit=IntArgument('limit # of listed networks', ('-n', '--number')),
843
        more=FlagArgument(
844
            'output results in pages (-n to set items per page, default 10)',
845
            '--more'),
846
        enum=FlagArgument('Enumerate results', '--enumerate'),
847
        status=ValueArgument('filter by status', ('--status')),
848
        public=FlagArgument('only public networks', ('--public')),
849
        private=FlagArgument('only private networks', ('--private')),
850
        dhcp=FlagArgument('show networks with dhcp', ('--with-dhcp')),
851
        no_dhcp=FlagArgument('show networks without dhcp', ('--without-dhcp')),
852
        user_id=ValueArgument('filter by user id', ('--user-id')),
853
        user_name=ValueArgument('filter by user name', ('--user-name')),
854
        gateway=ValueArgument('filter by gateway (IPv4)', ('--gateway')),
855
        gateway6=ValueArgument('filter by gateway (IPv6)', ('--gateway6')),
856
        cidr=ValueArgument('filter by cidr (IPv4)', ('--cidr')),
857
        cidr6=ValueArgument('filter by cidr (IPv6)', ('--cidr6')),
858
        type=ValueArgument('filter by type', ('--type')),
859
    )
860

    
861
    def _apply_common_filters(self, networks):
862
        common_filter = dict()
863
        if self['public']:
864
            if self['private']:
865
                return []
866
            common_filter['public'] = self['public']
867
        elif self['private']:
868
            common_filter['public'] = False
869
        if self['dhcp']:
870
            if self['no_dhcp']:
871
                return []
872
            common_filter['dhcp'] = True
873
        elif self['no_dhcp']:
874
            common_filter['dhcp'] = False
875
        if self['user_id'] or self['user_name']:
876
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
877
            common_filter['user_id'] = uuid
878
        for term in ('status', 'gateway', 'gateway6', 'cidr', 'cidr6', 'type'):
879
            if self[term]:
880
                common_filter[term] = self[term]
881
        return filter_dicts_by_dict(networks, common_filter)
882

    
883
    def _add_name(self, networks, key='user_id'):
884
        uuids = self._uuids2usernames(
885
            list(set([net[key] for net in networks])))
886
        for net in networks:
887
            v = net.get(key, None)
888
            if v:
889
                net[key] += ' (%s)' % uuids[v]
890
        return networks
891

    
892
    @errors.generic.all
893
    @errors.cyclades.connection
894
    def _run(self):
895
        withcommons = False
896
        for term in (
897
                'status', 'public', 'private', 'user_id', 'user_name', 'type',
898
                'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
899
            if self[term]:
900
                withcommons = True
901
                break
902
        detail = self['detail'] or withcommons
903
        networks = self.client.list_networks(detail)
904
        networks = self._filter_by_name(networks)
905
        networks = self._filter_by_id(networks)
906
        if withcommons:
907
            networks = self._apply_common_filters(networks)
908
        if not (self['detail'] or self['json_output']):
909
            remove_from_items(networks, 'links')
910
        if detail and not self['detail']:
911
            for net in networks:
912
                for key in set(net).difference(self.PERMANENTS):
913
                    net.pop(key)
914
        if self['detail'] and not self['json_output']:
915
            self._add_name(networks)
916
            self._add_name(networks, 'tenant_id')
917
        kwargs = dict(with_enumeration=self['enum'])
918
        if self['more']:
919
            kwargs['out'] = StringIO()
920
            kwargs['title'] = ()
921
        if self['limit']:
922
            networks = networks[:self['limit']]
923
        self._print(networks, **kwargs)
924
        if self['more']:
925
            pager(kwargs['out'].getvalue())
926

    
927
    def main(self):
928
        super(self.__class__, self)._run()
929
        self._run()
930

    
931

    
932
@command(network_cmds)
933
class network_create(_init_cyclades, _optional_json, _network_wait):
934
    """Create an (unconnected) network"""
935

    
936
    arguments = dict(
937
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
938
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
939
        dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
940
        type=ValueArgument(
941
            'Valid network types are '
942
            'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
943
            '--with-type',
944
            default='MAC_FILTERED'),
945
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
946
    )
947

    
948
    @errors.generic.all
949
    @errors.cyclades.connection
950
    @errors.cyclades.network_max
951
    def _run(self, name):
952
        r = self.client.create_network(
953
            name,
954
            cidr=self['cidr'],
955
            gateway=self['gateway'],
956
            dhcp=self['dhcp'],
957
            type=self['type'])
958
        _add_name(self, r)
959
        self._print(r, self.print_dict)
960
        if self['wait'] and r['status'] in ('PENDING', ):
961
            self._wait(r['id'], 'PENDING')
962

    
963
    def main(self, name):
964
        super(self.__class__, self)._run()
965
        self._run(name)
966

    
967

    
968
@command(network_cmds)
969
class network_rename(_init_cyclades, _optional_output_cmd):
970
    """Set the name of a network"""
971

    
972
    @errors.generic.all
973
    @errors.cyclades.connection
974
    @errors.cyclades.network_id
975
    def _run(self, network_id, new_name):
976
        self._optional_output(
977
                self.client.update_network_name(int(network_id), new_name))
978

    
979
    def main(self, network_id, new_name):
980
        super(self.__class__, self)._run()
981
        self._run(network_id=network_id, new_name=new_name)
982

    
983

    
984
@command(network_cmds)
985
class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
986
    """Delete a network"""
987

    
988
    arguments = dict(
989
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
990
    )
991

    
992
    @errors.generic.all
993
    @errors.cyclades.connection
994
    @errors.cyclades.network_id
995
    @errors.cyclades.network_in_use
996
    def _run(self, network_id):
997
        status = 'DELETED'
998
        if self['wait']:
999
            r = self.client.get_network_details(network_id)
1000
            status = r['status']
1001
            if status in ('DELETED', ):
1002
                return
1003

    
1004
        r = self.client.delete_network(int(network_id))
1005
        self._optional_output(r)
1006

    
1007
        if self['wait']:
1008
            self._wait(network_id, status)
1009

    
1010
    def main(self, network_id):
1011
        super(self.__class__, self)._run()
1012
        self._run(network_id=network_id)
1013

    
1014

    
1015
@command(network_cmds)
1016
class network_connect(_init_cyclades, _optional_output_cmd):
1017
    """Connect a server to a network"""
1018

    
1019
    @errors.generic.all
1020
    @errors.cyclades.connection
1021
    @errors.cyclades.server_id
1022
    @errors.cyclades.network_id
1023
    def _run(self, server_id, network_id):
1024
        self._optional_output(
1025
                self.client.connect_server(int(server_id), int(network_id)))
1026

    
1027
    def main(self, server_id, network_id):
1028
        super(self.__class__, self)._run()
1029
        self._run(server_id=server_id, network_id=network_id)
1030

    
1031

    
1032
@command(network_cmds)
1033
class network_disconnect(_init_cyclades):
1034
    """Disconnect a nic that connects a server to a network
1035
    Nic ids are listed as "attachments" in detailed network information
1036
    To get detailed network information: /network info <network id>
1037
    """
1038

    
1039
    @errors.cyclades.nic_format
1040
    def _server_id_from_nic(self, nic_id):
1041
        return nic_id.split('-')[1]
1042

    
1043
    @errors.generic.all
1044
    @errors.cyclades.connection
1045
    @errors.cyclades.server_id
1046
    @errors.cyclades.nic_id
1047
    def _run(self, nic_id, server_id):
1048
        num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
1049
        if not num_of_disconnected:
1050
            raise ClientError(
1051
                'Network Interface %s not found on server %s' % (
1052
                    nic_id, server_id),
1053
                status=404)
1054
        print('Disconnected %s connections' % num_of_disconnected)
1055

    
1056
    def main(self, nic_id):
1057
        super(self.__class__, self)._run()
1058
        server_id = self._server_id_from_nic(nic_id=nic_id)
1059
        self._run(nic_id=nic_id, server_id=server_id)
1060

    
1061

    
1062
@command(network_cmds)
1063
class network_wait(_init_cyclades, _network_wait):
1064
    """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1065

    
1066
    @errors.generic.all
1067
    @errors.cyclades.connection
1068
    @errors.cyclades.network_id
1069
    def _run(self, network_id, current_status):
1070
        net = self.client.get_network_details(network_id)
1071
        if net['status'].lower() == current_status.lower():
1072
            self._wait(network_id, current_status)
1073
        else:
1074
            self.error(
1075
                'Network %s: Cannot wait for status %s, '
1076
                'status is already %s' % (
1077
                    network_id, current_status, net['status']))
1078

    
1079
    def main(self, network_id, current_status='PENDING'):
1080
        super(self.__class__, self)._run()
1081
        self._run(network_id=network_id, current_status=current_status)
1082

    
1083

    
1084
@command(server_cmds)
1085
class server_ip(_init_cyclades):
1086
    """Manage floating IPs for the servers"""
1087

    
1088

    
1089
@command(server_cmds)
1090
class server_ip_pools(_init_cyclades, _optional_json):
1091
    """List all floating pools of floating ips"""
1092

    
1093
    @errors.generic.all
1094
    @errors.cyclades.connection
1095
    def _run(self):
1096
        r = self.client.get_floating_ip_pools()
1097
        self._print(r if self['json_output'] else r['floating_ip_pools'])
1098

    
1099
    def main(self):
1100
        super(self.__class__, self)._run()
1101
        self._run()
1102

    
1103

    
1104
@command(server_cmds)
1105
class server_ip_list(_init_cyclades, _optional_json):
1106
    """List all floating ips"""
1107

    
1108
    @errors.generic.all
1109
    @errors.cyclades.connection
1110
    def _run(self):
1111
        r = self.client.get_floating_ips()
1112
        self._print(r if self['json_output'] else r['floating_ips'])
1113

    
1114
    def main(self):
1115
        super(self.__class__, self)._run()
1116
        self._run()
1117

    
1118

    
1119
@command(server_cmds)
1120
class server_ip_info(_init_cyclades, _optional_json):
1121
    """A floating IPs' details"""
1122

    
1123
    @errors.generic.all
1124
    @errors.cyclades.connection
1125
    def _run(self, ip):
1126
        self._print(self.client.get_floating_ip(ip), self.print_dict)
1127

    
1128
    def main(self, ip):
1129
        super(self.__class__, self)._run()
1130
        self._run(ip=ip)
1131

    
1132

    
1133
@command(server_cmds)
1134
class server_ip_create(_init_cyclades, _optional_json):
1135
    """Create a new floating IP"""
1136

    
1137
    arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
1138

    
1139
    @errors.generic.all
1140
    @errors.cyclades.connection
1141
    def _run(self, ip=None):
1142
        self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1143

    
1144
    def main(self, requested_address=None):
1145
        super(self.__class__, self)._run()
1146
        self._run(ip=requested_address)
1147

    
1148

    
1149
@command(server_cmds)
1150
class server_ip_delete(_init_cyclades, _optional_output_cmd):
1151
    """Delete a floating ip"""
1152

    
1153
    @errors.generic.all
1154
    @errors.cyclades.connection
1155
    def _run(self, ip):
1156
        self._optional_output(self.client.delete_floating_ip(ip))
1157

    
1158
    def main(self, ip):
1159
        super(self.__class__, self)._run()
1160
        self._run(ip=ip)
1161

    
1162

    
1163
@command(server_cmds)
1164
class server_ip_attach(_init_cyclades, _optional_output_cmd):
1165
    """Attach a floating ip to a server with server_id
1166
    """
1167

    
1168
    @errors.generic.all
1169
    @errors.cyclades.connection
1170
    @errors.cyclades.server_id
1171
    def _run(self, server_id, ip):
1172
        self._optional_output(self.client.attach_floating_ip(server_id, ip))
1173

    
1174
    def main(self, server_id, ip):
1175
        super(self.__class__, self)._run()
1176
        self._run(server_id=server_id, ip=ip)
1177

    
1178

    
1179
@command(server_cmds)
1180
class server_ip_detach(_init_cyclades, _optional_output_cmd):
1181
    """Detach floating IP from server
1182
    """
1183

    
1184
    @errors.generic.all
1185
    @errors.cyclades.connection
1186
    @errors.cyclades.server_id
1187
    def _run(self, server_id, ip):
1188
        self._optional_output(self.client.detach_floating_ip(server_id, ip))
1189

    
1190
    def main(self, server_id, ip):
1191
        super(self.__class__, self)._run()
1192
        self._run(server_id=server_id, ip=ip)