Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (33.8 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 _optional_output_cmd, _optional_json
44

    
45
from base64 import b64encode
46
from os.path import exists
47

    
48

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

    
54

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

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

    
68

    
69
class _server_wait(object):
70

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

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

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

    
97

    
98
class _network_wait(object):
99

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

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

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

    
126

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

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

    
156

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

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

    
163
    __doc__ += about_authentication
164

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

    
191
    def _filtered_by_name(self, servers):
192
        if self['name']:
193
            servers = filter_dicts_by_dict(servers, dict(name=self['name']))
194
        np, ns, nl = self['name_pref'], self['name_suff'], self['name_like']
195
        return [img for img in servers if (
196
            (not np) or img['name'].lower().startswith(np.lower())) and (
197
            (not ns) or img['name'].lower().endswith(ns.lower())) and (
198
            (not nl) or nl.lower() in img['name'].lower())]
199

    
200
    def _add_user_name(self, servers):
201
        uuids = self._uuids2usernames(
202
            list(set([srv['user_id'] for srv in servers])))
203
        for srv in servers:
204
            srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
205
        return servers
206

    
207
    def _filtered_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 _filtered_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 _filtered_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
        detail = self['detail'] or withimage or withflavor or withmeta
246
        servers = self.client.list_servers(detail, self['since'])
247

    
248
        servers = self._filtered_by_name(servers)
249
        if withimage:
250
            servers = self._filtered_by_image(servers)
251
        if withflavor:
252
            servers = self._filtered_by_flavor(servers)
253
        if withmeta:
254
            servers = self._filtered_by_metadata(servers)
255

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

    
271
    def main(self):
272
        super(self.__class__, self)._run()
273
        self._run()
274

    
275

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

    
286
    @errors.generic.all
287
    @errors.cyclades.connection
288
    @errors.cyclades.server_id
289
    def _run(self, server_id):
290
        self._print(self.client.get_server_details(server_id), print_dict)
291

    
292
    def main(self, server_id):
293
        super(self.__class__, self)._run()
294
        self._run(server_id=server_id)
295

    
296

    
297
class PersonalityArgument(KeyValueArgument):
298
    @property
299
    def value(self):
300
        return self._value if hasattr(self, '_value') else []
301

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

    
330

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

    
340
    arguments = dict(
341
        personality=PersonalityArgument(
342
            (80 * ' ').join(howto_personality), ('-p', '--personality')),
343
        wait=FlagArgument('Wait server to build', ('-w', '--wait'))
344
    )
345

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

    
357
    def main(self, name, flavor_id, image_id):
358
        super(self.__class__, self)._run()
359
        self._run(name=name, flavor_id=flavor_id, image_id=image_id)
360

    
361

    
362
@command(server_cmds)
363
class server_rename(_init_cyclades, _optional_output_cmd):
364
    """Set/update a server (VM) name
365
    VM names are not unique, therefore multiple servers may share the same name
366
    """
367

    
368
    @errors.generic.all
369
    @errors.cyclades.connection
370
    @errors.cyclades.server_id
371
    def _run(self, server_id, new_name):
372
        self._optional_output(
373
            self.client.update_server_name(int(server_id), new_name))
374

    
375
    def main(self, server_id, new_name):
376
        super(self.__class__, self)._run()
377
        self._run(server_id=server_id, new_name=new_name)
378

    
379

    
380
@command(server_cmds)
381
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
382
    """Delete a server (VM)"""
383

    
384
    arguments = dict(
385
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
386
    )
387

    
388
    @errors.generic.all
389
    @errors.cyclades.connection
390
    @errors.cyclades.server_id
391
    def _run(self, server_id):
392
            status = 'DELETED'
393
            if self['wait']:
394
                details = self.client.get_server_details(server_id)
395
                status = details['status']
396

    
397
            r = self.client.delete_server(int(server_id))
398
            self._optional_output(r)
399

    
400
            if self['wait']:
401
                self._wait(server_id, status)
402

    
403
    def main(self, server_id):
404
        super(self.__class__, self)._run()
405
        self._run(server_id=server_id)
406

    
407

    
408
@command(server_cmds)
409
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
410
    """Reboot a server (VM)"""
411

    
412
    arguments = dict(
413
        hard=FlagArgument('perform a hard reboot', ('-f', '--force')),
414
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
415
    )
416

    
417
    @errors.generic.all
418
    @errors.cyclades.connection
419
    @errors.cyclades.server_id
420
    def _run(self, server_id):
421
        r = self.client.reboot_server(int(server_id), self['hard'])
422
        self._optional_output(r)
423

    
424
        if self['wait']:
425
            self._wait(server_id, 'REBOOT')
426

    
427
    def main(self, server_id):
428
        super(self.__class__, self)._run()
429
        self._run(server_id=server_id)
430

    
431

    
432
@command(server_cmds)
433
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
434
    """Start an existing server (VM)"""
435

    
436
    arguments = dict(
437
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
438
    )
439

    
440
    @errors.generic.all
441
    @errors.cyclades.connection
442
    @errors.cyclades.server_id
443
    def _run(self, server_id):
444
        status = 'ACTIVE'
445
        if self['wait']:
446
            details = self.client.get_server_details(server_id)
447
            status = details['status']
448
            if status in ('ACTIVE', ):
449
                return
450

    
451
        r = self.client.start_server(int(server_id))
452
        self._optional_output(r)
453

    
454
        if self['wait']:
455
            self._wait(server_id, status)
456

    
457
    def main(self, server_id):
458
        super(self.__class__, self)._run()
459
        self._run(server_id=server_id)
460

    
461

    
462
@command(server_cmds)
463
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
464
    """Shutdown an active server (VM)"""
465

    
466
    arguments = dict(
467
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
468
    )
469

    
470
    @errors.generic.all
471
    @errors.cyclades.connection
472
    @errors.cyclades.server_id
473
    def _run(self, server_id):
474
        status = 'STOPPED'
475
        if self['wait']:
476
            details = self.client.get_server_details(server_id)
477
            status = details['status']
478
            if status in ('STOPPED', ):
479
                return
480

    
481
        r = self.client.shutdown_server(int(server_id))
482
        self._optional_output(r)
483

    
484
        if self['wait']:
485
            self._wait(server_id, status)
486

    
487
    def main(self, server_id):
488
        super(self.__class__, self)._run()
489
        self._run(server_id=server_id)
490

    
491

    
492
@command(server_cmds)
493
class server_console(_init_cyclades, _optional_json):
494
    """Get a VNC console to access an existing server (VM)
495
    Console connection information provided (at least):
496
    - host: (url or address) a VNC host
497
    - port: (int) the gateway to enter VM on host
498
    - password: for VNC authorization
499
    """
500

    
501
    @errors.generic.all
502
    @errors.cyclades.connection
503
    @errors.cyclades.server_id
504
    def _run(self, server_id):
505
        self._print(
506
            self.client.get_server_console(int(server_id)), print_dict)
507

    
508
    def main(self, server_id):
509
        super(self.__class__, self)._run()
510
        self._run(server_id=server_id)
511

    
512

    
513
@command(server_cmds)
514
class server_resize(_init_cyclades, _optional_output_cmd):
515
    """Set a different flavor for an existing server
516
    To get server ids and flavor ids:
517
    /server list
518
    /flavor list
519
    """
520

    
521
    @errors.generic.all
522
    @errors.cyclades.connection
523
    @errors.cyclades.server_id
524
    @errors.cyclades.flavor_id
525
    def _run(self, server_id, flavor_id):
526
        self._optional_output(self.client.resize_server(server_id, flavor_id))
527

    
528
    def main(self, server_id, flavor_id):
529
        super(self.__class__, self)._run()
530
        self._run(server_id=server_id, flavor_id=flavor_id)
531

    
532

    
533
@command(server_cmds)
534
class server_firewall(_init_cyclades):
535
    """Manage server (VM) firewall profiles for public networks"""
536

    
537

    
538
@command(server_cmds)
539
class server_firewall_set(_init_cyclades, _optional_output_cmd):
540
    """Set the server (VM) firewall profile on VMs public network
541
    Values for profile:
542
    - DISABLED: Shutdown firewall
543
    - ENABLED: Firewall in normal mode
544
    - PROTECTED: Firewall in secure mode
545
    """
546

    
547
    @errors.generic.all
548
    @errors.cyclades.connection
549
    @errors.cyclades.server_id
550
    @errors.cyclades.firewall
551
    def _run(self, server_id, profile):
552
        self._optional_output(self.client.set_firewall_profile(
553
            server_id=int(server_id), profile=('%s' % profile).upper()))
554

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

    
559

    
560
@command(server_cmds)
561
class server_firewall_get(_init_cyclades):
562
    """Get the server (VM) firewall profile for its public network"""
563

    
564
    @errors.generic.all
565
    @errors.cyclades.connection
566
    @errors.cyclades.server_id
567
    def _run(self, server_id):
568
        print(self.client.get_firewall_profile(server_id))
569

    
570
    def main(self, server_id):
571
        super(self.__class__, self)._run()
572
        self._run(server_id=server_id)
573

    
574

    
575
@command(server_cmds)
576
class server_addr(_init_cyclades, _optional_json):
577
    """List the addresses of all network interfaces on a server (VM)"""
578

    
579
    arguments = dict(
580
        enum=FlagArgument('Enumerate results', '--enumerate')
581
    )
582

    
583
    @errors.generic.all
584
    @errors.cyclades.connection
585
    @errors.cyclades.server_id
586
    def _run(self, server_id):
587
        reply = self.client.list_server_nics(int(server_id))
588
        self._print(
589
            reply, with_enumeration=self['enum'] and len(reply) > 1)
590

    
591
    def main(self, server_id):
592
        super(self.__class__, self)._run()
593
        self._run(server_id=server_id)
594

    
595

    
596
@command(server_cmds)
597
class server_metadata(_init_cyclades):
598
    """Manage Server metadata (key:value pairs of server attributes)"""
599

    
600

    
601
@command(server_cmds)
602
class server_metadata_list(_init_cyclades, _optional_json):
603
    """Get server metadata"""
604

    
605
    @errors.generic.all
606
    @errors.cyclades.connection
607
    @errors.cyclades.server_id
608
    @errors.cyclades.metadata
609
    def _run(self, server_id, key=''):
610
        self._print(
611
            self.client.get_server_metadata(int(server_id), key), print_dict)
612

    
613
    def main(self, server_id, key=''):
614
        super(self.__class__, self)._run()
615
        self._run(server_id=server_id, key=key)
616

    
617

    
618
@command(server_cmds)
619
class server_metadata_set(_init_cyclades, _optional_json):
620
    """Set / update server(VM) metadata
621
    Metadata should be given in key/value pairs in key=value format
622
    For example: /server metadata set <server id> key1=value1 key2=value2
623
    Old, unreferenced metadata will remain intact
624
    """
625

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

    
648
    def main(self, server_id, *key_equals_val):
649
        super(self.__class__, self)._run()
650
        self._run(server_id=server_id, keyvals=key_equals_val)
651

    
652

    
653
@command(server_cmds)
654
class server_metadata_delete(_init_cyclades, _optional_output_cmd):
655
    """Delete server (VM) metadata"""
656

    
657
    @errors.generic.all
658
    @errors.cyclades.connection
659
    @errors.cyclades.server_id
660
    @errors.cyclades.metadata
661
    def _run(self, server_id, key):
662
        self._optional_output(
663
            self.client.delete_server_metadata(int(server_id), key))
664

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

    
669

    
670
@command(server_cmds)
671
class server_stats(_init_cyclades, _optional_json):
672
    """Get server (VM) statistics"""
673

    
674
    @errors.generic.all
675
    @errors.cyclades.connection
676
    @errors.cyclades.server_id
677
    def _run(self, server_id):
678
        self._print(self.client.get_server_stats(int(server_id)), print_dict)
679

    
680
    def main(self, server_id):
681
        super(self.__class__, self)._run()
682
        self._run(server_id=server_id)
683

    
684

    
685
@command(server_cmds)
686
class server_wait(_init_cyclades, _server_wait):
687
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
688

    
689
    @errors.generic.all
690
    @errors.cyclades.connection
691
    @errors.cyclades.server_id
692
    def _run(self, server_id, currect_status):
693
        self._wait(server_id, currect_status)
694

    
695
    def main(self, server_id, currect_status='BUILD'):
696
        super(self.__class__, self)._run()
697
        self._run(server_id=server_id, currect_status=currect_status)
698

    
699

    
700
@command(flavor_cmds)
701
class flavor_list(_init_cyclades, _optional_json):
702
    """List available hardware flavors"""
703

    
704
    arguments = dict(
705
        detail=FlagArgument('show detailed output', ('-l', '--details')),
706
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
707
        more=FlagArgument(
708
            'output results in pages (-n to set items per page, default 10)',
709
            '--more'),
710
        enum=FlagArgument('Enumerate results', '--enumerate')
711
    )
712

    
713
    @errors.generic.all
714
    @errors.cyclades.connection
715
    def _run(self):
716
        flavors = self.client.list_flavors(self['detail'])
717
        if not (self['detail'] or self['json_output']):
718
            remove_from_items(flavors, 'links')
719
        pg_size = 10 if self['more'] and not self['limit'] else self['limit']
720
        self._print(
721
            flavors,
722
            with_redundancy=self['detail'],
723
            page_size=pg_size,
724
            with_enumeration=self['enum'])
725

    
726
    def main(self):
727
        super(self.__class__, self)._run()
728
        self._run()
729

    
730

    
731
@command(flavor_cmds)
732
class flavor_info(_init_cyclades, _optional_json):
733
    """Detailed information on a hardware flavor
734
    To get a list of available flavors and flavor ids, try /flavor list
735
    """
736

    
737
    @errors.generic.all
738
    @errors.cyclades.connection
739
    @errors.cyclades.flavor_id
740
    def _run(self, flavor_id):
741
        self._print(
742
            self.client.get_flavor_details(int(flavor_id)), print_dict)
743

    
744
    def main(self, flavor_id):
745
        super(self.__class__, self)._run()
746
        self._run(flavor_id=flavor_id)
747

    
748

    
749
@command(network_cmds)
750
class network_info(_init_cyclades, _optional_json):
751
    """Detailed information on a network
752
    To get a list of available networks and network ids, try /network list
753
    """
754

    
755
    @errors.generic.all
756
    @errors.cyclades.connection
757
    @errors.cyclades.network_id
758
    def _run(self, network_id):
759
        network = self.client.get_network_details(int(network_id))
760
        self._print(network, print_dict, exclude=('id'))
761

    
762
    def main(self, network_id):
763
        super(self.__class__, self)._run()
764
        self._run(network_id=network_id)
765

    
766

    
767
@command(network_cmds)
768
class network_list(_init_cyclades, _optional_json):
769
    """List networks"""
770

    
771
    arguments = dict(
772
        detail=FlagArgument('show detailed output', ('-l', '--details')),
773
        limit=IntArgument('limit # of listed networks', ('-n', '--number')),
774
        more=FlagArgument(
775
            'output results in pages (-n to set items per page, default 10)',
776
            '--more'),
777
        enum=FlagArgument('Enumerate results', '--enumerate')
778
    )
779

    
780
    @errors.generic.all
781
    @errors.cyclades.connection
782
    def _run(self):
783
        networks = self.client.list_networks(self['detail'])
784
        if not (self['detail'] or self['json_output']):
785
            remove_from_items(networks, 'links')
786
        kwargs = dict(with_enumeration=self['enum'])
787
        if self['more']:
788
            kwargs['page_size'] = self['limit'] or 10
789
        elif self['limit']:
790
            networks = networks[:self['limit']]
791
        self._print(networks, **kwargs)
792

    
793
    def main(self):
794
        super(self.__class__, self)._run()
795
        self._run()
796

    
797

    
798
@command(network_cmds)
799
class network_create(_init_cyclades, _optional_json, _network_wait):
800
    """Create an (unconnected) network"""
801

    
802
    arguments = dict(
803
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
804
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
805
        dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
806
        type=ValueArgument(
807
            'Valid network types are '
808
            'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
809
            '--with-type',
810
            default='MAC_FILTERED'),
811
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
812
    )
813

    
814
    @errors.generic.all
815
    @errors.cyclades.connection
816
    @errors.cyclades.network_max
817
    def _run(self, name):
818
        r = self.client.create_network(
819
            name,
820
            cidr=self['cidr'],
821
            gateway=self['gateway'],
822
            dhcp=self['dhcp'],
823
            type=self['type'])
824
        self._print(r, print_dict)
825

    
826
        if self['wait']:
827
            self._wait(r['id'], 'PENDING')
828

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

    
833

    
834
@command(network_cmds)
835
class network_rename(_init_cyclades, _optional_output_cmd):
836
    """Set the name of a network"""
837

    
838
    @errors.generic.all
839
    @errors.cyclades.connection
840
    @errors.cyclades.network_id
841
    def _run(self, network_id, new_name):
842
        self._optional_output(
843
                self.client.update_network_name(int(network_id), new_name))
844

    
845
    def main(self, network_id, new_name):
846
        super(self.__class__, self)._run()
847
        self._run(network_id=network_id, new_name=new_name)
848

    
849

    
850
@command(network_cmds)
851
class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
852
    """Delete a network"""
853

    
854
    arguments = dict(
855
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
856
    )
857

    
858
    @errors.generic.all
859
    @errors.cyclades.connection
860
    @errors.cyclades.network_id
861
    @errors.cyclades.network_in_use
862
    def _run(self, network_id):
863
        status = 'DELETED'
864
        if self['wait']:
865
            r = self.client.get_network_details(network_id)
866
            status = r['status']
867
            if status in ('DELETED', ):
868
                return
869

    
870
        r = self.client.delete_network(int(network_id))
871
        self._optional_output(r)
872

    
873
        if self['wait']:
874
            self._wait(network_id, status)
875

    
876
    def main(self, network_id):
877
        super(self.__class__, self)._run()
878
        self._run(network_id=network_id)
879

    
880

    
881
@command(network_cmds)
882
class network_connect(_init_cyclades, _optional_output_cmd):
883
    """Connect a server to a network"""
884

    
885
    @errors.generic.all
886
    @errors.cyclades.connection
887
    @errors.cyclades.server_id
888
    @errors.cyclades.network_id
889
    def _run(self, server_id, network_id):
890
        self._optional_output(
891
                self.client.connect_server(int(server_id), int(network_id)))
892

    
893
    def main(self, server_id, network_id):
894
        super(self.__class__, self)._run()
895
        self._run(server_id=server_id, network_id=network_id)
896

    
897

    
898
@command(network_cmds)
899
class network_disconnect(_init_cyclades):
900
    """Disconnect a nic that connects a server to a network
901
    Nic ids are listed as "attachments" in detailed network information
902
    To get detailed network information: /network info <network id>
903
    """
904

    
905
    @errors.cyclades.nic_format
906
    def _server_id_from_nic(self, nic_id):
907
        return nic_id.split('-')[1]
908

    
909
    @errors.generic.all
910
    @errors.cyclades.connection
911
    @errors.cyclades.server_id
912
    @errors.cyclades.nic_id
913
    def _run(self, nic_id, server_id):
914
        num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
915
        if not num_of_disconnected:
916
            raise ClientError(
917
                'Network Interface %s not found on server %s' % (
918
                    nic_id,
919
                    server_id),
920
                status=404)
921
        print('Disconnected %s connections' % num_of_disconnected)
922

    
923
    def main(self, nic_id):
924
        super(self.__class__, self)._run()
925
        server_id = self._server_id_from_nic(nic_id=nic_id)
926
        self._run(nic_id=nic_id, server_id=server_id)
927

    
928

    
929
@command(network_cmds)
930
class network_wait(_init_cyclades, _network_wait):
931
    """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
932

    
933
    @errors.generic.all
934
    @errors.cyclades.connection
935
    @errors.cyclades.network_id
936
    def _run(self, network_id, currect_status):
937
        self._wait(network_id, currect_status)
938

    
939
    def main(self, network_id, currect_status='PENDING'):
940
        super(self.__class__, self)._run()
941
        self._run(network_id=network_id, currect_status=currect_status)
942

    
943

    
944
@command(server_cmds)
945
class server_ip(_init_cyclades):
946
    """Manage floating IPs for the servers"""
947

    
948

    
949
@command(server_cmds)
950
class server_ip_pools(_init_cyclades, _optional_json):
951
    """List all floating pools of floating ips"""
952

    
953
    @errors.generic.all
954
    @errors.cyclades.connection
955
    def _run(self):
956
        r = self.client.get_floating_ip_pools()
957
        self._print(r if self['json_output'] else r['floating_ip_pools'])
958

    
959
    def main(self):
960
        super(self.__class__, self)._run()
961
        self._run()
962

    
963

    
964
@command(server_cmds)
965
class server_ip_list(_init_cyclades, _optional_json):
966
    """List all floating ips"""
967

    
968
    @errors.generic.all
969
    @errors.cyclades.connection
970
    def _run(self):
971
        r = self.client.get_floating_ips()
972
        self._print(r if self['json_output'] else r['floating_ips'])
973

    
974
    def main(self):
975
        super(self.__class__, self)._run()
976
        self._run()
977

    
978

    
979
@command(server_cmds)
980
class server_ip_info(_init_cyclades, _optional_json):
981
    """A floating IPs' details"""
982

    
983
    @errors.generic.all
984
    @errors.cyclades.connection
985
    def _run(self, ip):
986
        self._print(self.client.get_floating_ip(ip), print_dict)
987

    
988
    def main(self, ip):
989
        super(self.__class__, self)._run()
990
        self._run(ip=ip)
991

    
992

    
993
@command(server_cmds)
994
class server_ip_create(_init_cyclades, _optional_json):
995
    """Create a new floating IP"""
996

    
997
    arguments = dict(
998
        pool=ValueArgument('Source IP pool', ('--pool'), None)
999
    )
1000

    
1001
    @errors.generic.all
1002
    @errors.cyclades.connection
1003
    def _run(self, ip=None):
1004
        self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1005

    
1006
    def main(self, requested_address=None):
1007
        super(self.__class__, self)._run()
1008
        self._run(ip=requested_address)
1009

    
1010

    
1011
@command(server_cmds)
1012
class server_ip_delete(_init_cyclades, _optional_output_cmd):
1013
    """Delete a floating ip"""
1014

    
1015
    @errors.generic.all
1016
    @errors.cyclades.connection
1017
    def _run(self, ip):
1018
        self._optional_output(self.client.delete_floating_ip(ip))
1019

    
1020
    def main(self, ip):
1021
        super(self.__class__, self)._run()
1022
        self._run(ip=ip)
1023

    
1024

    
1025
@command(server_cmds)
1026
class server_ip_attach(_init_cyclades, _optional_output_cmd):
1027
    """Attach a floating ip to a server with server_id
1028
    """
1029

    
1030
    @errors.generic.all
1031
    @errors.cyclades.connection
1032
    @errors.cyclades.server_id
1033
    def _run(self, server_id, ip):
1034
        self._optional_output(self.client.attach_floating_ip(server_id, ip))
1035

    
1036
    def main(self, server_id, ip):
1037
        super(self.__class__, self)._run()
1038
        self._run(server_id=server_id, ip=ip)
1039

    
1040

    
1041
@command(server_cmds)
1042
class server_ip_detach(_init_cyclades, _optional_output_cmd):
1043
    """Detach floating IP from server
1044
    """
1045

    
1046
    @errors.generic.all
1047
    @errors.cyclades.connection
1048
    @errors.cyclades.server_id
1049
    def _run(self, server_id, ip):
1050
        self._optional_output(self.client.detach_floating_ip(server_id, ip))
1051

    
1052
    def main(self, server_id, ip):
1053
        super(self.__class__, self)._run()
1054
        self._run(server_id=server_id, ip=ip)