Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 3185cd6d

History | View | Annotate | Download (38.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 kamaki.cli import command
35
from kamaki.cli.command_tree import CommandTree
36
from kamaki.cli.utils import (
37
    print_dict, remove_from_items, filter_dicts_by_dict, pager)
38
from kamaki.cli.errors import raiseCLIError, CLISyntaxError, CLIBaseUrlError
39
from kamaki.clients.cyclades import CycladesClient, ClientError
40
from kamaki.cli.argument import FlagArgument, ValueArgument, KeyValueArgument
41
from kamaki.cli.argument import ProgressBarArgument, DateArgument, IntArgument
42
from kamaki.cli.commands import _command_init, errors, addLogSettings
43
from kamaki.cli.commands import (
44
    _optional_output_cmd, _optional_json, _name_filter, _id_filter)
45

    
46
from base64 import b64encode
47
from os.path import exists
48
from io import StringIO
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 VMs 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: VMs user id of the remote destination file',
67
    '  GROUP: VMs 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 Machines accessible by user"""
139

    
140
    PERMANENTS = ('id', 'name')
141

    
142
    __doc__ += about_authentication
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('limit number of listed VMs', ('-n', '--number')),
150
        more=FlagArgument(
151
            'output results in pages (-n to set items per page, default 10)',
152
            '--more'),
153
        enum=FlagArgument('Enumerate results', '--enumerate'),
154
        flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
155
        image_id=ValueArgument('filter by image id', ('--image-id')),
156
        user_id=ValueArgument('filter by user id', ('--user-id')),
157
        user_name=ValueArgument('filter by user name', ('--user-name')),
158
        status=ValueArgument(
159
            'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
160
            ('--status')),
161
        meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
162
        meta_like=KeyValueArgument(
163
            'print only if in key=value, the value is part of actual value',
164
            ('--metadata-like')),
165
    )
166

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

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

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

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

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

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

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

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

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

    
254

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

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

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

    
279

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

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

    
312

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

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

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

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

    
346

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

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

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

    
364

    
365
@command(server_cmds)
366
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
367
    """Delete a server (VM)"""
368

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

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

    
382
            r = self.client.delete_server(int(server_id))
383
            self._optional_output(r)
384

    
385
            if self['wait']:
386
                self._wait(server_id, status)
387

    
388
    def main(self, server_id):
389
        super(self.__class__, self)._run()
390
        self._run(server_id=server_id)
391

    
392

    
393
@command(server_cmds)
394
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
395
    """Reboot a server (VM)"""
396

    
397
    arguments = dict(
398
        hard=FlagArgument('perform a hard reboot', ('-f', '--force')),
399
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
400
    )
401

    
402
    @errors.generic.all
403
    @errors.cyclades.connection
404
    @errors.cyclades.server_id
405
    def _run(self, server_id):
406
        r = self.client.reboot_server(int(server_id), self['hard'])
407
        self._optional_output(r)
408

    
409
        if self['wait']:
410
            self._wait(server_id, 'REBOOT')
411

    
412
    def main(self, server_id):
413
        super(self.__class__, self)._run()
414
        self._run(server_id=server_id)
415

    
416

    
417
@command(server_cmds)
418
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
419
    """Start an existing server (VM)"""
420

    
421
    arguments = dict(
422
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
423
    )
424

    
425
    @errors.generic.all
426
    @errors.cyclades.connection
427
    @errors.cyclades.server_id
428
    def _run(self, server_id):
429
        status = 'ACTIVE'
430
        if self['wait']:
431
            details = self.client.get_server_details(server_id)
432
            status = details['status']
433
            if status in ('ACTIVE', ):
434
                return
435

    
436
        r = self.client.start_server(int(server_id))
437
        self._optional_output(r)
438

    
439
        if self['wait']:
440
            self._wait(server_id, status)
441

    
442
    def main(self, server_id):
443
        super(self.__class__, self)._run()
444
        self._run(server_id=server_id)
445

    
446

    
447
@command(server_cmds)
448
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
449
    """Shutdown an active server (VM)"""
450

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

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

    
466
        r = self.client.shutdown_server(int(server_id))
467
        self._optional_output(r)
468

    
469
        if self['wait']:
470
            self._wait(server_id, status)
471

    
472
    def main(self, server_id):
473
        super(self.__class__, self)._run()
474
        self._run(server_id=server_id)
475

    
476

    
477
@command(server_cmds)
478
class server_console(_init_cyclades, _optional_json):
479
    """Get a VNC console to access an existing server (VM)
480
    Console connection information provided (at least):
481
    - host: (url or address) a VNC host
482
    - port: (int) the gateway to enter VM on host
483
    - password: for VNC authorization
484
    """
485

    
486
    @errors.generic.all
487
    @errors.cyclades.connection
488
    @errors.cyclades.server_id
489
    def _run(self, server_id):
490
        self._print(
491
            self.client.get_server_console(int(server_id)), print_dict)
492

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

    
497

    
498
@command(server_cmds)
499
class server_resize(_init_cyclades, _optional_output_cmd):
500
    """Set a different flavor for an existing server
501
    To get server ids and flavor ids:
502
    /server list
503
    /flavor list
504
    """
505

    
506
    @errors.generic.all
507
    @errors.cyclades.connection
508
    @errors.cyclades.server_id
509
    @errors.cyclades.flavor_id
510
    def _run(self, server_id, flavor_id):
511
        self._optional_output(self.client.resize_server(server_id, flavor_id))
512

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

    
517

    
518
@command(server_cmds)
519
class server_firewall(_init_cyclades):
520
    """Manage server (VM) firewall profiles for public networks"""
521

    
522

    
523
@command(server_cmds)
524
class server_firewall_set(_init_cyclades, _optional_output_cmd):
525
    """Set the server (VM) firewall profile on VMs public network
526
    Values for profile:
527
    - DISABLED: Shutdown firewall
528
    - ENABLED: Firewall in normal mode
529
    - PROTECTED: Firewall in secure mode
530
    """
531

    
532
    @errors.generic.all
533
    @errors.cyclades.connection
534
    @errors.cyclades.server_id
535
    @errors.cyclades.firewall
536
    def _run(self, server_id, profile):
537
        self._optional_output(self.client.set_firewall_profile(
538
            server_id=int(server_id), profile=('%s' % profile).upper()))
539

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

    
544

    
545
@command(server_cmds)
546
class server_firewall_get(_init_cyclades):
547
    """Get the server (VM) firewall profile for its public network"""
548

    
549
    @errors.generic.all
550
    @errors.cyclades.connection
551
    @errors.cyclades.server_id
552
    def _run(self, server_id):
553
        self.writeln(self.client.get_firewall_profile(server_id))
554

    
555
    def main(self, server_id):
556
        super(self.__class__, self)._run()
557
        self._run(server_id=server_id)
558

    
559

    
560
@command(server_cmds)
561
class server_addr(_init_cyclades, _optional_json):
562
    """List the addresses of all network interfaces on a server (VM)"""
563

    
564
    arguments = dict(
565
        enum=FlagArgument('Enumerate results', '--enumerate')
566
    )
567

    
568
    @errors.generic.all
569
    @errors.cyclades.connection
570
    @errors.cyclades.server_id
571
    def _run(self, server_id):
572
        reply = self.client.list_server_nics(int(server_id))
573
        self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
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_metadata(_init_cyclades):
582
    """Manage Server metadata (key:value pairs of server attributes)"""
583

    
584

    
585
@command(server_cmds)
586
class server_metadata_list(_init_cyclades, _optional_json):
587
    """Get server metadata"""
588

    
589
    @errors.generic.all
590
    @errors.cyclades.connection
591
    @errors.cyclades.server_id
592
    @errors.cyclades.metadata
593
    def _run(self, server_id, key=''):
594
        self._print(
595
            self.client.get_server_metadata(int(server_id), key), print_dict)
596

    
597
    def main(self, server_id, key=''):
598
        super(self.__class__, self)._run()
599
        self._run(server_id=server_id, key=key)
600

    
601

    
602
@command(server_cmds)
603
class server_metadata_set(_init_cyclades, _optional_json):
604
    """Set / update server(VM) metadata
605
    Metadata should be given in key/value pairs in key=value format
606
    For example: /server metadata set <server id> key1=value1 key2=value2
607
    Old, unreferenced metadata will remain intact
608
    """
609

    
610
    @errors.generic.all
611
    @errors.cyclades.connection
612
    @errors.cyclades.server_id
613
    def _run(self, server_id, keyvals):
614
        assert keyvals, 'Please, add some metadata ( key=value)'
615
        metadata = dict()
616
        for keyval in keyvals:
617
            k, sep, v = keyval.partition('=')
618
            if sep and k:
619
                metadata[k] = v
620
            else:
621
                raiseCLIError(
622
                    'Invalid piece of metadata %s' % keyval,
623
                    importance=2, details=[
624
                        'Correct metadata format: key=val',
625
                        'For example:',
626
                        '/server metadata set <server id>'
627
                        'key1=value1 key2=value2'])
628
        self._print(
629
            self.client.update_server_metadata(int(server_id), **metadata),
630
            print_dict)
631

    
632
    def main(self, server_id, *key_equals_val):
633
        super(self.__class__, self)._run()
634
        self._run(server_id=server_id, keyvals=key_equals_val)
635

    
636

    
637
@command(server_cmds)
638
class server_metadata_delete(_init_cyclades, _optional_output_cmd):
639
    """Delete server (VM) metadata"""
640

    
641
    @errors.generic.all
642
    @errors.cyclades.connection
643
    @errors.cyclades.server_id
644
    @errors.cyclades.metadata
645
    def _run(self, server_id, key):
646
        self._optional_output(
647
            self.client.delete_server_metadata(int(server_id), key))
648

    
649
    def main(self, server_id, key):
650
        super(self.__class__, self)._run()
651
        self._run(server_id=server_id, key=key)
652

    
653

    
654
@command(server_cmds)
655
class server_stats(_init_cyclades, _optional_json):
656
    """Get server (VM) statistics"""
657

    
658
    @errors.generic.all
659
    @errors.cyclades.connection
660
    @errors.cyclades.server_id
661
    def _run(self, server_id):
662
        self._print(self.client.get_server_stats(int(server_id)), print_dict)
663

    
664
    def main(self, server_id):
665
        super(self.__class__, self)._run()
666
        self._run(server_id=server_id)
667

    
668

    
669
@command(server_cmds)
670
class server_wait(_init_cyclades, _server_wait):
671
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
672

    
673
    @errors.generic.all
674
    @errors.cyclades.connection
675
    @errors.cyclades.server_id
676
    def _run(self, server_id, currect_status):
677
        self._wait(server_id, currect_status)
678

    
679
    def main(self, server_id, currect_status='BUILD'):
680
        super(self.__class__, self)._run()
681
        self._run(server_id=server_id, currect_status=currect_status)
682

    
683

    
684
@command(flavor_cmds)
685
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
686
    """List available hardware flavors"""
687

    
688
    PERMANENTS = ('id', 'name')
689

    
690
    arguments = dict(
691
        detail=FlagArgument('show detailed output', ('-l', '--details')),
692
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
693
        more=FlagArgument(
694
            'output results in pages (-n to set items per page, default 10)',
695
            '--more'),
696
        enum=FlagArgument('Enumerate results', '--enumerate'),
697
        ram=ValueArgument('filter by ram', ('--ram')),
698
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
699
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
700
        disk_template=ValueArgument(
701
            'filter by disk_templace', ('--disk-template'))
702
    )
703

    
704
    def _apply_common_filters(self, flavors):
705
        common_filters = dict()
706
        if self['ram']:
707
            common_filters['ram'] = self['ram']
708
        if self['vcpus']:
709
            common_filters['vcpus'] = self['vcpus']
710
        if self['disk']:
711
            common_filters['disk'] = self['disk']
712
        if self['disk_template']:
713
            common_filters['SNF:disk_template'] = self['disk_template']
714
        return filter_dicts_by_dict(flavors, common_filters)
715

    
716
    @errors.generic.all
717
    @errors.cyclades.connection
718
    def _run(self):
719
        withcommons = self['ram'] or self['vcpus'] or (
720
            self['disk'] or self['disk_template'])
721
        detail = self['detail'] or withcommons
722
        flavors = self.client.list_flavors(detail)
723
        flavors = self._filter_by_name(flavors)
724
        flavors = self._filter_by_id(flavors)
725
        if withcommons:
726
            flavors = self._apply_common_filters(flavors)
727
        if not (self['detail'] or self['json_output']):
728
            remove_from_items(flavors, 'links')
729
        if detail and not self['detail']:
730
            for flv in flavors:
731
                for key in set(flv).difference(self.PERMANENTS):
732
                    flv.pop(key)
733
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
734
        self._print(
735
            flavors,
736
            with_redundancy=self['detail'], with_enumeration=self['enum'],
737
            **kwargs)
738
        if self['more']:
739
            pager(kwargs['out'].getvalue())
740

    
741
    def main(self):
742
        super(self.__class__, self)._run()
743
        self._run()
744

    
745

    
746
@command(flavor_cmds)
747
class flavor_info(_init_cyclades, _optional_json):
748
    """Detailed information on a hardware flavor
749
    To get a list of available flavors and flavor ids, try /flavor list
750
    """
751

    
752
    @errors.generic.all
753
    @errors.cyclades.connection
754
    @errors.cyclades.flavor_id
755
    def _run(self, flavor_id):
756
        self._print(
757
            self.client.get_flavor_details(int(flavor_id)), print_dict)
758

    
759
    def main(self, flavor_id):
760
        super(self.__class__, self)._run()
761
        self._run(flavor_id=flavor_id)
762

    
763

    
764
def _add_name(self, net):
765
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
766
        if user_id:
767
            uuids.append(user_id)
768
        if tenant_id:
769
            uuids.append(tenant_id)
770
        if uuids:
771
            usernames = self._uuids2usernames(uuids)
772
            if user_id:
773
                net['user_id'] += ' (%s)' % usernames[user_id]
774
            if tenant_id:
775
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]
776

    
777

    
778
@command(network_cmds)
779
class network_info(_init_cyclades, _optional_json):
780
    """Detailed information on a network
781
    To get a list of available networks and network ids, try /network list
782
    """
783

    
784
    @errors.generic.all
785
    @errors.cyclades.connection
786
    @errors.cyclades.network_id
787
    def _run(self, network_id):
788
        network = self.client.get_network_details(int(network_id))
789
        _add_name(self, network)
790
        self._print(network, print_dict, exclude=('id'))
791

    
792
    def main(self, network_id):
793
        super(self.__class__, self)._run()
794
        self._run(network_id=network_id)
795

    
796

    
797
@command(network_cmds)
798
class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
799
    """List networks"""
800

    
801
    PERMANENTS = ('id', 'name')
802

    
803
    arguments = dict(
804
        detail=FlagArgument('show detailed output', ('-l', '--details')),
805
        limit=IntArgument('limit # of listed networks', ('-n', '--number')),
806
        more=FlagArgument(
807
            'output results in pages (-n to set items per page, default 10)',
808
            '--more'),
809
        enum=FlagArgument('Enumerate results', '--enumerate'),
810
        status=ValueArgument('filter by status', ('--status')),
811
        public=FlagArgument('only public networks', ('--public')),
812
        private=FlagArgument('only private networks', ('--private')),
813
        dhcp=FlagArgument('show networks with dhcp', ('--with-dhcp')),
814
        no_dhcp=FlagArgument('show networks without dhcp', ('--without-dhcp')),
815
        user_id=ValueArgument('filter by user id', ('--user-id')),
816
        user_name=ValueArgument('filter by user name', ('--user-name')),
817
        gateway=ValueArgument('filter by gateway (IPv4)', ('--gateway')),
818
        gateway6=ValueArgument('filter by gateway (IPv6)', ('--gateway6')),
819
        cidr=ValueArgument('filter by cidr (IPv4)', ('--cidr')),
820
        cidr6=ValueArgument('filter by cidr (IPv6)', ('--cidr6')),
821
        type=ValueArgument('filter by type', ('--type')),
822
    )
823

    
824
    def _apply_common_filters(self, networks):
825
        common_filter = dict()
826
        if self['public']:
827
            if self['private']:
828
                return []
829
            common_filter['public'] = self['public']
830
        elif self['private']:
831
            common_filter['public'] = False
832
        if self['dhcp']:
833
            if self['no_dhcp']:
834
                return []
835
            common_filter['dhcp'] = True
836
        elif self['no_dhcp']:
837
            common_filter['dhcp'] = False
838
        if self['user_id'] or self['user_name']:
839
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
840
            common_filter['user_id'] = uuid
841
        for term in ('status', 'gateway', 'gateway6', 'cidr', 'cidr6', 'type'):
842
            if self[term]:
843
                common_filter[term] = self[term]
844
        return filter_dicts_by_dict(networks, common_filter)
845

    
846
    def _add_name(self, networks, key='user_id'):
847
        uuids = self._uuids2usernames(
848
            list(set([net[key] for net in networks])))
849
        for net in networks:
850
            v = net.get(key, None)
851
            if v:
852
                net[key] += ' (%s)' % uuids[v]
853
        return networks
854

    
855
    @errors.generic.all
856
    @errors.cyclades.connection
857
    def _run(self):
858
        withcommons = False
859
        for term in (
860
                'status', 'public', 'private', 'user_id', 'user_name', 'type',
861
                'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
862
            if self[term]:
863
                withcommons = True
864
                break
865
        detail = self['detail'] or withcommons
866
        networks = self.client.list_networks(detail)
867
        networks = self._filter_by_name(networks)
868
        networks = self._filter_by_id(networks)
869
        if withcommons:
870
            networks = self._apply_common_filters(networks)
871
        if not (self['detail'] or self['json_output']):
872
            remove_from_items(networks, 'links')
873
        if detail and not self['detail']:
874
            for net in networks:
875
                for key in set(net).difference(self.PERMANENTS):
876
                    net.pop(key)
877
        if self['detail'] and not self['json_output']:
878
            self._add_name(networks)
879
            self._add_name(networks, 'tenant_id')
880
        kwargs = dict(with_enumeration=self['enum'])
881
        if self['more']:
882
            kwargs['out'] = StringIO()
883
            kwargs['title'] = ()
884
        if self['limit']:
885
            networks = networks[:self['limit']]
886
        self._print(networks, **kwargs)
887
        if self['more']:
888
            pager(kwargs['out'].getvalue())
889

    
890
    def main(self):
891
        super(self.__class__, self)._run()
892
        self._run()
893

    
894

    
895
@command(network_cmds)
896
class network_create(_init_cyclades, _optional_json, _network_wait):
897
    """Create an (unconnected) network"""
898

    
899
    arguments = dict(
900
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
901
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
902
        dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
903
        type=ValueArgument(
904
            'Valid network types are '
905
            'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
906
            '--with-type',
907
            default='MAC_FILTERED'),
908
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
909
    )
910

    
911
    @errors.generic.all
912
    @errors.cyclades.connection
913
    @errors.cyclades.network_max
914
    def _run(self, name):
915
        r = self.client.create_network(
916
            name,
917
            cidr=self['cidr'],
918
            gateway=self['gateway'],
919
            dhcp=self['dhcp'],
920
            type=self['type'])
921
        _add_name(self, r)
922
        self._print(r, print_dict)
923
        if self['wait']:
924
            self._wait(r['id'], 'PENDING')
925

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

    
930

    
931
@command(network_cmds)
932
class network_rename(_init_cyclades, _optional_output_cmd):
933
    """Set the name of a network"""
934

    
935
    @errors.generic.all
936
    @errors.cyclades.connection
937
    @errors.cyclades.network_id
938
    def _run(self, network_id, new_name):
939
        self._optional_output(
940
                self.client.update_network_name(int(network_id), new_name))
941

    
942
    def main(self, network_id, new_name):
943
        super(self.__class__, self)._run()
944
        self._run(network_id=network_id, new_name=new_name)
945

    
946

    
947
@command(network_cmds)
948
class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
949
    """Delete a network"""
950

    
951
    arguments = dict(
952
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
953
    )
954

    
955
    @errors.generic.all
956
    @errors.cyclades.connection
957
    @errors.cyclades.network_id
958
    @errors.cyclades.network_in_use
959
    def _run(self, network_id):
960
        status = 'DELETED'
961
        if self['wait']:
962
            r = self.client.get_network_details(network_id)
963
            status = r['status']
964
            if status in ('DELETED', ):
965
                return
966

    
967
        r = self.client.delete_network(int(network_id))
968
        self._optional_output(r)
969

    
970
        if self['wait']:
971
            self._wait(network_id, status)
972

    
973
    def main(self, network_id):
974
        super(self.__class__, self)._run()
975
        self._run(network_id=network_id)
976

    
977

    
978
@command(network_cmds)
979
class network_connect(_init_cyclades, _optional_output_cmd):
980
    """Connect a server to a network"""
981

    
982
    @errors.generic.all
983
    @errors.cyclades.connection
984
    @errors.cyclades.server_id
985
    @errors.cyclades.network_id
986
    def _run(self, server_id, network_id):
987
        self._optional_output(
988
                self.client.connect_server(int(server_id), int(network_id)))
989

    
990
    def main(self, server_id, network_id):
991
        super(self.__class__, self)._run()
992
        self._run(server_id=server_id, network_id=network_id)
993

    
994

    
995
@command(network_cmds)
996
class network_disconnect(_init_cyclades):
997
    """Disconnect a nic that connects a server to a network
998
    Nic ids are listed as "attachments" in detailed network information
999
    To get detailed network information: /network info <network id>
1000
    """
1001

    
1002
    @errors.cyclades.nic_format
1003
    def _server_id_from_nic(self, nic_id):
1004
        return nic_id.split('-')[1]
1005

    
1006
    @errors.generic.all
1007
    @errors.cyclades.connection
1008
    @errors.cyclades.server_id
1009
    @errors.cyclades.nic_id
1010
    def _run(self, nic_id, server_id):
1011
        num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
1012
        if not num_of_disconnected:
1013
            raise ClientError(
1014
                'Network Interface %s not found on server %s' % (
1015
                    nic_id, server_id),
1016
                status=404)
1017
        print('Disconnected %s connections' % num_of_disconnected)
1018

    
1019
    def main(self, nic_id):
1020
        super(self.__class__, self)._run()
1021
        server_id = self._server_id_from_nic(nic_id=nic_id)
1022
        self._run(nic_id=nic_id, server_id=server_id)
1023

    
1024

    
1025
@command(network_cmds)
1026
class network_wait(_init_cyclades, _network_wait):
1027
    """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1028

    
1029
    @errors.generic.all
1030
    @errors.cyclades.connection
1031
    @errors.cyclades.network_id
1032
    def _run(self, network_id, currect_status):
1033
        self._wait(network_id, currect_status)
1034

    
1035
    def main(self, network_id, currect_status='PENDING'):
1036
        super(self.__class__, self)._run()
1037
        self._run(network_id=network_id, currect_status=currect_status)
1038

    
1039

    
1040
@command(server_cmds)
1041
class server_ip(_init_cyclades):
1042
    """Manage floating IPs for the servers"""
1043

    
1044

    
1045
@command(server_cmds)
1046
class server_ip_pools(_init_cyclades, _optional_json):
1047
    """List all floating pools of floating ips"""
1048

    
1049
    @errors.generic.all
1050
    @errors.cyclades.connection
1051
    def _run(self):
1052
        r = self.client.get_floating_ip_pools()
1053
        self._print(r if self['json_output'] else r['floating_ip_pools'])
1054

    
1055
    def main(self):
1056
        super(self.__class__, self)._run()
1057
        self._run()
1058

    
1059

    
1060
@command(server_cmds)
1061
class server_ip_list(_init_cyclades, _optional_json):
1062
    """List all floating ips"""
1063

    
1064
    @errors.generic.all
1065
    @errors.cyclades.connection
1066
    def _run(self):
1067
        r = self.client.get_floating_ips()
1068
        self._print(r if self['json_output'] else r['floating_ips'])
1069

    
1070
    def main(self):
1071
        super(self.__class__, self)._run()
1072
        self._run()
1073

    
1074

    
1075
@command(server_cmds)
1076
class server_ip_info(_init_cyclades, _optional_json):
1077
    """A floating IPs' details"""
1078

    
1079
    @errors.generic.all
1080
    @errors.cyclades.connection
1081
    def _run(self, ip):
1082
        self._print(self.client.get_floating_ip(ip), print_dict)
1083

    
1084
    def main(self, ip):
1085
        super(self.__class__, self)._run()
1086
        self._run(ip=ip)
1087

    
1088

    
1089
@command(server_cmds)
1090
class server_ip_create(_init_cyclades, _optional_json):
1091
    """Create a new floating IP"""
1092

    
1093
    arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
1094

    
1095
    @errors.generic.all
1096
    @errors.cyclades.connection
1097
    def _run(self, ip=None):
1098
        self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1099

    
1100
    def main(self, requested_address=None):
1101
        super(self.__class__, self)._run()
1102
        self._run(ip=requested_address)
1103

    
1104

    
1105
@command(server_cmds)
1106
class server_ip_delete(_init_cyclades, _optional_output_cmd):
1107
    """Delete a floating ip"""
1108

    
1109
    @errors.generic.all
1110
    @errors.cyclades.connection
1111
    def _run(self, ip):
1112
        self._optional_output(self.client.delete_floating_ip(ip))
1113

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

    
1118

    
1119
@command(server_cmds)
1120
class server_ip_attach(_init_cyclades, _optional_output_cmd):
1121
    """Attach a floating ip to a server with server_id
1122
    """
1123

    
1124
    @errors.generic.all
1125
    @errors.cyclades.connection
1126
    @errors.cyclades.server_id
1127
    def _run(self, server_id, ip):
1128
        self._optional_output(self.client.attach_floating_ip(server_id, ip))
1129

    
1130
    def main(self, server_id, ip):
1131
        super(self.__class__, self)._run()
1132
        self._run(server_id=server_id, ip=ip)
1133

    
1134

    
1135
@command(server_cmds)
1136
class server_ip_detach(_init_cyclades, _optional_output_cmd):
1137
    """Detach floating IP from server
1138
    """
1139

    
1140
    @errors.generic.all
1141
    @errors.cyclades.connection
1142
    @errors.cyclades.server_id
1143
    def _run(self, server_id, ip):
1144
        self._optional_output(self.client.detach_floating_ip(server_id, ip))
1145

    
1146
    def main(self, server_id, ip):
1147
        super(self.__class__, self)._run()
1148
        self._run(server_id=server_id, ip=ip)