Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 89ea97e1

History | View | Annotate | Download (33.5 kB)

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

    
34
from kamaki.cli import command
35
from kamaki.cli.command_tree import CommandTree
36
from kamaki.cli.utils import (
37
    print_dict, remove_from_items, filter_dicts_by_dict)
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 _filtered_by_image(self, servers):
201
        iid = self['image_id']
202
        new_servers = []
203
        for srv in servers:
204
            if srv['image']['id'] == iid:
205
                new_servers.append(srv)
206
        return new_servers
207

    
208
    def _filtered_by_flavor(self, servers):
209
        fid = self['flavor_id']
210
        new_servers = []
211
        for srv in servers:
212
            if '%s' % srv['flavor']['id'] == '%s' % fid:
213
                new_servers.append(srv)
214
        return new_servers
215

    
216
    def _filtered_by_metadata(self, servers):
217
        new_servers = []
218
        for srv in servers:
219
            if not 'metadata' in srv:
220
                continue
221
            meta = [dict(srv['metadata'])]
222
            if self['meta']:
223
                meta = filter_dicts_by_dict(meta, self['meta'])
224
            if meta and self['meta_like']:
225
                meta = filter_dicts_by_dict(
226
                    meta, self['meta_like'], exact_match=False)
227
            if meta:
228
                new_servers.append(srv)
229
        return new_servers
230

    
231
    @errors.generic.all
232
    @errors.cyclades.connection
233
    @errors.cyclades.date
234
    def _run(self):
235
        withimage = bool(self['image_id'])
236
        withflavor = bool(self['flavor_id'])
237
        withmeta = bool(self['meta'] or self['meta_like'])
238
        detail = self['detail'] or withimage or withflavor or withmeta
239
        servers = self.client.list_servers(detail, self['since'])
240

    
241
        servers = self._filtered_by_name(servers)
242
        if withimage:
243
            servers = self._filtered_by_image(servers)
244
        if withflavor:
245
            servers = self._filtered_by_flavor(servers)
246
        if withmeta:
247
            servers = self._filtered_by_metadata(servers)
248

    
249
        if not (self['detail'] or self['json_output']):
250
            remove_from_items(servers, 'links')
251
        #if self['detail'] and not self['json_output']:
252
        #    servers = self._add_owner_name(servers)
253
        if detail and not self['detail']:
254
            for srv in servers:
255
                for key in set(srv).difference(self.PERMANENTS):
256
                    srv.pop(key)
257
        kwargs = dict(with_enumeration=self['enum'])
258
        if self['more']:
259
            kwargs['page_size'] = self['limit'] if self['limit'] else 10
260
        elif self['limit']:
261
            servers = servers[:self['limit']]
262
        self._print(servers, **kwargs)
263

    
264
    def main(self):
265
        super(self.__class__, self)._run()
266
        self._run()
267

    
268

    
269
@command(server_cmds)
270
class server_info(_init_cyclades, _optional_json):
271
    """Detailed information on a Virtual Machine
272
    Contains:
273
    - name, id, status, create/update dates
274
    - network interfaces
275
    - metadata (e.g. os, superuser) and diagnostics
276
    - hardware flavor and os image ids
277
    """
278

    
279
    @errors.generic.all
280
    @errors.cyclades.connection
281
    @errors.cyclades.server_id
282
    def _run(self, server_id):
283
        self._print(self.client.get_server_details(server_id), print_dict)
284

    
285
    def main(self, server_id):
286
        super(self.__class__, self)._run()
287
        self._run(server_id=server_id)
288

    
289

    
290
class PersonalityArgument(KeyValueArgument):
291
    @property
292
    def value(self):
293
        return self._value if hasattr(self, '_value') else []
294

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

    
323

    
324
@command(server_cmds)
325
class server_create(_init_cyclades, _optional_json, _server_wait):
326
    """Create a server (aka Virtual Machine)
327
    Parameters:
328
    - name: (single quoted text)
329
    - flavor id: Hardware flavor. Pick one from: /flavor list
330
    - image id: OS images. Pick one from: /image list
331
    """
332

    
333
    arguments = dict(
334
        personality=PersonalityArgument(
335
            (80 * ' ').join(howto_personality), ('-p', '--personality')),
336
        wait=FlagArgument('Wait server to build', ('-w', '--wait'))
337
    )
338

    
339
    @errors.generic.all
340
    @errors.cyclades.connection
341
    @errors.plankton.id
342
    @errors.cyclades.flavor_id
343
    def _run(self, name, flavor_id, image_id):
344
        r = self.client.create_server(
345
            name, int(flavor_id), image_id, self['personality'])
346
        self._print(r, print_dict)
347
        if self['wait']:
348
            self._wait(r['id'], r['status'])
349

    
350
    def main(self, name, flavor_id, image_id):
351
        super(self.__class__, self)._run()
352
        self._run(name=name, flavor_id=flavor_id, image_id=image_id)
353

    
354

    
355
@command(server_cmds)
356
class server_rename(_init_cyclades, _optional_output_cmd):
357
    """Set/update a server (VM) name
358
    VM names are not unique, therefore multiple servers may share the same name
359
    """
360

    
361
    @errors.generic.all
362
    @errors.cyclades.connection
363
    @errors.cyclades.server_id
364
    def _run(self, server_id, new_name):
365
        self._optional_output(
366
            self.client.update_server_name(int(server_id), new_name))
367

    
368
    def main(self, server_id, new_name):
369
        super(self.__class__, self)._run()
370
        self._run(server_id=server_id, new_name=new_name)
371

    
372

    
373
@command(server_cmds)
374
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
375
    """Delete a server (VM)"""
376

    
377
    arguments = dict(
378
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
379
    )
380

    
381
    @errors.generic.all
382
    @errors.cyclades.connection
383
    @errors.cyclades.server_id
384
    def _run(self, server_id):
385
            status = 'DELETED'
386
            if self['wait']:
387
                details = self.client.get_server_details(server_id)
388
                status = details['status']
389

    
390
            r = self.client.delete_server(int(server_id))
391
            self._optional_output(r)
392

    
393
            if self['wait']:
394
                self._wait(server_id, status)
395

    
396
    def main(self, server_id):
397
        super(self.__class__, self)._run()
398
        self._run(server_id=server_id)
399

    
400

    
401
@command(server_cmds)
402
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
403
    """Reboot a server (VM)"""
404

    
405
    arguments = dict(
406
        hard=FlagArgument('perform a hard reboot', ('-f', '--force')),
407
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
408
    )
409

    
410
    @errors.generic.all
411
    @errors.cyclades.connection
412
    @errors.cyclades.server_id
413
    def _run(self, server_id):
414
        r = self.client.reboot_server(int(server_id), self['hard'])
415
        self._optional_output(r)
416

    
417
        if self['wait']:
418
            self._wait(server_id, 'REBOOT')
419

    
420
    def main(self, server_id):
421
        super(self.__class__, self)._run()
422
        self._run(server_id=server_id)
423

    
424

    
425
@command(server_cmds)
426
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
427
    """Start an existing server (VM)"""
428

    
429
    arguments = dict(
430
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
431
    )
432

    
433
    @errors.generic.all
434
    @errors.cyclades.connection
435
    @errors.cyclades.server_id
436
    def _run(self, server_id):
437
        status = 'ACTIVE'
438
        if self['wait']:
439
            details = self.client.get_server_details(server_id)
440
            status = details['status']
441
            if status in ('ACTIVE', ):
442
                return
443

    
444
        r = self.client.start_server(int(server_id))
445
        self._optional_output(r)
446

    
447
        if self['wait']:
448
            self._wait(server_id, status)
449

    
450
    def main(self, server_id):
451
        super(self.__class__, self)._run()
452
        self._run(server_id=server_id)
453

    
454

    
455
@command(server_cmds)
456
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
457
    """Shutdown an active server (VM)"""
458

    
459
    arguments = dict(
460
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
461
    )
462

    
463
    @errors.generic.all
464
    @errors.cyclades.connection
465
    @errors.cyclades.server_id
466
    def _run(self, server_id):
467
        status = 'STOPPED'
468
        if self['wait']:
469
            details = self.client.get_server_details(server_id)
470
            status = details['status']
471
            if status in ('STOPPED', ):
472
                return
473

    
474
        r = self.client.shutdown_server(int(server_id))
475
        self._optional_output(r)
476

    
477
        if self['wait']:
478
            self._wait(server_id, status)
479

    
480
    def main(self, server_id):
481
        super(self.__class__, self)._run()
482
        self._run(server_id=server_id)
483

    
484

    
485
@command(server_cmds)
486
class server_console(_init_cyclades, _optional_json):
487
    """Get a VNC console to access an existing server (VM)
488
    Console connection information provided (at least):
489
    - host: (url or address) a VNC host
490
    - port: (int) the gateway to enter VM on host
491
    - password: for VNC authorization
492
    """
493

    
494
    @errors.generic.all
495
    @errors.cyclades.connection
496
    @errors.cyclades.server_id
497
    def _run(self, server_id):
498
        self._print(
499
            self.client.get_server_console(int(server_id)), print_dict)
500

    
501
    def main(self, server_id):
502
        super(self.__class__, self)._run()
503
        self._run(server_id=server_id)
504

    
505

    
506
@command(server_cmds)
507
class server_resize(_init_cyclades, _optional_output_cmd):
508
    """Set a different flavor for an existing server
509
    To get server ids and flavor ids:
510
    /server list
511
    /flavor list
512
    """
513

    
514
    @errors.generic.all
515
    @errors.cyclades.connection
516
    @errors.cyclades.server_id
517
    @errors.cyclades.flavor_id
518
    def _run(self, server_id, flavor_id):
519
        self._optional_output(self.client.resize_server(server_id, flavor_id))
520

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

    
525

    
526
@command(server_cmds)
527
class server_firewall(_init_cyclades):
528
    """Manage server (VM) firewall profiles for public networks"""
529

    
530

    
531
@command(server_cmds)
532
class server_firewall_set(_init_cyclades, _optional_output_cmd):
533
    """Set the server (VM) firewall profile on VMs public network
534
    Values for profile:
535
    - DISABLED: Shutdown firewall
536
    - ENABLED: Firewall in normal mode
537
    - PROTECTED: Firewall in secure mode
538
    """
539

    
540
    @errors.generic.all
541
    @errors.cyclades.connection
542
    @errors.cyclades.server_id
543
    @errors.cyclades.firewall
544
    def _run(self, server_id, profile):
545
        self._optional_output(self.client.set_firewall_profile(
546
            server_id=int(server_id), profile=('%s' % profile).upper()))
547

    
548
    def main(self, server_id, profile):
549
        super(self.__class__, self)._run()
550
        self._run(server_id=server_id, profile=profile)
551

    
552

    
553
@command(server_cmds)
554
class server_firewall_get(_init_cyclades):
555
    """Get the server (VM) firewall profile for its public network"""
556

    
557
    @errors.generic.all
558
    @errors.cyclades.connection
559
    @errors.cyclades.server_id
560
    def _run(self, server_id):
561
        print(self.client.get_firewall_profile(server_id))
562

    
563
    def main(self, server_id):
564
        super(self.__class__, self)._run()
565
        self._run(server_id=server_id)
566

    
567

    
568
@command(server_cmds)
569
class server_addr(_init_cyclades, _optional_json):
570
    """List the addresses of all network interfaces on a server (VM)"""
571

    
572
    arguments = dict(
573
        enum=FlagArgument('Enumerate results', '--enumerate')
574
    )
575

    
576
    @errors.generic.all
577
    @errors.cyclades.connection
578
    @errors.cyclades.server_id
579
    def _run(self, server_id):
580
        reply = self.client.list_server_nics(int(server_id))
581
        self._print(
582
            reply, with_enumeration=self['enum'] and len(reply) > 1)
583

    
584
    def main(self, server_id):
585
        super(self.__class__, self)._run()
586
        self._run(server_id=server_id)
587

    
588

    
589
@command(server_cmds)
590
class server_metadata(_init_cyclades):
591
    """Manage Server metadata (key:value pairs of server attributes)"""
592

    
593

    
594
@command(server_cmds)
595
class server_metadata_list(_init_cyclades, _optional_json):
596
    """Get server metadata"""
597

    
598
    @errors.generic.all
599
    @errors.cyclades.connection
600
    @errors.cyclades.server_id
601
    @errors.cyclades.metadata
602
    def _run(self, server_id, key=''):
603
        self._print(
604
            self.client.get_server_metadata(int(server_id), key), print_dict)
605

    
606
    def main(self, server_id, key=''):
607
        super(self.__class__, self)._run()
608
        self._run(server_id=server_id, key=key)
609

    
610

    
611
@command(server_cmds)
612
class server_metadata_set(_init_cyclades, _optional_json):
613
    """Set / update server(VM) metadata
614
    Metadata should be given in key/value pairs in key=value format
615
    For example: /server metadata set <server id> key1=value1 key2=value2
616
    Old, unreferenced metadata will remain intact
617
    """
618

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

    
641
    def main(self, server_id, *key_equals_val):
642
        super(self.__class__, self)._run()
643
        self._run(server_id=server_id, keyvals=key_equals_val)
644

    
645

    
646
@command(server_cmds)
647
class server_metadata_delete(_init_cyclades, _optional_output_cmd):
648
    """Delete server (VM) metadata"""
649

    
650
    @errors.generic.all
651
    @errors.cyclades.connection
652
    @errors.cyclades.server_id
653
    @errors.cyclades.metadata
654
    def _run(self, server_id, key):
655
        self._optional_output(
656
            self.client.delete_server_metadata(int(server_id), key))
657

    
658
    def main(self, server_id, key):
659
        super(self.__class__, self)._run()
660
        self._run(server_id=server_id, key=key)
661

    
662

    
663
@command(server_cmds)
664
class server_stats(_init_cyclades, _optional_json):
665
    """Get server (VM) statistics"""
666

    
667
    @errors.generic.all
668
    @errors.cyclades.connection
669
    @errors.cyclades.server_id
670
    def _run(self, server_id):
671
        self._print(self.client.get_server_stats(int(server_id)), print_dict)
672

    
673
    def main(self, server_id):
674
        super(self.__class__, self)._run()
675
        self._run(server_id=server_id)
676

    
677

    
678
@command(server_cmds)
679
class server_wait(_init_cyclades, _server_wait):
680
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
681

    
682
    @errors.generic.all
683
    @errors.cyclades.connection
684
    @errors.cyclades.server_id
685
    def _run(self, server_id, currect_status):
686
        self._wait(server_id, currect_status)
687

    
688
    def main(self, server_id, currect_status='BUILD'):
689
        super(self.__class__, self)._run()
690
        self._run(server_id=server_id, currect_status=currect_status)
691

    
692

    
693
@command(flavor_cmds)
694
class flavor_list(_init_cyclades, _optional_json):
695
    """List available hardware flavors"""
696

    
697
    arguments = dict(
698
        detail=FlagArgument('show detailed output', ('-l', '--details')),
699
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
700
        more=FlagArgument(
701
            'output results in pages (-n to set items per page, default 10)',
702
            '--more'),
703
        enum=FlagArgument('Enumerate results', '--enumerate')
704
    )
705

    
706
    @errors.generic.all
707
    @errors.cyclades.connection
708
    def _run(self):
709
        flavors = self.client.list_flavors(self['detail'])
710
        if not (self['detail'] or self['json_output']):
711
            remove_from_items(flavors, 'links')
712
        pg_size = 10 if self['more'] and not self['limit'] else self['limit']
713
        self._print(
714
            flavors,
715
            with_redundancy=self['detail'],
716
            page_size=pg_size,
717
            with_enumeration=self['enum'])
718

    
719
    def main(self):
720
        super(self.__class__, self)._run()
721
        self._run()
722

    
723

    
724
@command(flavor_cmds)
725
class flavor_info(_init_cyclades, _optional_json):
726
    """Detailed information on a hardware flavor
727
    To get a list of available flavors and flavor ids, try /flavor list
728
    """
729

    
730
    @errors.generic.all
731
    @errors.cyclades.connection
732
    @errors.cyclades.flavor_id
733
    def _run(self, flavor_id):
734
        self._print(
735
            self.client.get_flavor_details(int(flavor_id)), print_dict)
736

    
737
    def main(self, flavor_id):
738
        super(self.__class__, self)._run()
739
        self._run(flavor_id=flavor_id)
740

    
741

    
742
@command(network_cmds)
743
class network_info(_init_cyclades, _optional_json):
744
    """Detailed information on a network
745
    To get a list of available networks and network ids, try /network list
746
    """
747

    
748
    @errors.generic.all
749
    @errors.cyclades.connection
750
    @errors.cyclades.network_id
751
    def _run(self, network_id):
752
        network = self.client.get_network_details(int(network_id))
753
        self._print(network, print_dict, exclude=('id'))
754

    
755
    def main(self, network_id):
756
        super(self.__class__, self)._run()
757
        self._run(network_id=network_id)
758

    
759

    
760
@command(network_cmds)
761
class network_list(_init_cyclades, _optional_json):
762
    """List networks"""
763

    
764
    arguments = dict(
765
        detail=FlagArgument('show detailed output', ('-l', '--details')),
766
        limit=IntArgument('limit # of listed networks', ('-n', '--number')),
767
        more=FlagArgument(
768
            'output results in pages (-n to set items per page, default 10)',
769
            '--more'),
770
        enum=FlagArgument('Enumerate results', '--enumerate')
771
    )
772

    
773
    @errors.generic.all
774
    @errors.cyclades.connection
775
    def _run(self):
776
        networks = self.client.list_networks(self['detail'])
777
        if not (self['detail'] or self['json_output']):
778
            remove_from_items(networks, 'links')
779
        kwargs = dict(with_enumeration=self['enum'])
780
        if self['more']:
781
            kwargs['page_size'] = self['limit'] or 10
782
        elif self['limit']:
783
            networks = networks[:self['limit']]
784
        self._print(networks, **kwargs)
785

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

    
790

    
791
@command(network_cmds)
792
class network_create(_init_cyclades, _optional_json, _network_wait):
793
    """Create an (unconnected) network"""
794

    
795
    arguments = dict(
796
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
797
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
798
        dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
799
        type=ValueArgument(
800
            'Valid network types are '
801
            'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
802
            '--with-type',
803
            default='MAC_FILTERED'),
804
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
805
    )
806

    
807
    @errors.generic.all
808
    @errors.cyclades.connection
809
    @errors.cyclades.network_max
810
    def _run(self, name):
811
        r = self.client.create_network(
812
            name,
813
            cidr=self['cidr'],
814
            gateway=self['gateway'],
815
            dhcp=self['dhcp'],
816
            type=self['type'])
817
        self._print(r, print_dict)
818

    
819
        if self['wait']:
820
            self._wait(r['id'], 'PENDING')
821

    
822
    def main(self, name):
823
        super(self.__class__, self)._run()
824
        self._run(name)
825

    
826

    
827
@command(network_cmds)
828
class network_rename(_init_cyclades, _optional_output_cmd):
829
    """Set the name of a network"""
830

    
831
    @errors.generic.all
832
    @errors.cyclades.connection
833
    @errors.cyclades.network_id
834
    def _run(self, network_id, new_name):
835
        self._optional_output(
836
                self.client.update_network_name(int(network_id), new_name))
837

    
838
    def main(self, network_id, new_name):
839
        super(self.__class__, self)._run()
840
        self._run(network_id=network_id, new_name=new_name)
841

    
842

    
843
@command(network_cmds)
844
class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
845
    """Delete a network"""
846

    
847
    arguments = dict(
848
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
849
    )
850

    
851
    @errors.generic.all
852
    @errors.cyclades.connection
853
    @errors.cyclades.network_id
854
    @errors.cyclades.network_in_use
855
    def _run(self, network_id):
856
        status = 'DELETED'
857
        if self['wait']:
858
            r = self.client.get_network_details(network_id)
859
            status = r['status']
860
            if status in ('DELETED', ):
861
                return
862

    
863
        r = self.client.delete_network(int(network_id))
864
        self._optional_output(r)
865

    
866
        if self['wait']:
867
            self._wait(network_id, status)
868

    
869
    def main(self, network_id):
870
        super(self.__class__, self)._run()
871
        self._run(network_id=network_id)
872

    
873

    
874
@command(network_cmds)
875
class network_connect(_init_cyclades, _optional_output_cmd):
876
    """Connect a server to a network"""
877

    
878
    @errors.generic.all
879
    @errors.cyclades.connection
880
    @errors.cyclades.server_id
881
    @errors.cyclades.network_id
882
    def _run(self, server_id, network_id):
883
        self._optional_output(
884
                self.client.connect_server(int(server_id), int(network_id)))
885

    
886
    def main(self, server_id, network_id):
887
        super(self.__class__, self)._run()
888
        self._run(server_id=server_id, network_id=network_id)
889

    
890

    
891
@command(network_cmds)
892
class network_disconnect(_init_cyclades):
893
    """Disconnect a nic that connects a server to a network
894
    Nic ids are listed as "attachments" in detailed network information
895
    To get detailed network information: /network info <network id>
896
    """
897

    
898
    @errors.cyclades.nic_format
899
    def _server_id_from_nic(self, nic_id):
900
        return nic_id.split('-')[1]
901

    
902
    @errors.generic.all
903
    @errors.cyclades.connection
904
    @errors.cyclades.server_id
905
    @errors.cyclades.nic_id
906
    def _run(self, nic_id, server_id):
907
        num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
908
        if not num_of_disconnected:
909
            raise ClientError(
910
                'Network Interface %s not found on server %s' % (
911
                    nic_id,
912
                    server_id),
913
                status=404)
914
        print('Disconnected %s connections' % num_of_disconnected)
915

    
916
    def main(self, nic_id):
917
        super(self.__class__, self)._run()
918
        server_id = self._server_id_from_nic(nic_id=nic_id)
919
        self._run(nic_id=nic_id, server_id=server_id)
920

    
921

    
922
@command(network_cmds)
923
class network_wait(_init_cyclades, _network_wait):
924
    """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
925

    
926
    @errors.generic.all
927
    @errors.cyclades.connection
928
    @errors.cyclades.network_id
929
    def _run(self, network_id, currect_status):
930
        self._wait(network_id, currect_status)
931

    
932
    def main(self, network_id, currect_status='PENDING'):
933
        super(self.__class__, self)._run()
934
        self._run(network_id=network_id, currect_status=currect_status)
935

    
936

    
937
@command(server_cmds)
938
class server_ip(_init_cyclades):
939
    """Manage floating IPs for the servers"""
940

    
941

    
942
@command(server_cmds)
943
class server_ip_pools(_init_cyclades, _optional_json):
944
    """List all floating pools of floating ips"""
945

    
946
    @errors.generic.all
947
    @errors.cyclades.connection
948
    def _run(self):
949
        r = self.client.get_floating_ip_pools()
950
        self._print(r if self['json_output'] else r['floating_ip_pools'])
951

    
952
    def main(self):
953
        super(self.__class__, self)._run()
954
        self._run()
955

    
956

    
957
@command(server_cmds)
958
class server_ip_list(_init_cyclades, _optional_json):
959
    """List all floating ips"""
960

    
961
    @errors.generic.all
962
    @errors.cyclades.connection
963
    def _run(self):
964
        r = self.client.get_floating_ips()
965
        self._print(r if self['json_output'] else r['floating_ips'])
966

    
967
    def main(self):
968
        super(self.__class__, self)._run()
969
        self._run()
970

    
971

    
972
@command(server_cmds)
973
class server_ip_info(_init_cyclades, _optional_json):
974
    """A floating IPs' details"""
975

    
976
    @errors.generic.all
977
    @errors.cyclades.connection
978
    def _run(self, ip):
979
        self._print(self.client.get_floating_ip(ip), print_dict)
980

    
981
    def main(self, ip):
982
        super(self.__class__, self)._run()
983
        self._run(ip=ip)
984

    
985

    
986
@command(server_cmds)
987
class server_ip_create(_init_cyclades, _optional_json):
988
    """Create a new floating IP"""
989

    
990
    arguments = dict(
991
        pool=ValueArgument('Source IP pool', ('--pool'), None)
992
    )
993

    
994
    @errors.generic.all
995
    @errors.cyclades.connection
996
    def _run(self, ip=None):
997
        self._print([self.client.alloc_floating_ip(self['pool'], ip)])
998

    
999
    def main(self, requested_address=None):
1000
        super(self.__class__, self)._run()
1001
        self._run(ip=requested_address)
1002

    
1003

    
1004
@command(server_cmds)
1005
class server_ip_delete(_init_cyclades, _optional_output_cmd):
1006
    """Delete a floating ip"""
1007

    
1008
    @errors.generic.all
1009
    @errors.cyclades.connection
1010
    def _run(self, ip):
1011
        self._optional_output(self.client.delete_floating_ip(ip))
1012

    
1013
    def main(self, ip):
1014
        super(self.__class__, self)._run()
1015
        self._run(ip=ip)
1016

    
1017

    
1018
@command(server_cmds)
1019
class server_ip_attach(_init_cyclades, _optional_output_cmd):
1020
    """Attach a floating ip to a server with server_id
1021
    """
1022

    
1023
    @errors.generic.all
1024
    @errors.cyclades.connection
1025
    @errors.cyclades.server_id
1026
    def _run(self, server_id, ip):
1027
        self._optional_output(self.client.attach_floating_ip(server_id, ip))
1028

    
1029
    def main(self, server_id, ip):
1030
        super(self.__class__, self)._run()
1031
        self._run(server_id=server_id, ip=ip)
1032

    
1033

    
1034
@command(server_cmds)
1035
class server_ip_detach(_init_cyclades, _optional_output_cmd):
1036
    """Detach floating IP from server
1037
    """
1038

    
1039
    @errors.generic.all
1040
    @errors.cyclades.connection
1041
    @errors.cyclades.server_id
1042
    def _run(self, server_id, ip):
1043
        self._optional_output(self.client.detach_floating_ip(server_id, ip))
1044

    
1045
    def main(self, server_id, ip):
1046
        super(self.__class__, self)._run()
1047
        self._run(server_id=server_id, ip=ip)