Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (35.4 kB)

1
# Copyright 2011-2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from 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
        self._print(self.client.get_server_details(server_id), print_dict)
296

    
297
    def main(self, server_id):
298
        super(self.__class__, self)._run()
299
        self._run(server_id=server_id)
300

    
301

    
302
class PersonalityArgument(KeyValueArgument):
303
    @property
304
    def value(self):
305
        return self._value if hasattr(self, '_value') else []
306

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

    
335

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

    
345
    arguments = dict(
346
        personality=PersonalityArgument(
347
            (80 * ' ').join(howto_personality), ('-p', '--personality')),
348
        wait=FlagArgument('Wait server to build', ('-w', '--wait'))
349
    )
350

    
351
    @errors.generic.all
352
    @errors.cyclades.connection
353
    @errors.plankton.id
354
    @errors.cyclades.flavor_id
355
    def _run(self, name, flavor_id, image_id):
356
        r = self.client.create_server(
357
            name, int(flavor_id), image_id, self['personality'])
358
        self._print(r, print_dict)
359
        if self['wait']:
360
            self._wait(r['id'], r['status'])
361

    
362
    def main(self, name, flavor_id, image_id):
363
        super(self.__class__, self)._run()
364
        self._run(name=name, flavor_id=flavor_id, image_id=image_id)
365

    
366

    
367
@command(server_cmds)
368
class server_rename(_init_cyclades, _optional_output_cmd):
369
    """Set/update a server (VM) name
370
    VM names are not unique, therefore multiple servers may share the same name
371
    """
372

    
373
    @errors.generic.all
374
    @errors.cyclades.connection
375
    @errors.cyclades.server_id
376
    def _run(self, server_id, new_name):
377
        self._optional_output(
378
            self.client.update_server_name(int(server_id), new_name))
379

    
380
    def main(self, server_id, new_name):
381
        super(self.__class__, self)._run()
382
        self._run(server_id=server_id, new_name=new_name)
383

    
384

    
385
@command(server_cmds)
386
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
387
    """Delete a server (VM)"""
388

    
389
    arguments = dict(
390
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
391
    )
392

    
393
    @errors.generic.all
394
    @errors.cyclades.connection
395
    @errors.cyclades.server_id
396
    def _run(self, server_id):
397
            status = 'DELETED'
398
            if self['wait']:
399
                details = self.client.get_server_details(server_id)
400
                status = details['status']
401

    
402
            r = self.client.delete_server(int(server_id))
403
            self._optional_output(r)
404

    
405
            if self['wait']:
406
                self._wait(server_id, status)
407

    
408
    def main(self, server_id):
409
        super(self.__class__, self)._run()
410
        self._run(server_id=server_id)
411

    
412

    
413
@command(server_cmds)
414
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
415
    """Reboot a server (VM)"""
416

    
417
    arguments = dict(
418
        hard=FlagArgument('perform a hard reboot', ('-f', '--force')),
419
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
420
    )
421

    
422
    @errors.generic.all
423
    @errors.cyclades.connection
424
    @errors.cyclades.server_id
425
    def _run(self, server_id):
426
        r = self.client.reboot_server(int(server_id), self['hard'])
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 server (VM)"""
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 server (VM)"""
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 server (VM)
500
    Console connection information provided (at least):
501
    - host: (url or address) a VNC host
502
    - port: (int) the gateway to enter VM 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)), 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 server (VM) firewall profiles for public networks"""
541

    
542

    
543
@command(server_cmds)
544
class server_firewall_set(_init_cyclades, _optional_output_cmd):
545
    """Set the server (VM) firewall profile on VMs 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 server (VM) firewall profile for its public network"""
568

    
569
    @errors.generic.all
570
    @errors.cyclades.connection
571
    @errors.cyclades.server_id
572
    def _run(self, server_id):
573
        print(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 server (VM)"""
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(
594
            reply, with_enumeration=self['enum'] and len(reply) > 1)
595

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

    
600

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

    
605

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

    
610
    @errors.generic.all
611
    @errors.cyclades.connection
612
    @errors.cyclades.server_id
613
    @errors.cyclades.metadata
614
    def _run(self, server_id, key=''):
615
        self._print(
616
            self.client.get_server_metadata(int(server_id), key), 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 server(VM) 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
            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 server (VM) 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 server (VM) statistics"""
678

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

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

    
689

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

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

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

    
704

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

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

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

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

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

    
761
    def main(self):
762
        super(self.__class__, self)._run()
763
        self._run()
764

    
765

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

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

    
779
    def main(self, flavor_id):
780
        super(self.__class__, self)._run()
781
        self._run(flavor_id=flavor_id)
782

    
783

    
784
@command(network_cmds)
785
class network_info(_init_cyclades, _optional_json):
786
    """Detailed information on a network
787
    To get a list of available networks and network ids, try /network list
788
    """
789

    
790
    @errors.generic.all
791
    @errors.cyclades.connection
792
    @errors.cyclades.network_id
793
    def _run(self, network_id):
794
        network = self.client.get_network_details(int(network_id))
795
        self._print(network, print_dict, exclude=('id'))
796

    
797
    def main(self, network_id):
798
        super(self.__class__, self)._run()
799
        self._run(network_id=network_id)
800

    
801

    
802
@command(network_cmds)
803
class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
804
    """List networks"""
805

    
806
    arguments = dict(
807
        detail=FlagArgument('show detailed output', ('-l', '--details')),
808
        limit=IntArgument('limit # of listed networks', ('-n', '--number')),
809
        more=FlagArgument(
810
            'output results in pages (-n to set items per page, default 10)',
811
            '--more'),
812
        enum=FlagArgument('Enumerate results', '--enumerate')
813
    )
814

    
815
    @errors.generic.all
816
    @errors.cyclades.connection
817
    def _run(self):
818
        networks = self.client.list_networks(self['detail'])
819
        networks = self._filter_by_name(networks)
820
        networks = self._filter_by_id(networks)
821
        if not (self['detail'] or self['json_output']):
822
            remove_from_items(networks, 'links')
823
        kwargs = dict(with_enumeration=self['enum'])
824
        if self['more']:
825
            kwargs['page_size'] = self['limit'] or 10
826
        elif self['limit']:
827
            networks = networks[:self['limit']]
828
        self._print(networks, **kwargs)
829

    
830
    def main(self):
831
        super(self.__class__, self)._run()
832
        self._run()
833

    
834

    
835
@command(network_cmds)
836
class network_create(_init_cyclades, _optional_json, _network_wait):
837
    """Create an (unconnected) network"""
838

    
839
    arguments = dict(
840
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
841
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
842
        dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
843
        type=ValueArgument(
844
            'Valid network types are '
845
            'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
846
            '--with-type',
847
            default='MAC_FILTERED'),
848
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
849
    )
850

    
851
    @errors.generic.all
852
    @errors.cyclades.connection
853
    @errors.cyclades.network_max
854
    def _run(self, name):
855
        r = self.client.create_network(
856
            name,
857
            cidr=self['cidr'],
858
            gateway=self['gateway'],
859
            dhcp=self['dhcp'],
860
            type=self['type'])
861
        self._print(r, print_dict)
862

    
863
        if self['wait']:
864
            self._wait(r['id'], 'PENDING')
865

    
866
    def main(self, name):
867
        super(self.__class__, self)._run()
868
        self._run(name)
869

    
870

    
871
@command(network_cmds)
872
class network_rename(_init_cyclades, _optional_output_cmd):
873
    """Set the name of a network"""
874

    
875
    @errors.generic.all
876
    @errors.cyclades.connection
877
    @errors.cyclades.network_id
878
    def _run(self, network_id, new_name):
879
        self._optional_output(
880
                self.client.update_network_name(int(network_id), new_name))
881

    
882
    def main(self, network_id, new_name):
883
        super(self.__class__, self)._run()
884
        self._run(network_id=network_id, new_name=new_name)
885

    
886

    
887
@command(network_cmds)
888
class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
889
    """Delete a network"""
890

    
891
    arguments = dict(
892
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
893
    )
894

    
895
    @errors.generic.all
896
    @errors.cyclades.connection
897
    @errors.cyclades.network_id
898
    @errors.cyclades.network_in_use
899
    def _run(self, network_id):
900
        status = 'DELETED'
901
        if self['wait']:
902
            r = self.client.get_network_details(network_id)
903
            status = r['status']
904
            if status in ('DELETED', ):
905
                return
906

    
907
        r = self.client.delete_network(int(network_id))
908
        self._optional_output(r)
909

    
910
        if self['wait']:
911
            self._wait(network_id, status)
912

    
913
    def main(self, network_id):
914
        super(self.__class__, self)._run()
915
        self._run(network_id=network_id)
916

    
917

    
918
@command(network_cmds)
919
class network_connect(_init_cyclades, _optional_output_cmd):
920
    """Connect a server to a network"""
921

    
922
    @errors.generic.all
923
    @errors.cyclades.connection
924
    @errors.cyclades.server_id
925
    @errors.cyclades.network_id
926
    def _run(self, server_id, network_id):
927
        self._optional_output(
928
                self.client.connect_server(int(server_id), int(network_id)))
929

    
930
    def main(self, server_id, network_id):
931
        super(self.__class__, self)._run()
932
        self._run(server_id=server_id, network_id=network_id)
933

    
934

    
935
@command(network_cmds)
936
class network_disconnect(_init_cyclades):
937
    """Disconnect a nic that connects a server to a network
938
    Nic ids are listed as "attachments" in detailed network information
939
    To get detailed network information: /network info <network id>
940
    """
941

    
942
    @errors.cyclades.nic_format
943
    def _server_id_from_nic(self, nic_id):
944
        return nic_id.split('-')[1]
945

    
946
    @errors.generic.all
947
    @errors.cyclades.connection
948
    @errors.cyclades.server_id
949
    @errors.cyclades.nic_id
950
    def _run(self, nic_id, server_id):
951
        num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
952
        if not num_of_disconnected:
953
            raise ClientError(
954
                'Network Interface %s not found on server %s' % (
955
                    nic_id,
956
                    server_id),
957
                status=404)
958
        print('Disconnected %s connections' % num_of_disconnected)
959

    
960
    def main(self, nic_id):
961
        super(self.__class__, self)._run()
962
        server_id = self._server_id_from_nic(nic_id=nic_id)
963
        self._run(nic_id=nic_id, server_id=server_id)
964

    
965

    
966
@command(network_cmds)
967
class network_wait(_init_cyclades, _network_wait):
968
    """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
969

    
970
    @errors.generic.all
971
    @errors.cyclades.connection
972
    @errors.cyclades.network_id
973
    def _run(self, network_id, currect_status):
974
        self._wait(network_id, currect_status)
975

    
976
    def main(self, network_id, currect_status='PENDING'):
977
        super(self.__class__, self)._run()
978
        self._run(network_id=network_id, currect_status=currect_status)
979

    
980

    
981
@command(server_cmds)
982
class server_ip(_init_cyclades):
983
    """Manage floating IPs for the servers"""
984

    
985

    
986
@command(server_cmds)
987
class server_ip_pools(_init_cyclades, _optional_json):
988
    """List all floating pools of floating ips"""
989

    
990
    @errors.generic.all
991
    @errors.cyclades.connection
992
    def _run(self):
993
        r = self.client.get_floating_ip_pools()
994
        self._print(r if self['json_output'] else r['floating_ip_pools'])
995

    
996
    def main(self):
997
        super(self.__class__, self)._run()
998
        self._run()
999

    
1000

    
1001
@command(server_cmds)
1002
class server_ip_list(_init_cyclades, _optional_json):
1003
    """List all floating ips"""
1004

    
1005
    @errors.generic.all
1006
    @errors.cyclades.connection
1007
    def _run(self):
1008
        r = self.client.get_floating_ips()
1009
        self._print(r if self['json_output'] else r['floating_ips'])
1010

    
1011
    def main(self):
1012
        super(self.__class__, self)._run()
1013
        self._run()
1014

    
1015

    
1016
@command(server_cmds)
1017
class server_ip_info(_init_cyclades, _optional_json):
1018
    """A floating IPs' details"""
1019

    
1020
    @errors.generic.all
1021
    @errors.cyclades.connection
1022
    def _run(self, ip):
1023
        self._print(self.client.get_floating_ip(ip), print_dict)
1024

    
1025
    def main(self, ip):
1026
        super(self.__class__, self)._run()
1027
        self._run(ip=ip)
1028

    
1029

    
1030
@command(server_cmds)
1031
class server_ip_create(_init_cyclades, _optional_json):
1032
    """Create a new floating IP"""
1033

    
1034
    arguments = dict(
1035
        pool=ValueArgument('Source IP pool', ('--pool'), None)
1036
    )
1037

    
1038
    @errors.generic.all
1039
    @errors.cyclades.connection
1040
    def _run(self, ip=None):
1041
        self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1042

    
1043
    def main(self, requested_address=None):
1044
        super(self.__class__, self)._run()
1045
        self._run(ip=requested_address)
1046

    
1047

    
1048
@command(server_cmds)
1049
class server_ip_delete(_init_cyclades, _optional_output_cmd):
1050
    """Delete a floating ip"""
1051

    
1052
    @errors.generic.all
1053
    @errors.cyclades.connection
1054
    def _run(self, ip):
1055
        self._optional_output(self.client.delete_floating_ip(ip))
1056

    
1057
    def main(self, ip):
1058
        super(self.__class__, self)._run()
1059
        self._run(ip=ip)
1060

    
1061

    
1062
@command(server_cmds)
1063
class server_ip_attach(_init_cyclades, _optional_output_cmd):
1064
    """Attach a floating ip to a server with server_id
1065
    """
1066

    
1067
    @errors.generic.all
1068
    @errors.cyclades.connection
1069
    @errors.cyclades.server_id
1070
    def _run(self, server_id, ip):
1071
        self._optional_output(self.client.attach_floating_ip(server_id, ip))
1072

    
1073
    def main(self, server_id, ip):
1074
        super(self.__class__, self)._run()
1075
        self._run(server_id=server_id, ip=ip)
1076

    
1077

    
1078
@command(server_cmds)
1079
class server_ip_detach(_init_cyclades, _optional_output_cmd):
1080
    """Detach floating IP from server
1081
    """
1082

    
1083
    @errors.generic.all
1084
    @errors.cyclades.connection
1085
    @errors.cyclades.server_id
1086
    def _run(self, server_id, ip):
1087
        self._optional_output(self.client.detach_floating_ip(server_id, ip))
1088

    
1089
    def main(self, server_id, ip):
1090
        super(self.__class__, self)._run()
1091
        self._run(server_id=server_id, ip=ip)