Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 54c90711

History | View | Annotate | Download (39 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)
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

    
49

    
50
server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
51
flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
52
network_cmds = CommandTree('network', 'Cyclades/Compute API network commands')
53
_commands = [server_cmds, flavor_cmds, network_cmds]
54

    
55

    
56
about_authentication = '\nUser Authentication:\
57
    \n* to check authentication: /user authenticate\
58
    \n* to set authentication token: /config set cloud.<cloud>.token <token>'
59

    
60
howto_personality = [
61
    'Defines a file to be injected to VMs personality.',
62
    'Personality value syntax: PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
63
    '  PATH: of local file to be injected',
64
    '  SERVER_PATH: destination location inside server Image',
65
    '  OWNER: user id of destination file owner',
66
    '  GROUP: group id or name to own destination file',
67
    '  MODEL: permition in octal (e.g. 0777 or o+rwx)']
68

    
69

    
70
class _server_wait(object):
71

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

    
80
    def _wait(self, server_id, currect_status):
81
        (progress_bar, wait_cb) = self._safe_progress_bar(
82
            'Server %s still in %s mode' % (server_id, currect_status))
83

    
84
        try:
85
            new_mode = self.client.wait_server(
86
                server_id,
87
                currect_status,
88
                wait_cb=wait_cb)
89
        except Exception:
90
            raise
91
        finally:
92
            self._safe_progress_bar_finish(progress_bar)
93
        if new_mode:
94
            print('Server %s is now in %s mode' % (server_id, new_mode))
95
        else:
96
            raiseCLIError(None, 'Time out')
97

    
98

    
99
class _network_wait(object):
100

    
101
    wait_arguments = dict(
102
        progress_bar=ProgressBarArgument(
103
            'do not show progress bar',
104
            ('-N', '--no-progress-bar'),
105
            False
106
        )
107
    )
108

    
109
    def _wait(self, net_id, currect_status):
110
        (progress_bar, wait_cb) = self._safe_progress_bar(
111
            'Network %s still in %s mode' % (net_id, currect_status))
112

    
113
        try:
114
            new_mode = self.client.wait_network(
115
                net_id,
116
                currect_status,
117
                wait_cb=wait_cb)
118
        except Exception:
119
            raise
120
        finally:
121
            self._safe_progress_bar_finish(progress_bar)
122
        if new_mode:
123
            print('Network %s is now in %s mode' % (net_id, new_mode))
124
        else:
125
            raiseCLIError(None, 'Time out')
126

    
127

    
128
class _init_cyclades(_command_init):
129
    @errors.generic.all
130
    @addLogSettings
131
    def _run(self, service='compute'):
132
        if getattr(self, 'cloud', None):
133
            base_url = self._custom_url(service)\
134
                or self._custom_url('cyclades')
135
            if base_url:
136
                token = self._custom_token(service)\
137
                    or self._custom_token('cyclades')\
138
                    or self.config.get_cloud('token')
139
                self.client = CycladesClient(
140
                    base_url=base_url, token=token)
141
                return
142
        else:
143
            self.cloud = 'default'
144
        if getattr(self, 'auth_base', False):
145
            cyclades_endpoints = self.auth_base.get_service_endpoints(
146
                self._custom_type('cyclades') or 'compute',
147
                self._custom_version('cyclades') or '')
148
            base_url = cyclades_endpoints['publicURL']
149
            token = self.auth_base.token
150
            self.client = CycladesClient(base_url=base_url, token=token)
151
        else:
152
            raise CLIBaseUrlError(service='cyclades')
153

    
154
    def main(self):
155
        self._run()
156

    
157

    
158
@command(server_cmds)
159
class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
160
    """List Virtual Machines accessible by user"""
161

    
162
    PERMANENTS = ('id', 'name')
163

    
164
    __doc__ += about_authentication
165

    
166
    arguments = dict(
167
        detail=FlagArgument('show detailed output', ('-l', '--details')),
168
        since=DateArgument(
169
            'show only items since date (\' d/m/Y H:M:S \')',
170
            '--since'),
171
        limit=IntArgument('limit number of listed VMs', ('-n', '--number')),
172
        more=FlagArgument(
173
            'output results in pages (-n to set items per page, default 10)',
174
            '--more'),
175
        enum=FlagArgument('Enumerate results', '--enumerate'),
176
        flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
177
        image_id=ValueArgument('filter by image id', ('--image-id')),
178
        user_id=ValueArgument('filter by user id', ('--user-id')),
179
        user_name=ValueArgument('filter by user name', ('--user-name')),
180
        status=ValueArgument(
181
            'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
182
            ('--status')),
183
        meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
184
        meta_like=KeyValueArgument(
185
            'print only if in key=value, the value is part of actual value',
186
            ('--metadata-like')),
187
    )
188

    
189
    def _add_user_name(self, servers):
190
        uuids = self._uuids2usernames(list(set(
191
                [srv['user_id'] for srv in servers] +
192
                [srv['tenant_id'] for srv in servers])))
193
        for srv in servers:
194
            srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
195
            srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
196
        return servers
197

    
198
    def _apply_common_filters(self, servers):
199
        common_filters = dict()
200
        if self['status']:
201
            common_filters['status'] = self['status']
202
        if self['user_id'] or self['user_name']:
203
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
204
            common_filters['user_id'] = uuid
205
        return filter_dicts_by_dict(servers, common_filters)
206

    
207
    def _filter_by_image(self, servers):
208
        iid = self['image_id']
209
        new_servers = []
210
        for srv in servers:
211
            if srv['image']['id'] == iid:
212
                new_servers.append(srv)
213
        return new_servers
214

    
215
    def _filter_by_flavor(self, servers):
216
        fid = self['flavor_id']
217
        new_servers = []
218
        for srv in servers:
219
            if '%s' % srv['flavor']['id'] == '%s' % fid:
220
                new_servers.append(srv)
221
        return new_servers
222

    
223
    def _filter_by_metadata(self, servers):
224
        new_servers = []
225
        for srv in servers:
226
            if not 'metadata' in srv:
227
                continue
228
            meta = [dict(srv['metadata'])]
229
            if self['meta']:
230
                meta = filter_dicts_by_dict(meta, self['meta'])
231
            if meta and self['meta_like']:
232
                meta = filter_dicts_by_dict(
233
                    meta, self['meta_like'], exact_match=False)
234
            if meta:
235
                new_servers.append(srv)
236
        return new_servers
237

    
238
    @errors.generic.all
239
    @errors.cyclades.connection
240
    @errors.cyclades.date
241
    def _run(self):
242
        withimage = bool(self['image_id'])
243
        withflavor = bool(self['flavor_id'])
244
        withmeta = bool(self['meta'] or self['meta_like'])
245
        withcommons = bool(
246
            self['status'] or self['user_id'] or self['user_name'])
247
        detail = self['detail'] or (
248
            withimage or withflavor or withmeta or withcommons)
249
        servers = self.client.list_servers(detail, self['since'])
250

    
251
        servers = self._filter_by_name(servers)
252
        servers = self._filter_by_id(servers)
253
        servers = self._apply_common_filters(servers)
254
        if withimage:
255
            servers = self._filter_by_image(servers)
256
        if withflavor:
257
            servers = self._filter_by_flavor(servers)
258
        if withmeta:
259
            servers = self._filter_by_metadata(servers)
260

    
261
        if self['detail'] and not self['json_output']:
262
            servers = self._add_user_name(servers)
263
        elif not (self['detail'] or self['json_output']):
264
            remove_from_items(servers, 'links')
265
        if detail and not self['detail']:
266
            for srv in servers:
267
                for key in set(srv).difference(self.PERMANENTS):
268
                    srv.pop(key)
269
        kwargs = dict(with_enumeration=self['enum'])
270
        if self['more']:
271
            kwargs['page_size'] = self['limit'] if self['limit'] else 10
272
        elif self['limit']:
273
            servers = servers[:self['limit']]
274
        self._print(servers, **kwargs)
275

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

    
280

    
281
@command(server_cmds)
282
class server_info(_init_cyclades, _optional_json):
283
    """Detailed information on a Virtual Machine
284
    Contains:
285
    - name, id, status, create/update dates
286
    - network interfaces
287
    - metadata (e.g. os, superuser) and diagnostics
288
    - hardware flavor and os image ids
289
    """
290

    
291
    @errors.generic.all
292
    @errors.cyclades.connection
293
    @errors.cyclades.server_id
294
    def _run(self, server_id):
295
        vm = self.client.get_server_details(server_id)
296
        uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
297
        vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
298
        vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
299
        self._print(vm, print_dict)
300

    
301
    def main(self, server_id):
302
        super(self.__class__, self)._run()
303
        self._run(server_id=server_id)
304

    
305

    
306
class PersonalityArgument(KeyValueArgument):
307
    @property
308
    def value(self):
309
        return self._value if hasattr(self, '_value') else []
310

    
311
    @value.setter
312
    def value(self, newvalue):
313
        if newvalue == self.default:
314
            return self.value
315
        self._value = []
316
        for i, terms in enumerate(newvalue):
317
            termlist = terms.split(',')
318
            if len(termlist) > 5:
319
                msg = 'Wrong number of terms (should be 1 to 5)'
320
                raiseCLIError(CLISyntaxError(msg), details=howto_personality)
321
            path = termlist[0]
322
            if not exists(path):
323
                raiseCLIError(
324
                    None,
325
                    '--personality: File %s does not exist' % path,
326
                    importance=1,
327
                    details=howto_personality)
328
            self._value.append(dict(path=path))
329
            with open(path) as f:
330
                self._value[i]['contents'] = b64encode(f.read())
331
            try:
332
                self._value[i]['path'] = termlist[1]
333
                self._value[i]['owner'] = termlist[2]
334
                self._value[i]['group'] = termlist[3]
335
                self._value[i]['mode'] = termlist[4]
336
            except IndexError:
337
                pass
338

    
339

    
340
@command(server_cmds)
341
class server_create(_init_cyclades, _optional_json, _server_wait):
342
    """Create a server (aka Virtual Machine)
343
    Parameters:
344
    - name: (single quoted text)
345
    - flavor id: Hardware flavor. Pick one from: /flavor list
346
    - image id: OS images. Pick one from: /image list
347
    """
348

    
349
    arguments = dict(
350
        personality=PersonalityArgument(
351
            (80 * ' ').join(howto_personality), ('-p', '--personality')),
352
        wait=FlagArgument('Wait server to build', ('-w', '--wait'))
353
    )
354

    
355
    @errors.generic.all
356
    @errors.cyclades.connection
357
    @errors.plankton.id
358
    @errors.cyclades.flavor_id
359
    def _run(self, name, flavor_id, image_id):
360
        r = self.client.create_server(
361
            name, int(flavor_id), image_id, self['personality'])
362
        usernames = self._uuids2usernames([r['user_id'], r['tenant_id']])
363
        r['user_id'] += ' (%s)' % usernames[r['user_id']]
364
        r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
365
        self._print(r, print_dict)
366
        if self['wait']:
367
            self._wait(r['id'], r['status'])
368

    
369
    def main(self, name, flavor_id, image_id):
370
        super(self.__class__, self)._run()
371
        self._run(name=name, flavor_id=flavor_id, image_id=image_id)
372

    
373

    
374
@command(server_cmds)
375
class server_rename(_init_cyclades, _optional_output_cmd):
376
    """Set/update a server (VM) name
377
    VM names are not unique, therefore multiple servers may share the same name
378
    """
379

    
380
    @errors.generic.all
381
    @errors.cyclades.connection
382
    @errors.cyclades.server_id
383
    def _run(self, server_id, new_name):
384
        self._optional_output(
385
            self.client.update_server_name(int(server_id), new_name))
386

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

    
391

    
392
@command(server_cmds)
393
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
394
    """Delete a server (VM)"""
395

    
396
    arguments = dict(
397
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
398
    )
399

    
400
    @errors.generic.all
401
    @errors.cyclades.connection
402
    @errors.cyclades.server_id
403
    def _run(self, server_id):
404
            status = 'DELETED'
405
            if self['wait']:
406
                details = self.client.get_server_details(server_id)
407
                status = details['status']
408

    
409
            r = self.client.delete_server(int(server_id))
410
            self._optional_output(r)
411

    
412
            if self['wait']:
413
                self._wait(server_id, status)
414

    
415
    def main(self, server_id):
416
        super(self.__class__, self)._run()
417
        self._run(server_id=server_id)
418

    
419

    
420
@command(server_cmds)
421
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
422
    """Reboot a server (VM)"""
423

    
424
    arguments = dict(
425
        hard=FlagArgument('perform a hard reboot', ('-f', '--force')),
426
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
427
    )
428

    
429
    @errors.generic.all
430
    @errors.cyclades.connection
431
    @errors.cyclades.server_id
432
    def _run(self, server_id):
433
        r = self.client.reboot_server(int(server_id), self['hard'])
434
        self._optional_output(r)
435

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

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

    
443

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

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

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

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

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

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

    
473

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

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

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

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

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

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

    
503

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

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

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

    
524

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

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

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

    
544

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

    
549

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

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

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

    
571

    
572
@command(server_cmds)
573
class server_firewall_get(_init_cyclades):
574
    """Get the server (VM) firewall profile for its public network"""
575

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

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

    
586

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

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

    
595
    @errors.generic.all
596
    @errors.cyclades.connection
597
    @errors.cyclades.server_id
598
    def _run(self, server_id):
599
        reply = self.client.list_server_nics(int(server_id))
600
        self._print(
601
            reply, with_enumeration=self['enum'] and len(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), print_dict)
624

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

    
629

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

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

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

    
664

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

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

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

    
681

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

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

    
692
    def main(self, server_id):
693
        super(self.__class__, self)._run()
694
        self._run(server_id=server_id)
695

    
696

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

    
701
    @errors.generic.all
702
    @errors.cyclades.connection
703
    @errors.cyclades.server_id
704
    def _run(self, server_id, currect_status):
705
        self._wait(server_id, currect_status)
706

    
707
    def main(self, server_id, currect_status='BUILD'):
708
        super(self.__class__, self)._run()
709
        self._run(server_id=server_id, currect_status=currect_status)
710

    
711

    
712
@command(flavor_cmds)
713
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
714
    """List available hardware flavors"""
715

    
716
    PERMANENTS = ('id', 'name')
717

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

    
732
    def _apply_common_filters(self, flavors):
733
        common_filters = dict()
734
        if self['ram']:
735
            common_filters['ram'] = self['ram']
736
        if self['vcpus']:
737
            common_filters['vcpus'] = self['vcpus']
738
        if self['disk']:
739
            common_filters['disk'] = self['disk']
740
        if self['disk_template']:
741
            common_filters['SNF:disk_template'] = self['disk_template']
742
        return filter_dicts_by_dict(flavors, common_filters)
743

    
744
    @errors.generic.all
745
    @errors.cyclades.connection
746
    def _run(self):
747
        withcommons = self['ram'] or self['vcpus'] or (
748
            self['disk'] or self['disk_template'])
749
        detail = self['detail'] or withcommons
750
        flavors = self.client.list_flavors(detail)
751
        flavors = self._filter_by_name(flavors)
752
        flavors = self._filter_by_id(flavors)
753
        if withcommons:
754
            flavors = self._apply_common_filters(flavors)
755
        if not (self['detail'] or self['json_output']):
756
            remove_from_items(flavors, 'links')
757
        if detail and not self['detail']:
758
            for flv in flavors:
759
                for key in set(flv).difference(self.PERMANENTS):
760
                    flv.pop(key)
761
        pg_size = 10 if self['more'] and not self['limit'] else self['limit']
762
        self._print(
763
            flavors,
764
            with_redundancy=self['detail'],
765
            page_size=pg_size,
766
            with_enumeration=self['enum'])
767

    
768
    def main(self):
769
        super(self.__class__, self)._run()
770
        self._run()
771

    
772

    
773
@command(flavor_cmds)
774
class flavor_info(_init_cyclades, _optional_json):
775
    """Detailed information on a hardware flavor
776
    To get a list of available flavors and flavor ids, try /flavor list
777
    """
778

    
779
    @errors.generic.all
780
    @errors.cyclades.connection
781
    @errors.cyclades.flavor_id
782
    def _run(self, flavor_id):
783
        self._print(
784
            self.client.get_flavor_details(int(flavor_id)), print_dict)
785

    
786
    def main(self, flavor_id):
787
        super(self.__class__, self)._run()
788
        self._run(flavor_id=flavor_id)
789

    
790

    
791
def _add_name(self, net):
792
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
793
        if user_id:
794
            uuids.append(user_id)
795
        if tenant_id:
796
            uuids.append(tenant_id)
797
        if uuids:
798
            usernames = self._uuids2usernames(uuids)
799
            if user_id:
800
                net['user_id'] += ' (%s)' % usernames[user_id]
801
            if tenant_id:
802
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]
803

    
804

    
805
@command(network_cmds)
806
class network_info(_init_cyclades, _optional_json):
807
    """Detailed information on a network
808
    To get a list of available networks and network ids, try /network list
809
    """
810

    
811
    @errors.generic.all
812
    @errors.cyclades.connection
813
    @errors.cyclades.network_id
814
    def _run(self, network_id):
815
        network = self.client.get_network_details(int(network_id))
816
        _add_name(self, network)
817
        self._print(network, print_dict, exclude=('id'))
818

    
819
    def main(self, network_id):
820
        super(self.__class__, self)._run()
821
        self._run(network_id=network_id)
822

    
823

    
824
@command(network_cmds)
825
class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
826
    """List networks"""
827

    
828
    PERMANENTS = ('id', 'name')
829

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

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

    
873
    def _add_name(self, networks, key='user_id'):
874
        uuids = self._uuids2usernames(
875
            list(set([net[key] for net in networks])))
876
        for net in networks:
877
            v = net.get(key, None)
878
            if v:
879
                net[key] += ' (%s)' % uuids[net[key]]
880
        return networks
881

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

    
914
    def main(self):
915
        super(self.__class__, self)._run()
916
        self._run()
917

    
918

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

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

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

    
950
    def main(self, name):
951
        super(self.__class__, self)._run()
952
        self._run(name)
953

    
954

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

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

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

    
970

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

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

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

    
991
        r = self.client.delete_network(int(network_id))
992
        self._optional_output(r)
993

    
994
        if self['wait']:
995
            self._wait(network_id, status)
996

    
997
    def main(self, network_id):
998
        super(self.__class__, self)._run()
999
        self._run(network_id=network_id)
1000

    
1001

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

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

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

    
1018

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

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

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

    
1044
    def main(self, nic_id):
1045
        super(self.__class__, self)._run()
1046
        server_id = self._server_id_from_nic(nic_id=nic_id)
1047
        self._run(nic_id=nic_id, server_id=server_id)
1048

    
1049

    
1050
@command(network_cmds)
1051
class network_wait(_init_cyclades, _network_wait):
1052
    """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1053

    
1054
    @errors.generic.all
1055
    @errors.cyclades.connection
1056
    @errors.cyclades.network_id
1057
    def _run(self, network_id, currect_status):
1058
        self._wait(network_id, currect_status)
1059

    
1060
    def main(self, network_id, currect_status='PENDING'):
1061
        super(self.__class__, self)._run()
1062
        self._run(network_id=network_id, currect_status=currect_status)
1063

    
1064

    
1065
@command(server_cmds)
1066
class server_ip(_init_cyclades):
1067
    """Manage floating IPs for the servers"""
1068

    
1069

    
1070
@command(server_cmds)
1071
class server_ip_pools(_init_cyclades, _optional_json):
1072
    """List all floating pools of floating ips"""
1073

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

    
1080
    def main(self):
1081
        super(self.__class__, self)._run()
1082
        self._run()
1083

    
1084

    
1085
@command(server_cmds)
1086
class server_ip_list(_init_cyclades, _optional_json):
1087
    """List all floating ips"""
1088

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

    
1095
    def main(self):
1096
        super(self.__class__, self)._run()
1097
        self._run()
1098

    
1099

    
1100
@command(server_cmds)
1101
class server_ip_info(_init_cyclades, _optional_json):
1102
    """A floating IPs' details"""
1103

    
1104
    @errors.generic.all
1105
    @errors.cyclades.connection
1106
    def _run(self, ip):
1107
        self._print(self.client.get_floating_ip(ip), print_dict)
1108

    
1109
    def main(self, ip):
1110
        super(self.__class__, self)._run()
1111
        self._run(ip=ip)
1112

    
1113

    
1114
@command(server_cmds)
1115
class server_ip_create(_init_cyclades, _optional_json):
1116
    """Create a new floating IP"""
1117

    
1118
    arguments = dict(
1119
        pool=ValueArgument('Source IP pool', ('--pool'), None)
1120
    )
1121

    
1122
    @errors.generic.all
1123
    @errors.cyclades.connection
1124
    def _run(self, ip=None):
1125
        self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1126

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

    
1131

    
1132
@command(server_cmds)
1133
class server_ip_delete(_init_cyclades, _optional_output_cmd):
1134
    """Delete a floating ip"""
1135

    
1136
    @errors.generic.all
1137
    @errors.cyclades.connection
1138
    def _run(self, ip):
1139
        self._optional_output(self.client.delete_floating_ip(ip))
1140

    
1141
    def main(self, ip):
1142
        super(self.__class__, self)._run()
1143
        self._run(ip=ip)
1144

    
1145

    
1146
@command(server_cmds)
1147
class server_ip_attach(_init_cyclades, _optional_output_cmd):
1148
    """Attach a floating ip to a server with server_id
1149
    """
1150

    
1151
    @errors.generic.all
1152
    @errors.cyclades.connection
1153
    @errors.cyclades.server_id
1154
    def _run(self, server_id, ip):
1155
        self._optional_output(self.client.attach_floating_ip(server_id, ip))
1156

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

    
1161

    
1162
@command(server_cmds)
1163
class server_ip_detach(_init_cyclades, _optional_output_cmd):
1164
    """Detach floating IP from server
1165
    """
1166

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

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