Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 9d2f656a

History | View | Annotate | Download (33.9 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(list(set(
202
                [srv['user_id'] for srv in servers] +
203
                [srv['tenant_id'] for srv in servers])))
204
        for srv in servers:
205
            srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
206
            srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
207
        return servers
208

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

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

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

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

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

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

    
273
    def main(self):
274
        super(self.__class__, self)._run()
275
        self._run()
276

    
277

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

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

    
294
    def main(self, server_id):
295
        super(self.__class__, self)._run()
296
        self._run(server_id=server_id)
297

    
298

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

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

    
332

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

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

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

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

    
363

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

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

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

    
381

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

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

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

    
399
            r = self.client.delete_server(int(server_id))
400
            self._optional_output(r)
401

    
402
            if self['wait']:
403
                self._wait(server_id, status)
404

    
405
    def main(self, server_id):
406
        super(self.__class__, self)._run()
407
        self._run(server_id=server_id)
408

    
409

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

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

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

    
426
        if self['wait']:
427
            self._wait(server_id, 'REBOOT')
428

    
429
    def main(self, server_id):
430
        super(self.__class__, self)._run()
431
        self._run(server_id=server_id)
432

    
433

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

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

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

    
453
        r = self.client.start_server(int(server_id))
454
        self._optional_output(r)
455

    
456
        if self['wait']:
457
            self._wait(server_id, status)
458

    
459
    def main(self, server_id):
460
        super(self.__class__, self)._run()
461
        self._run(server_id=server_id)
462

    
463

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

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

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

    
483
        r = self.client.shutdown_server(int(server_id))
484
        self._optional_output(r)
485

    
486
        if self['wait']:
487
            self._wait(server_id, status)
488

    
489
    def main(self, server_id):
490
        super(self.__class__, self)._run()
491
        self._run(server_id=server_id)
492

    
493

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

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

    
510
    def main(self, server_id):
511
        super(self.__class__, self)._run()
512
        self._run(server_id=server_id)
513

    
514

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

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

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

    
534

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

    
539

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

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

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

    
561

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

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

    
572
    def main(self, server_id):
573
        super(self.__class__, self)._run()
574
        self._run(server_id=server_id)
575

    
576

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

    
581
    arguments = dict(
582
        enum=FlagArgument('Enumerate results', '--enumerate')
583
    )
584

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

    
593
    def main(self, server_id):
594
        super(self.__class__, self)._run()
595
        self._run(server_id=server_id)
596

    
597

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

    
602

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

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

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

    
619

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

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

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

    
654

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

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

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

    
671

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

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

    
682
    def main(self, server_id):
683
        super(self.__class__, self)._run()
684
        self._run(server_id=server_id)
685

    
686

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

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

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

    
701

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

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

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

    
728
    def main(self):
729
        super(self.__class__, self)._run()
730
        self._run()
731

    
732

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

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

    
746
    def main(self, flavor_id):
747
        super(self.__class__, self)._run()
748
        self._run(flavor_id=flavor_id)
749

    
750

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

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

    
764
    def main(self, network_id):
765
        super(self.__class__, self)._run()
766
        self._run(network_id=network_id)
767

    
768

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

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

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

    
795
    def main(self):
796
        super(self.__class__, self)._run()
797
        self._run()
798

    
799

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

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

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

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

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

    
835

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

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

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

    
851

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

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

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

    
872
        r = self.client.delete_network(int(network_id))
873
        self._optional_output(r)
874

    
875
        if self['wait']:
876
            self._wait(network_id, status)
877

    
878
    def main(self, network_id):
879
        super(self.__class__, self)._run()
880
        self._run(network_id=network_id)
881

    
882

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

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

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

    
899

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

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

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

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

    
930

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

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

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

    
945

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

    
950

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

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

    
961
    def main(self):
962
        super(self.__class__, self)._run()
963
        self._run()
964

    
965

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

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

    
976
    def main(self):
977
        super(self.__class__, self)._run()
978
        self._run()
979

    
980

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

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

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

    
994

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

    
999
    arguments = dict(
1000
        pool=ValueArgument('Source IP pool', ('--pool'), None)
1001
    )
1002

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

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

    
1012

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

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

    
1022
    def main(self, ip):
1023
        super(self.__class__, self)._run()
1024
        self._run(ip=ip)
1025

    
1026

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

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

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

    
1042

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

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

    
1054
    def main(self, server_id, ip):
1055
        super(self.__class__, self)._run()
1056
        self._run(server_id=server_id, ip=ip)