Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 16d7b9ff

History | View | Annotate | Download (39.5 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(self, service, service_id, status_method, currect_status):
79
        (progress_bar, wait_cb) = self._safe_progress_bar(
80
            '%s %s still in %s mode' % (service, service_id, currect_status))
81

    
82
        try:
83
            new_mode = status_method(
84
                service_id, currect_status, wait_cb=wait_cb)
85
        finally:
86
            self._safe_progress_bar_finish(progress_bar)
87
        if new_mode:
88
            self.error('%s %s is now in %s mode' % (
89
                service, service_id, new_mode))
90
        else:
91
            raiseCLIError(None, 'Time out')
92

    
93

    
94
class _server_wait(_service_wait):
95

    
96
    def _wait(self, server_id, currect_status):
97
        super(_server_wait, self)._wait(
98
            'Server', server_id, self.client.wait_server, currect_status)
99

    
100

    
101
class _network_wait(_service_wait):
102

    
103
    def _wait(self, net_id, currect_status):
104
        super(_network_wait, self)._wait(
105
            'Network', net_id, self.client.wait_network, currect_status)
106

    
107

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

    
132
    def main(self):
133
        self._run()
134

    
135

    
136
@command(server_cmds)
137
class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
138
    """List virtual servers accessible by user
139
    Use filtering arguments (e.g., --name-like) to manage long server lists
140
    """
141

    
142
    PERMANENTS = ('id', 'name')
143

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

    
168
    def _add_user_name(self, servers):
169
        uuids = self._uuids2usernames(list(set(
170
                [srv['user_id'] for srv in servers] +
171
                [srv['tenant_id'] for srv in servers])))
172
        for srv in servers:
173
            srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
174
            srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
175
        return servers
176

    
177
    def _apply_common_filters(self, servers):
178
        common_filters = dict()
179
        if self['status']:
180
            common_filters['status'] = self['status']
181
        if self['user_id'] or self['user_name']:
182
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
183
            common_filters['user_id'] = uuid
184
        return filter_dicts_by_dict(servers, common_filters)
185

    
186
    def _filter_by_image(self, servers):
187
        iid = self['image_id']
188
        return [srv for srv in servers if srv['image']['id'] == iid]
189

    
190
    def _filter_by_flavor(self, servers):
191
        fid = self['flavor_id']
192
        return [srv for srv in servers if (
193
            '%s' % srv['image']['id'] == '%s' % fid)]
194

    
195
    def _filter_by_metadata(self, servers):
196
        new_servers = []
197
        for srv in servers:
198
            if not 'metadata' in srv:
199
                continue
200
            meta = [dict(srv['metadata'])]
201
            if self['meta']:
202
                meta = filter_dicts_by_dict(meta, self['meta'])
203
            if meta and self['meta_like']:
204
                meta = filter_dicts_by_dict(
205
                    meta, self['meta_like'], exact_match=False)
206
            if meta:
207
                new_servers.append(srv)
208
        return new_servers
209

    
210
    @errors.generic.all
211
    @errors.cyclades.connection
212
    @errors.cyclades.date
213
    def _run(self):
214
        withimage = bool(self['image_id'])
215
        withflavor = bool(self['flavor_id'])
216
        withmeta = bool(self['meta'] or self['meta_like'])
217
        withcommons = bool(
218
            self['status'] or self['user_id'] or self['user_name'])
219
        detail = self['detail'] or (
220
            withimage or withflavor or withmeta or withcommons)
221
        servers = self.client.list_servers(detail, self['since'])
222

    
223
        servers = self._filter_by_name(servers)
224
        servers = self._filter_by_id(servers)
225
        servers = self._apply_common_filters(servers)
226
        if withimage:
227
            servers = self._filter_by_image(servers)
228
        if withflavor:
229
            servers = self._filter_by_flavor(servers)
230
        if withmeta:
231
            servers = self._filter_by_metadata(servers)
232

    
233
        if self['detail'] and not self['json_output']:
234
            servers = self._add_user_name(servers)
235
        elif not (self['detail'] or self['json_output']):
236
            remove_from_items(servers, 'links')
237
        if detail and not self['detail']:
238
            for srv in servers:
239
                for key in set(srv).difference(self.PERMANENTS):
240
                    srv.pop(key)
241
        kwargs = dict(with_enumeration=self['enum'])
242
        if self['more']:
243
            kwargs['out'] = StringIO()
244
            kwargs['title'] = ()
245
        if self['limit']:
246
            servers = servers[:self['limit']]
247
        self._print(servers, **kwargs)
248
        if self['more']:
249
            pager(kwargs['out'].getvalue())
250

    
251
    def main(self):
252
        super(self.__class__, self)._run()
253
        self._run()
254

    
255

    
256
@command(server_cmds)
257
class server_info(_init_cyclades, _optional_json):
258
    """Detailed information on a Virtual Machine
259
    Contains:
260
    - name, id, status, create/update dates
261
    - network interfaces
262
    - metadata (e.g., os, superuser) and diagnostics
263
    - hardware flavor and os image ids
264
    """
265

    
266
    @errors.generic.all
267
    @errors.cyclades.connection
268
    @errors.cyclades.server_id
269
    def _run(self, server_id):
270
        vm = self.client.get_server_details(server_id)
271
        uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
272
        vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
273
        vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
274
        self._print(vm, self.print_dict)
275

    
276
    def main(self, server_id):
277
        super(self.__class__, self)._run()
278
        self._run(server_id=server_id)
279

    
280

    
281
class PersonalityArgument(KeyValueArgument):
282
    @property
283
    def value(self):
284
        return self._value if hasattr(self, '_value') else []
285

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

    
313

    
314
@command(server_cmds)
315
class server_create(_init_cyclades, _optional_json, _server_wait):
316
    """Create a server (aka Virtual Machine)
317
    Parameters:
318
    - name: (single quoted text)
319
    - flavor id: Hardware flavor. Pick one from: /flavor list
320
    - image id: OS images. Pick one from: /image list
321
    """
322

    
323
    arguments = dict(
324
        personality=PersonalityArgument(
325
            (80 * ' ').join(howto_personality), ('-p', '--personality')),
326
        wait=FlagArgument('Wait server to build', ('-w', '--wait'))
327
    )
328

    
329
    @errors.generic.all
330
    @errors.cyclades.connection
331
    @errors.plankton.id
332
    @errors.cyclades.flavor_id
333
    def _run(self, name, flavor_id, image_id):
334
        r = self.client.create_server(
335
            name, int(flavor_id), image_id, personality=self['personality'])
336
        usernames = self._uuids2usernames([r['user_id'], r['tenant_id']])
337
        r['user_id'] += ' (%s)' % usernames[r['user_id']]
338
        r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
339
        self._print(r, self.print_dict)
340
        if self['wait']:
341
            self._wait(r['id'], r['status'])
342

    
343
    def main(self, name, flavor_id, image_id):
344
        super(self.__class__, self)._run()
345
        self._run(name=name, flavor_id=flavor_id, image_id=image_id)
346

    
347

    
348
@command(server_cmds)
349
class server_rename(_init_cyclades, _optional_output_cmd):
350
    """Set/update a virtual server name
351
    virtual server names are not unique, therefore multiple servers may share
352
    the same name
353
    """
354

    
355
    @errors.generic.all
356
    @errors.cyclades.connection
357
    @errors.cyclades.server_id
358
    def _run(self, server_id, new_name):
359
        self._optional_output(
360
            self.client.update_server_name(int(server_id), new_name))
361

    
362
    def main(self, server_id, new_name):
363
        super(self.__class__, self)._run()
364
        self._run(server_id=server_id, new_name=new_name)
365

    
366

    
367
@command(server_cmds)
368
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
369
    """Delete a virtual server"""
370

    
371
    arguments = dict(
372
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
373
    )
374

    
375
    @errors.generic.all
376
    @errors.cyclades.connection
377
    @errors.cyclades.server_id
378
    def _run(self, server_id):
379
            status = 'DELETED'
380
            if self['wait']:
381
                details = self.client.get_server_details(server_id)
382
                status = details['status']
383

    
384
            r = self.client.delete_server(int(server_id))
385
            self._optional_output(r)
386

    
387
            if self['wait']:
388
                self._wait(server_id, status)
389

    
390
    def main(self, server_id):
391
        super(self.__class__, self)._run()
392
        self._run(server_id=server_id)
393

    
394

    
395
@command(server_cmds)
396
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
397
    """Reboot a virtual server"""
398

    
399
    arguments = dict(
400
        hard=FlagArgument(
401
            'perform a hard reboot (deprecated)', ('-f', '--force')),
402
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
403
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
404
    )
405

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

    
426
        r = self.client.reboot_server(int(server_id), hard_reboot)
427
        self._optional_output(r)
428

    
429
        if self['wait']:
430
            self._wait(server_id, 'REBOOT')
431

    
432
    def main(self, server_id):
433
        super(self.__class__, self)._run()
434
        self._run(server_id=server_id)
435

    
436

    
437
@command(server_cmds)
438
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
439
    """Start an existing virtual server"""
440

    
441
    arguments = dict(
442
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
443
    )
444

    
445
    @errors.generic.all
446
    @errors.cyclades.connection
447
    @errors.cyclades.server_id
448
    def _run(self, server_id):
449
        status = 'ACTIVE'
450
        if self['wait']:
451
            details = self.client.get_server_details(server_id)
452
            status = details['status']
453
            if status in ('ACTIVE', ):
454
                return
455

    
456
        r = self.client.start_server(int(server_id))
457
        self._optional_output(r)
458

    
459
        if self['wait']:
460
            self._wait(server_id, status)
461

    
462
    def main(self, server_id):
463
        super(self.__class__, self)._run()
464
        self._run(server_id=server_id)
465

    
466

    
467
@command(server_cmds)
468
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
469
    """Shutdown an active virtual server"""
470

    
471
    arguments = dict(
472
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
473
    )
474

    
475
    @errors.generic.all
476
    @errors.cyclades.connection
477
    @errors.cyclades.server_id
478
    def _run(self, server_id):
479
        status = 'STOPPED'
480
        if self['wait']:
481
            details = self.client.get_server_details(server_id)
482
            status = details['status']
483
            if status in ('STOPPED', ):
484
                return
485

    
486
        r = self.client.shutdown_server(int(server_id))
487
        self._optional_output(r)
488

    
489
        if self['wait']:
490
            self._wait(server_id, status)
491

    
492
    def main(self, server_id):
493
        super(self.__class__, self)._run()
494
        self._run(server_id=server_id)
495

    
496

    
497
@command(server_cmds)
498
class server_console(_init_cyclades, _optional_json):
499
    """Get a VNC console to access an existing virtual server
500
    Console connection information provided (at least):
501
    - host: (url or address) a VNC host
502
    - port: (int) the gateway to enter virtual server on host
503
    - password: for VNC authorization
504
    """
505

    
506
    @errors.generic.all
507
    @errors.cyclades.connection
508
    @errors.cyclades.server_id
509
    def _run(self, server_id):
510
        self._print(
511
            self.client.get_server_console(int(server_id)), self.print_dict)
512

    
513
    def main(self, server_id):
514
        super(self.__class__, self)._run()
515
        self._run(server_id=server_id)
516

    
517

    
518
@command(server_cmds)
519
class server_resize(_init_cyclades, _optional_output_cmd):
520
    """Set a different flavor for an existing server
521
    To get server ids and flavor ids:
522
    /server list
523
    /flavor list
524
    """
525

    
526
    @errors.generic.all
527
    @errors.cyclades.connection
528
    @errors.cyclades.server_id
529
    @errors.cyclades.flavor_id
530
    def _run(self, server_id, flavor_id):
531
        self._optional_output(self.client.resize_server(server_id, flavor_id))
532

    
533
    def main(self, server_id, flavor_id):
534
        super(self.__class__, self)._run()
535
        self._run(server_id=server_id, flavor_id=flavor_id)
536

    
537

    
538
@command(server_cmds)
539
class server_firewall(_init_cyclades):
540
    """Manage virtual server firewall profiles for public networks"""
541

    
542

    
543
@command(server_cmds)
544
class server_firewall_set(_init_cyclades, _optional_output_cmd):
545
    """Set the firewall profile on virtual server public network
546
    Values for profile:
547
    - DISABLED: Shutdown firewall
548
    - ENABLED: Firewall in normal mode
549
    - PROTECTED: Firewall in secure mode
550
    """
551

    
552
    @errors.generic.all
553
    @errors.cyclades.connection
554
    @errors.cyclades.server_id
555
    @errors.cyclades.firewall
556
    def _run(self, server_id, profile):
557
        self._optional_output(self.client.set_firewall_profile(
558
            server_id=int(server_id), profile=('%s' % profile).upper()))
559

    
560
    def main(self, server_id, profile):
561
        super(self.__class__, self)._run()
562
        self._run(server_id=server_id, profile=profile)
563

    
564

    
565
@command(server_cmds)
566
class server_firewall_get(_init_cyclades):
567
    """Get the firewall profile for a virtual servers' public network"""
568

    
569
    @errors.generic.all
570
    @errors.cyclades.connection
571
    @errors.cyclades.server_id
572
    def _run(self, server_id):
573
        self.writeln(self.client.get_firewall_profile(server_id))
574

    
575
    def main(self, server_id):
576
        super(self.__class__, self)._run()
577
        self._run(server_id=server_id)
578

    
579

    
580
@command(server_cmds)
581
class server_addr(_init_cyclades, _optional_json):
582
    """List the addresses of all network interfaces on a virtual server"""
583

    
584
    arguments = dict(
585
        enum=FlagArgument('Enumerate results', '--enumerate')
586
    )
587

    
588
    @errors.generic.all
589
    @errors.cyclades.connection
590
    @errors.cyclades.server_id
591
    def _run(self, server_id):
592
        reply = self.client.list_server_nics(int(server_id))
593
        self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
594

    
595
    def main(self, server_id):
596
        super(self.__class__, self)._run()
597
        self._run(server_id=server_id)
598

    
599

    
600
@command(server_cmds)
601
class server_metadata(_init_cyclades):
602
    """Manage Server metadata (key:value pairs of server attributes)"""
603

    
604

    
605
@command(server_cmds)
606
class server_metadata_list(_init_cyclades, _optional_json):
607
    """Get server metadata"""
608

    
609
    @errors.generic.all
610
    @errors.cyclades.connection
611
    @errors.cyclades.server_id
612
    @errors.cyclades.metadata
613
    def _run(self, server_id, key=''):
614
        self._print(
615
            self.client.get_server_metadata(int(server_id), key),
616
            self.print_dict)
617

    
618
    def main(self, server_id, key=''):
619
        super(self.__class__, self)._run()
620
        self._run(server_id=server_id, key=key)
621

    
622

    
623
@command(server_cmds)
624
class server_metadata_set(_init_cyclades, _optional_json):
625
    """Set / update virtual server metadata
626
    Metadata should be given in key/value pairs in key=value format
627
    For example: /server metadata set <server id> key1=value1 key2=value2
628
    Old, unreferenced metadata will remain intact
629
    """
630

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

    
653
    def main(self, server_id, *key_equals_val):
654
        super(self.__class__, self)._run()
655
        self._run(server_id=server_id, keyvals=key_equals_val)
656

    
657

    
658
@command(server_cmds)
659
class server_metadata_delete(_init_cyclades, _optional_output_cmd):
660
    """Delete virtual server metadata"""
661

    
662
    @errors.generic.all
663
    @errors.cyclades.connection
664
    @errors.cyclades.server_id
665
    @errors.cyclades.metadata
666
    def _run(self, server_id, key):
667
        self._optional_output(
668
            self.client.delete_server_metadata(int(server_id), key))
669

    
670
    def main(self, server_id, key):
671
        super(self.__class__, self)._run()
672
        self._run(server_id=server_id, key=key)
673

    
674

    
675
@command(server_cmds)
676
class server_stats(_init_cyclades, _optional_json):
677
    """Get virtual server statistics"""
678

    
679
    @errors.generic.all
680
    @errors.cyclades.connection
681
    @errors.cyclades.server_id
682
    def _run(self, server_id):
683
        self._print(
684
            self.client.get_server_stats(int(server_id)), self.print_dict)
685

    
686
    def main(self, server_id):
687
        super(self.__class__, self)._run()
688
        self._run(server_id=server_id)
689

    
690

    
691
@command(server_cmds)
692
class server_wait(_init_cyclades, _server_wait):
693
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
694

    
695
    @errors.generic.all
696
    @errors.cyclades.connection
697
    @errors.cyclades.server_id
698
    def _run(self, server_id, currect_status):
699
        self._wait(server_id, currect_status)
700

    
701
    def main(self, server_id, currect_status='BUILD'):
702
        super(self.__class__, self)._run()
703
        self._run(server_id=server_id, currect_status=currect_status)
704

    
705

    
706
@command(flavor_cmds)
707
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
708
    """List available hardware flavors"""
709

    
710
    PERMANENTS = ('id', 'name')
711

    
712
    arguments = dict(
713
        detail=FlagArgument('show detailed output', ('-l', '--details')),
714
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
715
        more=FlagArgument(
716
            'output results in pages (-n to set items per page, default 10)',
717
            '--more'),
718
        enum=FlagArgument('Enumerate results', '--enumerate'),
719
        ram=ValueArgument('filter by ram', ('--ram')),
720
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
721
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
722
        disk_template=ValueArgument(
723
            'filter by disk_templace', ('--disk-template'))
724
    )
725

    
726
    def _apply_common_filters(self, flavors):
727
        common_filters = dict()
728
        if self['ram']:
729
            common_filters['ram'] = self['ram']
730
        if self['vcpus']:
731
            common_filters['vcpus'] = self['vcpus']
732
        if self['disk']:
733
            common_filters['disk'] = self['disk']
734
        if self['disk_template']:
735
            common_filters['SNF:disk_template'] = self['disk_template']
736
        return filter_dicts_by_dict(flavors, common_filters)
737

    
738
    @errors.generic.all
739
    @errors.cyclades.connection
740
    def _run(self):
741
        withcommons = self['ram'] or self['vcpus'] or (
742
            self['disk'] or self['disk_template'])
743
        detail = self['detail'] or withcommons
744
        flavors = self.client.list_flavors(detail)
745
        flavors = self._filter_by_name(flavors)
746
        flavors = self._filter_by_id(flavors)
747
        if withcommons:
748
            flavors = self._apply_common_filters(flavors)
749
        if not (self['detail'] or self['json_output']):
750
            remove_from_items(flavors, 'links')
751
        if detail and not self['detail']:
752
            for flv in flavors:
753
                for key in set(flv).difference(self.PERMANENTS):
754
                    flv.pop(key)
755
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
756
        self._print(
757
            flavors,
758
            with_redundancy=self['detail'], with_enumeration=self['enum'],
759
            **kwargs)
760
        if self['more']:
761
            pager(kwargs['out'].getvalue())
762

    
763
    def main(self):
764
        super(self.__class__, self)._run()
765
        self._run()
766

    
767

    
768
@command(flavor_cmds)
769
class flavor_info(_init_cyclades, _optional_json):
770
    """Detailed information on a hardware flavor
771
    To get a list of available flavors and flavor ids, try /flavor list
772
    """
773

    
774
    @errors.generic.all
775
    @errors.cyclades.connection
776
    @errors.cyclades.flavor_id
777
    def _run(self, flavor_id):
778
        self._print(
779
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
780

    
781
    def main(self, flavor_id):
782
        super(self.__class__, self)._run()
783
        self._run(flavor_id=flavor_id)
784

    
785

    
786
def _add_name(self, net):
787
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
788
        if user_id:
789
            uuids.append(user_id)
790
        if tenant_id:
791
            uuids.append(tenant_id)
792
        if uuids:
793
            usernames = self._uuids2usernames(uuids)
794
            if user_id:
795
                net['user_id'] += ' (%s)' % usernames[user_id]
796
            if tenant_id:
797
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]
798

    
799

    
800
@command(network_cmds)
801
class network_info(_init_cyclades, _optional_json):
802
    """Detailed information on a network
803
    To get a list of available networks and network ids, try /network list
804
    """
805

    
806
    @errors.generic.all
807
    @errors.cyclades.connection
808
    @errors.cyclades.network_id
809
    def _run(self, network_id):
810
        network = self.client.get_network_details(int(network_id))
811
        _add_name(self, network)
812
        self._print(network, self.print_dict, exclude=('id'))
813

    
814
    def main(self, network_id):
815
        super(self.__class__, self)._run()
816
        self._run(network_id=network_id)
817

    
818

    
819
@command(network_cmds)
820
class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
821
    """List networks"""
822

    
823
    PERMANENTS = ('id', 'name')
824

    
825
    arguments = dict(
826
        detail=FlagArgument('show detailed output', ('-l', '--details')),
827
        limit=IntArgument('limit # of listed networks', ('-n', '--number')),
828
        more=FlagArgument(
829
            'output results in pages (-n to set items per page, default 10)',
830
            '--more'),
831
        enum=FlagArgument('Enumerate results', '--enumerate'),
832
        status=ValueArgument('filter by status', ('--status')),
833
        public=FlagArgument('only public networks', ('--public')),
834
        private=FlagArgument('only private networks', ('--private')),
835
        dhcp=FlagArgument('show networks with dhcp', ('--with-dhcp')),
836
        no_dhcp=FlagArgument('show networks without dhcp', ('--without-dhcp')),
837
        user_id=ValueArgument('filter by user id', ('--user-id')),
838
        user_name=ValueArgument('filter by user name', ('--user-name')),
839
        gateway=ValueArgument('filter by gateway (IPv4)', ('--gateway')),
840
        gateway6=ValueArgument('filter by gateway (IPv6)', ('--gateway6')),
841
        cidr=ValueArgument('filter by cidr (IPv4)', ('--cidr')),
842
        cidr6=ValueArgument('filter by cidr (IPv6)', ('--cidr6')),
843
        type=ValueArgument('filter by type', ('--type')),
844
    )
845

    
846
    def _apply_common_filters(self, networks):
847
        common_filter = dict()
848
        if self['public']:
849
            if self['private']:
850
                return []
851
            common_filter['public'] = self['public']
852
        elif self['private']:
853
            common_filter['public'] = False
854
        if self['dhcp']:
855
            if self['no_dhcp']:
856
                return []
857
            common_filter['dhcp'] = True
858
        elif self['no_dhcp']:
859
            common_filter['dhcp'] = False
860
        if self['user_id'] or self['user_name']:
861
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
862
            common_filter['user_id'] = uuid
863
        for term in ('status', 'gateway', 'gateway6', 'cidr', 'cidr6', 'type'):
864
            if self[term]:
865
                common_filter[term] = self[term]
866
        return filter_dicts_by_dict(networks, common_filter)
867

    
868
    def _add_name(self, networks, key='user_id'):
869
        uuids = self._uuids2usernames(
870
            list(set([net[key] for net in networks])))
871
        for net in networks:
872
            v = net.get(key, None)
873
            if v:
874
                net[key] += ' (%s)' % uuids[v]
875
        return networks
876

    
877
    @errors.generic.all
878
    @errors.cyclades.connection
879
    def _run(self):
880
        withcommons = False
881
        for term in (
882
                'status', 'public', 'private', 'user_id', 'user_name', 'type',
883
                'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
884
            if self[term]:
885
                withcommons = True
886
                break
887
        detail = self['detail'] or withcommons
888
        networks = self.client.list_networks(detail)
889
        networks = self._filter_by_name(networks)
890
        networks = self._filter_by_id(networks)
891
        if withcommons:
892
            networks = self._apply_common_filters(networks)
893
        if not (self['detail'] or self['json_output']):
894
            remove_from_items(networks, 'links')
895
        if detail and not self['detail']:
896
            for net in networks:
897
                for key in set(net).difference(self.PERMANENTS):
898
                    net.pop(key)
899
        if self['detail'] and not self['json_output']:
900
            self._add_name(networks)
901
            self._add_name(networks, 'tenant_id')
902
        kwargs = dict(with_enumeration=self['enum'])
903
        if self['more']:
904
            kwargs['out'] = StringIO()
905
            kwargs['title'] = ()
906
        if self['limit']:
907
            networks = networks[:self['limit']]
908
        self._print(networks, **kwargs)
909
        if self['more']:
910
            pager(kwargs['out'].getvalue())
911

    
912
    def main(self):
913
        super(self.__class__, self)._run()
914
        self._run()
915

    
916

    
917
@command(network_cmds)
918
class network_create(_init_cyclades, _optional_json, _network_wait):
919
    """Create an (unconnected) network"""
920

    
921
    arguments = dict(
922
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
923
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
924
        dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
925
        type=ValueArgument(
926
            'Valid network types are '
927
            'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
928
            '--with-type',
929
            default='MAC_FILTERED'),
930
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
931
    )
932

    
933
    @errors.generic.all
934
    @errors.cyclades.connection
935
    @errors.cyclades.network_max
936
    def _run(self, name):
937
        r = self.client.create_network(
938
            name,
939
            cidr=self['cidr'],
940
            gateway=self['gateway'],
941
            dhcp=self['dhcp'],
942
            type=self['type'])
943
        _add_name(self, r)
944
        self._print(r, self.print_dict)
945
        if self['wait']:
946
            self._wait(r['id'], 'PENDING')
947

    
948
    def main(self, name):
949
        super(self.__class__, self)._run()
950
        self._run(name)
951

    
952

    
953
@command(network_cmds)
954
class network_rename(_init_cyclades, _optional_output_cmd):
955
    """Set the name of a network"""
956

    
957
    @errors.generic.all
958
    @errors.cyclades.connection
959
    @errors.cyclades.network_id
960
    def _run(self, network_id, new_name):
961
        self._optional_output(
962
                self.client.update_network_name(int(network_id), new_name))
963

    
964
    def main(self, network_id, new_name):
965
        super(self.__class__, self)._run()
966
        self._run(network_id=network_id, new_name=new_name)
967

    
968

    
969
@command(network_cmds)
970
class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
971
    """Delete a network"""
972

    
973
    arguments = dict(
974
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
975
    )
976

    
977
    @errors.generic.all
978
    @errors.cyclades.connection
979
    @errors.cyclades.network_id
980
    @errors.cyclades.network_in_use
981
    def _run(self, network_id):
982
        status = 'DELETED'
983
        if self['wait']:
984
            r = self.client.get_network_details(network_id)
985
            status = r['status']
986
            if status in ('DELETED', ):
987
                return
988

    
989
        r = self.client.delete_network(int(network_id))
990
        self._optional_output(r)
991

    
992
        if self['wait']:
993
            self._wait(network_id, status)
994

    
995
    def main(self, network_id):
996
        super(self.__class__, self)._run()
997
        self._run(network_id=network_id)
998

    
999

    
1000
@command(network_cmds)
1001
class network_connect(_init_cyclades, _optional_output_cmd):
1002
    """Connect a server to a network"""
1003

    
1004
    @errors.generic.all
1005
    @errors.cyclades.connection
1006
    @errors.cyclades.server_id
1007
    @errors.cyclades.network_id
1008
    def _run(self, server_id, network_id):
1009
        self._optional_output(
1010
                self.client.connect_server(int(server_id), int(network_id)))
1011

    
1012
    def main(self, server_id, network_id):
1013
        super(self.__class__, self)._run()
1014
        self._run(server_id=server_id, network_id=network_id)
1015

    
1016

    
1017
@command(network_cmds)
1018
class network_disconnect(_init_cyclades):
1019
    """Disconnect a nic that connects a server to a network
1020
    Nic ids are listed as "attachments" in detailed network information
1021
    To get detailed network information: /network info <network id>
1022
    """
1023

    
1024
    @errors.cyclades.nic_format
1025
    def _server_id_from_nic(self, nic_id):
1026
        return nic_id.split('-')[1]
1027

    
1028
    @errors.generic.all
1029
    @errors.cyclades.connection
1030
    @errors.cyclades.server_id
1031
    @errors.cyclades.nic_id
1032
    def _run(self, nic_id, server_id):
1033
        num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
1034
        if not num_of_disconnected:
1035
            raise ClientError(
1036
                'Network Interface %s not found on server %s' % (
1037
                    nic_id, server_id),
1038
                status=404)
1039
        print('Disconnected %s connections' % num_of_disconnected)
1040

    
1041
    def main(self, nic_id):
1042
        super(self.__class__, self)._run()
1043
        server_id = self._server_id_from_nic(nic_id=nic_id)
1044
        self._run(nic_id=nic_id, server_id=server_id)
1045

    
1046

    
1047
@command(network_cmds)
1048
class network_wait(_init_cyclades, _network_wait):
1049
    """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1050

    
1051
    @errors.generic.all
1052
    @errors.cyclades.connection
1053
    @errors.cyclades.network_id
1054
    def _run(self, network_id, currect_status):
1055
        self._wait(network_id, currect_status)
1056

    
1057
    def main(self, network_id, currect_status='PENDING'):
1058
        super(self.__class__, self)._run()
1059
        self._run(network_id=network_id, currect_status=currect_status)
1060

    
1061

    
1062
@command(server_cmds)
1063
class server_ip(_init_cyclades):
1064
    """Manage floating IPs for the servers"""
1065

    
1066

    
1067
@command(server_cmds)
1068
class server_ip_pools(_init_cyclades, _optional_json):
1069
    """List all floating pools of floating ips"""
1070

    
1071
    @errors.generic.all
1072
    @errors.cyclades.connection
1073
    def _run(self):
1074
        r = self.client.get_floating_ip_pools()
1075
        self._print(r if self['json_output'] else r['floating_ip_pools'])
1076

    
1077
    def main(self):
1078
        super(self.__class__, self)._run()
1079
        self._run()
1080

    
1081

    
1082
@command(server_cmds)
1083
class server_ip_list(_init_cyclades, _optional_json):
1084
    """List all floating ips"""
1085

    
1086
    @errors.generic.all
1087
    @errors.cyclades.connection
1088
    def _run(self):
1089
        r = self.client.get_floating_ips()
1090
        self._print(r if self['json_output'] else r['floating_ips'])
1091

    
1092
    def main(self):
1093
        super(self.__class__, self)._run()
1094
        self._run()
1095

    
1096

    
1097
@command(server_cmds)
1098
class server_ip_info(_init_cyclades, _optional_json):
1099
    """A floating IPs' details"""
1100

    
1101
    @errors.generic.all
1102
    @errors.cyclades.connection
1103
    def _run(self, ip):
1104
        self._print(self.client.get_floating_ip(ip), self.print_dict)
1105

    
1106
    def main(self, ip):
1107
        super(self.__class__, self)._run()
1108
        self._run(ip=ip)
1109

    
1110

    
1111
@command(server_cmds)
1112
class server_ip_create(_init_cyclades, _optional_json):
1113
    """Create a new floating IP"""
1114

    
1115
    arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
1116

    
1117
    @errors.generic.all
1118
    @errors.cyclades.connection
1119
    def _run(self, ip=None):
1120
        self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1121

    
1122
    def main(self, requested_address=None):
1123
        super(self.__class__, self)._run()
1124
        self._run(ip=requested_address)
1125

    
1126

    
1127
@command(server_cmds)
1128
class server_ip_delete(_init_cyclades, _optional_output_cmd):
1129
    """Delete a floating ip"""
1130

    
1131
    @errors.generic.all
1132
    @errors.cyclades.connection
1133
    def _run(self, ip):
1134
        self._optional_output(self.client.delete_floating_ip(ip))
1135

    
1136
    def main(self, ip):
1137
        super(self.__class__, self)._run()
1138
        self._run(ip=ip)
1139

    
1140

    
1141
@command(server_cmds)
1142
class server_ip_attach(_init_cyclades, _optional_output_cmd):
1143
    """Attach a floating ip to a server with server_id
1144
    """
1145

    
1146
    @errors.generic.all
1147
    @errors.cyclades.connection
1148
    @errors.cyclades.server_id
1149
    def _run(self, server_id, ip):
1150
        self._optional_output(self.client.attach_floating_ip(server_id, ip))
1151

    
1152
    def main(self, server_id, ip):
1153
        super(self.__class__, self)._run()
1154
        self._run(server_id=server_id, ip=ip)
1155

    
1156

    
1157
@command(server_cmds)
1158
class server_ip_detach(_init_cyclades, _optional_output_cmd):
1159
    """Detach floating IP from server
1160
    """
1161

    
1162
    @errors.generic.all
1163
    @errors.cyclades.connection
1164
    @errors.cyclades.server_id
1165
    def _run(self, server_id, ip):
1166
        self._optional_output(self.client.detach_floating_ip(server_id, ip))
1167

    
1168
    def main(self, server_id, ip):
1169
        super(self.__class__, self)._run()
1170
        self._run(server_id=server_id, ip=ip)