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

34
from kamaki.cli import command
35
from kamaki.cli.command_tree import CommandTree
36
37
from kamaki.cli.utils import (
    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
Stavros Sachtouris's avatar
Stavros Sachtouris committed
46
47
48
from os.path import exists


49
50
51
server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
network_cmds = CommandTree('network', 'Cyclades/Compute API network commands')
52
_commands = [server_cmds, flavor_cmds, network_cmds]
Stavros Sachtouris's avatar
Stavros Sachtouris committed
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
60
howto_personality = [
    'Defines a file to be injected to VMs personality.',
61
    'Personality value syntax: PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
62
63
64
65
66
67
    '  PATH: of local file to be injected',
    '  SERVER_PATH: destination location inside server Image',
    '  OWNER: user id of destination file owner',
    '  GROUP: group id or name to own destination file',
    '  MODEL: permition in octal (e.g. 0777 or o+rwx)']

68

69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
class _server_wait(object):

    wait_arguments = dict(
        progress_bar=ProgressBarArgument(
            'do not show progress bar',
            ('-N', '--no-progress-bar'),
            False
        )
    )

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

        try:
            new_mode = self.client.wait_server(
                server_id,
                currect_status,
                wait_cb=wait_cb)
        except Exception:
            raise
        finally:
            self._safe_progress_bar_finish(progress_bar)
        if new_mode:
            print('Server %s is now in %s mode' % (server_id, new_mode))
        else:
            raiseCLIError(None, 'Time out')


98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
class _network_wait(object):

    wait_arguments = dict(
        progress_bar=ProgressBarArgument(
            'do not show progress bar',
            ('-N', '--no-progress-bar'),
            False
        )
    )

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

        try:
            new_mode = self.client.wait_network(
                net_id,
                currect_status,
                wait_cb=wait_cb)
        except Exception:
            raise
        finally:
            self._safe_progress_bar_finish(progress_bar)
        if new_mode:
            print('Network %s is now in %s mode' % (net_id, new_mode))
        else:
            raiseCLIError(None, 'Time out')


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

153
154
    def main(self):
        self._run()
Stavros Sachtouris's avatar
Stavros Sachtouris committed
155

Stavros Sachtouris's avatar
Stavros Sachtouris committed
156

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

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

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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
        enum=FlagArgument('Enumerate results', '--enumerate'),
        name=ValueArgument('filter by name', '--name'),
        name_pref=ValueArgument(
            'filter by name prefix (case insensitive)', '--name-prefix'),
        name_suff=ValueArgument(
            'filter by name suffix (case insensitive)', '--name-suffix'),
        name_like=ValueArgument(
            'print only if name contains this (case insensitive)',
            '--name-like'),
        flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
        image_id=ValueArgument('filter by image id', ('--image-id')),
        meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
        meta_like=KeyValueArgument(
            'print only if in key=value, the value is part of actual value',
            ('--metadata-like')),
189
190
    )

191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
    def _filtered_by_name(self, servers):
        if self['name']:
            servers = filter_dicts_by_dict(servers, dict(name=self['name']))
        np, ns, nl = self['name_pref'], self['name_suff'], self['name_like']
        return [img for img in servers if (
            (not np) or img['name'].lower().startswith(np.lower())) and (
            (not ns) or img['name'].lower().endswith(ns.lower())) and (
            (not nl) or nl.lower() in img['name'].lower())]

    def _filtered_by_image(self, servers):
        iid = self['image_id']
        new_servers = []
        for srv in servers:
            if srv['image']['id'] == iid:
                new_servers.append(srv)
        return new_servers

    def _filtered_by_flavor(self, servers):
        fid = self['flavor_id']
        new_servers = []
        for srv in servers:
            if '%s' % srv['flavor']['id'] == '%s' % fid:
                new_servers.append(srv)
        return new_servers

    def _filtered_by_metadata(self, servers):
        new_servers = []
        for srv in servers:
            if not 'metadata' in srv:
                continue
            meta = [dict(srv['metadata'])]
            if self['meta']:
                meta = filter_dicts_by_dict(meta, self['meta'])
            if meta and self['meta_like']:
                meta = filter_dicts_by_dict(
                    meta, self['meta_like'], exact_match=False)
            if meta:
                new_servers.append(srv)
        return new_servers

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

        servers = self._filtered_by_name(servers)
        if withimage:
            servers = self._filtered_by_image(servers)
        if withflavor:
            servers = self._filtered_by_flavor(servers)
        if withmeta:
            servers = self._filtered_by_metadata(servers)

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

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
268

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

279
280
    @errors.generic.all
    @errors.cyclades.connection
Stavros Sachtouris's avatar
Stavros Sachtouris committed
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
286
287
288
    def main(self, server_id):
        super(self.__class__, self)._run()
        self._run(server_id=server_id)

Stavros Sachtouris's avatar
Stavros Sachtouris committed
289

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

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
323

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

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

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

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
354

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

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

368
    def main(self, server_id, new_name):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
369
370
        super(self.__class__, self)._run()
        self._run(server_id=server_id, new_name=new_name)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
371

372

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

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
381
382
383
384
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
385
386
387
388
389
390
391
392
393
394
            status = 'DELETED'
            if self['wait']:
                details = self.client.get_server_details(server_id)
                status = details['status']

            r = self.client.delete_server(int(server_id))
            self._optional_output(r)

            if self['wait']:
                self._wait(server_id, status)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
395
396
397
398

    def main(self, server_id):
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
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
407
        hard=FlagArgument('perform a hard reboot', ('-f', '--force')),
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
408
    )
409

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

        if self['wait']:
            self._wait(server_id, 'REBOOT')
Stavros Sachtouris's avatar
Stavros Sachtouris committed
419

420
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
421
422
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
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
430
431
432
    arguments = dict(
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
433
434
435
436
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
437
438
439
440
441
442
443
444
445
446
447
448
        status = 'ACTIVE'
        if self['wait']:
            details = self.client.get_server_details(server_id)
            status = details['status']
            if status in ('ACTIVE', ):
                return

        r = self.client.start_server(int(server_id))
        self._optional_output(r)

        if self['wait']:
            self._wait(server_id, status)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
449

450
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
451
452
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
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
460
461
462
    arguments = dict(
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
463
464
465
466
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
467
468
469
470
471
472
473
474
475
476
477
478
        status = 'STOPPED'
        if self['wait']:
            details = self.client.get_server_details(server_id)
            status = details['status']
            if status in ('STOPPED', ):
                return

        r = self.client.shutdown_server(int(server_id))
        self._optional_output(r)

        if self['wait']:
            self._wait(server_id, status)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
479

480
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
481
482
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
483

484

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

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

501
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
502
503
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
504

Stavros Sachtouris's avatar
Stavros Sachtouris committed
505

506
507
508
509
@command(server_cmds)
class server_resize(_init_cyclades, _optional_output_cmd):
    """Set a different flavor for an existing server
    To get server ids and flavor ids:
510
511
    /server list
    /flavor list
512
513
514
515
516
517
518
519
520
521
522
523
524
525
    """

    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    @errors.cyclades.flavor_id
    def _run(self, server_id, flavor_id):
        self._optional_output(self.client.resize_server(server_id, flavor_id))

    def main(self, server_id, flavor_id):
        super(self.__class__, self)._run()
        self._run(server_id=server_id, flavor_id=flavor_id)


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


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

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

548
    def main(self, server_id, profile):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
549
550
        super(self.__class__, self)._run()
        self._run(server_id=server_id, profile=profile)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
551

552

553
@command(server_cmds)
554
555
556
557
558
559
560
561
562
563
564
565
566
567
class server_firewall_get(_init_cyclades):
    """Get the server (VM) firewall profile for its public network"""

    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
        print(self.client.get_firewall_profile(server_id))

    def main(self, server_id):
        super(self.__class__, self)._run()
        self._run(server_id=server_id)


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
    )

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
584
585
586
587
    def main(self, server_id):
        super(self.__class__, self)._run()
        self._run(server_id=server_id)

Stavros Sachtouris's avatar
Stavros Sachtouris committed
588

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


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

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

606
    def main(self, server_id, key=''):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
607
608
        super(self.__class__, self)._run()
        self._run(server_id=server_id, key=key)
609

Stavros Sachtouris's avatar
Stavros Sachtouris committed
610

611
@command(server_cmds)
612
class server_metadata_set(_init_cyclades, _optional_json):
613
614
    """Set / update server(VM) metadata
    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

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

    def main(self, server_id, *key_equals_val):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
642
        super(self.__class__, self)._run()
643
        self._run(server_id=server_id, keyvals=key_equals_val)
644

Stavros Sachtouris's avatar
Stavros Sachtouris committed
645

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

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

658
    def main(self, server_id, key):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
659
660
        super(self.__class__, self)._run()
        self._run(server_id=server_id, key=key)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
661

662

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

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

673
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
674
675
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
676

Stavros Sachtouris's avatar
Stavros Sachtouris committed
677

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
682
683
684
685
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id, currect_status):
686
        self._wait(server_id, currect_status)
687

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

692

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

697
    arguments = dict(
698
699
        detail=FlagArgument('show detailed output', ('-l', '--details')),
        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
703
            '--more'),
        enum=FlagArgument('Enumerate results', '--enumerate')
704
    )
705

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

719
    def main(self):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
720
721
        super(self.__class__, self)._run()
        self._run()
722

Stavros Sachtouris's avatar
Stavros Sachtouris committed
723

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

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
737
738
739
740
    def main(self, flavor_id):
        super(self.__class__, self)._run()
        self._run(flavor_id=flavor_id)

Stavros Sachtouris's avatar
Stavros Sachtouris committed
741

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

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

755
    def main(self, network_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
756
757
        super(self.__class__, self)._run()
        self._run(network_id=network_id)
758
759


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

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
773
774
775
776
    @errors.generic.all
    @errors.cyclades.connection
    def _run(self):
        networks = self.client.list_networks(self['detail'])
777
778
        if not (self['detail'] or self['json_output']):
            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
784
            networks = networks[:self['limit']]
        self._print(networks, **kwargs)
785

Stavros Sachtouris's avatar
Stavros Sachtouris committed
786
787
788
789
    def main(self):
        super(self.__class__, self)._run()
        self._run()

Stavros Sachtouris's avatar
Stavros Sachtouris committed
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
797
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
798
        dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
799
800
801
802
        type=ValueArgument(
            'Valid network types are '
            'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
            '--with-type',
803
804
            default='MAC_FILTERED'),
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
805
    )
806

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

        if self['wait']:
            self._wait(r['id'], 'PENDING')
Stavros Sachtouris's avatar
Stavros Sachtouris committed
821

822
    def main(self, name):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
823
824
        super(self.__class__, self)._run()
        self._run(name)
825

Stavros Sachtouris's avatar
Stavros Sachtouris committed
826

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

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

838
    def main(self, network_id, new_name):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
839
840
        super(self.__class__, self)._run()
        self._run(network_id=network_id, new_name=new_name)
841

Stavros Sachtouris's avatar
Stavros Sachtouris committed
842

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

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

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

        r = self.client.delete_network(int(network_id))
        self._optional_output(r)

        if self['wait']:
            self._wait(network_id, status)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
868

869
    def main(self, network_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
870
871
        super(self.__class__, self)._run()
        self._run(network_id=network_id)
872

Stavros Sachtouris's avatar
Stavros Sachtouris committed
873

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

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

886
    def main(self, server_id, network_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
887
888
        super(self.__class__, self)._run()
        self._run(server_id=server_id, network_id=network_id)
889

Stavros Sachtouris's avatar
Stavros Sachtouris committed
890

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
898
899
900
901
902
903
904
905
906
    @errors.cyclades.nic_format
    def _server_id_from_nic(self, nic_id):
        return nic_id.split('-')[1]

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

916
    def main(self, nic_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
917
918
919
        super(self.__class__, self)._run()
        server_id = self._server_id_from_nic(nic_id=nic_id)
        self._run(nic_id=nic_id, server_id=server_id)
920
921


922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
@command(network_cmds)
class network_wait(_init_cyclades, _network_wait):
    """Wait for server to finish [PENDING, ACTIVE, DELETED]"""

    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.network_id
    def _run(self, network_id, currect_status):
        self._wait(network_id, currect_status)

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


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


@command(server_cmds)
class server_ip_pools(_init_cyclades, _optional_json):
944
945
946
947
948
949
950
951
952
953
954
955
956
    """List all floating pools of floating ips"""

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

    def main(self):
        super(self.__class__, self)._run()
        self._run()


957
958
@command(server_cmds)
class server_ip_list(_init_cyclades, _optional_json):
959
960
961
962
963
964
965
966
967
968
969
970
971
    """List all floating ips"""

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

    def main(self):
        super(self.__class__, self)._run()
        self._run()


972
973
@command(server_cmds)
class server_ip_info(_init_cyclades, _optional_json):
974
975
976
977
978
979
980
981
982
983
984
985
    """A floating IPs' details"""

    @errors.generic.all
    @errors.cyclades.connection
    def _run(self, ip):
        self._print(self.client.get_floating_ip(ip), print_dict)

    def main(self, ip):
        super(self.__class__, self)._run()
        self._run(ip