cyclades.py 38.6 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
35
36
37
38
from base64 import b64encode
from os.path import exists
from io import StringIO
from pydoc import pager

39
from kamaki.cli import command
40
from kamaki.cli.command_tree import CommandTree
41
from kamaki.cli.utils import remove_from_items, filter_dicts_by_dict
42
from kamaki.cli.errors import raiseCLIError, CLISyntaxError, CLIBaseUrlError
43
from kamaki.clients.cyclades import CycladesClient, ClientError
44
from kamaki.cli.argument import FlagArgument, ValueArgument, KeyValueArgument
45
from kamaki.cli.argument import ProgressBarArgument, DateArgument, IntArgument
46
from kamaki.cli.commands import _command_init, errors, addLogSettings
47
48
from kamaki.cli.commands import (
    _optional_output_cmd, _optional_json, _name_filter, _id_filter)
49

Stavros Sachtouris's avatar
Stavros Sachtouris committed
50

51
52
53
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')
54
_commands = [server_cmds, flavor_cmds, network_cmds]
Stavros Sachtouris's avatar
Stavros Sachtouris committed
55

56

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

61
howto_personality = [
62
63
64
    'Defines a file to be injected to VMs file system.',
    'syntax:  PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
    '  PATH: local file to be injected (relative or absolute)',
65
    '  SERVER_PATH: destination location inside server Image',
66
67
    '  OWNER: VMs user id of the remote destination file',
    '  GROUP: VMs group id or name of the destination file',
68
69
    '  MODEL: permition in octal (e.g. 0777 or o+rwx)']

70

71
class _service_wait(object):
72
73
74

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

78
    def _wait(self, service, service_id, status_method, currect_status):
79
        (progress_bar, wait_cb) = self._safe_progress_bar(
80
            '%s %s still in %s mode' % (service, service_id, currect_status))
81
82

        try:
83
84
            new_mode = status_method(
                service_id, currect_status, wait_cb=wait_cb)
85
86
87
        finally:
            self._safe_progress_bar_finish(progress_bar)
        if new_mode:
88
89
            self.error('%s %s is now in %s mode' % (
                service, service_id, new_mode))
90
91
92
93
        else:
            raiseCLIError(None, 'Time out')


94
class _server_wait(_service_wait):
95

96
97
98
    def _wait(self, server_id, currect_status):
        super(_server_wait, self)._wait(
            'Server', server_id, self.client.wait_server, currect_status)
99
100


101
102
103
104
105
class _network_wait(_service_wait):

    def _wait(self, net_id, currect_status):
        super(_network_wait, self)._wait(
            'Network', net_id, self.client.wait_network, currect_status)
106
107


108
class _init_cyclades(_command_init):
109
    @errors.generic.all
110
    @addLogSettings
Stavros Sachtouris's avatar
Stavros Sachtouris committed
111
    def _run(self, service='compute'):
112
        if getattr(self, 'cloud', None):
113
114
            base_url = self._custom_url(service) or self._custom_url(
                'cyclades')
115
            if base_url:
116
117
118
                token = self._custom_token(service) or self._custom_token(
                    'cyclades') or self.config.get_cloud('token')
                self.client = CycladesClient(base_url=base_url, token=token)
119
120
121
                return
        else:
            self.cloud = 'default'
122
123
        if getattr(self, 'auth_base', False):
            cyclades_endpoints = self.auth_base.get_service_endpoints(
124
125
                self._custom_type('cyclades') or 'compute',
                self._custom_version('cyclades') or '')
126
            base_url = cyclades_endpoints['publicURL']
127
128
            token = self.auth_base.token
            self.client = CycladesClient(base_url=base_url, token=token)
129
130
131
        else:
            raise CLIBaseUrlError(service='cyclades')

132
133
    def main(self):
        self._run()
Stavros Sachtouris's avatar
Stavros Sachtouris committed
134

Stavros Sachtouris's avatar
Stavros Sachtouris committed
135

136
@command(server_cmds)
137
class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
138
    """List Virtual Machines accessible by user"""
139

140
141
    PERMANENTS = ('id', 'name')

142
    __doc__ += about_authentication
143

144
    arguments = dict(
145
        detail=FlagArgument('show detailed output', ('-l', '--details')),
146
        since=DateArgument(
147
            'show only items since date (\' d/m/Y H:M:S \')',
148
            '--since'),
149
        limit=IntArgument('limit number of listed VMs', ('-n', '--number')),
150
        more=FlagArgument(
151
            'output results in pages (-n to set items per page, default 10)',
152
            '--more'),
153
154
155
        enum=FlagArgument('Enumerate results', '--enumerate'),
        flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
        image_id=ValueArgument('filter by image id', ('--image-id')),
156
157
158
159
160
        user_id=ValueArgument('filter by user id', ('--user-id')),
        user_name=ValueArgument('filter by user name', ('--user-name')),
        status=ValueArgument(
            'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
            ('--status')),
161
162
163
164
        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')),
165
166
    )

167
    def _add_user_name(self, servers):
168
169
170
        uuids = self._uuids2usernames(list(set(
                [srv['user_id'] for srv in servers] +
                [srv['tenant_id'] for srv in servers])))
171
172
        for srv in servers:
            srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
173
            srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
174
175
        return servers

176
177
178
179
180
181
182
183
184
    def _apply_common_filters(self, servers):
        common_filters = dict()
        if self['status']:
            common_filters['status'] = self['status']
        if self['user_id'] or self['user_name']:
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
            common_filters['user_id'] = uuid
        return filter_dicts_by_dict(servers, common_filters)

185
    def _filter_by_image(self, servers):
186
        iid = self['image_id']
187
        return [srv for srv in servers if srv['image']['id'] == iid]
188

189
    def _filter_by_flavor(self, servers):
190
        fid = self['flavor_id']
191
192
        return [srv for srv in servers if (
            '%s' % srv['image']['id'] == '%s' % fid)]
193

194
    def _filter_by_metadata(self, servers):
195
196
197
198
199
200
201
202
203
204
205
206
207
208
        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

209
210
211
212
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.date
    def _run(self):
213
214
215
        withimage = bool(self['image_id'])
        withflavor = bool(self['flavor_id'])
        withmeta = bool(self['meta'] or self['meta_like'])
216
217
218
219
        withcommons = bool(
            self['status'] or self['user_id'] or self['user_name'])
        detail = self['detail'] or (
            withimage or withflavor or withmeta or withcommons)
220
221
        servers = self.client.list_servers(detail, self['since'])

222
223
        servers = self._filter_by_name(servers)
        servers = self._filter_by_id(servers)
224
        servers = self._apply_common_filters(servers)
225
        if withimage:
226
            servers = self._filter_by_image(servers)
227
        if withflavor:
228
            servers = self._filter_by_flavor(servers)
229
        if withmeta:
230
            servers = self._filter_by_metadata(servers)
231

232
233
234
        if self['detail'] and not self['json_output']:
            servers = self._add_user_name(servers)
        elif not (self['detail'] or self['json_output']):
235
            remove_from_items(servers, 'links')
236
237
238
239
        if detail and not self['detail']:
            for srv in servers:
                for key in set(srv).difference(self.PERMANENTS):
                    srv.pop(key)
240
        kwargs = dict(with_enumeration=self['enum'])
241
        if self['more']:
242
243
244
            kwargs['out'] = StringIO()
            kwargs['title'] = ()
        if self['limit']:
245
246
            servers = servers[:self['limit']]
        self._print(servers, **kwargs)
247
248
        if self['more']:
            pager(kwargs['out'].getvalue())
249

250
251
252
253
    def main(self):
        super(self.__class__, self)._run()
        self._run()

Stavros Sachtouris's avatar
Stavros Sachtouris committed
254

255
@command(server_cmds)
256
class server_info(_init_cyclades, _optional_json):
257
258
259
260
261
262
263
    """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
    """
264

265
266
    @errors.generic.all
    @errors.cyclades.connection
Stavros Sachtouris's avatar
Stavros Sachtouris committed
267
    @errors.cyclades.server_id
268
    def _run(self, server_id):
269
270
271
272
        vm = self.client.get_server_details(server_id)
        uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
        vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
        vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
273
        self._print(vm, self.print_dict)
274

275
276
277
278
    def main(self, server_id):
        super(self.__class__, self)._run()
        self._run(server_id=server_id)

Stavros Sachtouris's avatar
Stavros Sachtouris committed
279

280
class PersonalityArgument(KeyValueArgument):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
281
    @property
282
    def value(self):
283
        return self._value if hasattr(self, '_value') else []
Stavros Sachtouris's avatar
Stavros Sachtouris committed
284
285

    @value.setter
286
287
288
    def value(self, newvalue):
        if newvalue == self.default:
            return self.value
289
290
291
292
        self._value = []
        for i, terms in enumerate(newvalue):
            termlist = terms.split(',')
            if len(termlist) > 5:
293
294
                msg = 'Wrong number of terms (should be 1 to 5)'
                raiseCLIError(CLISyntaxError(msg), details=howto_personality)
295
296
            path = termlist[0]
            if not exists(path):
297
298
                raiseCLIError(
                    None,
299
                    '--personality: File %s does not exist' % path,
300
                    importance=1, details=howto_personality)
301
302
303
304
305
306
307
308
309
310
            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
311

Stavros Sachtouris's avatar
Stavros Sachtouris committed
312

313
@command(server_cmds)
314
class server_create(_init_cyclades, _optional_json, _server_wait):
315
316
    """Create a server (aka Virtual Machine)
    Parameters:
317
318
319
    - name: (single quoted text)
    - flavor id: Hardware flavor. Pick one from: /flavor list
    - image id: OS images. Pick one from: /image list
320
    """
321

322
323
    arguments = dict(
        personality=PersonalityArgument(
324
325
            (80 * ' ').join(howto_personality), ('-p', '--personality')),
        wait=FlagArgument('Wait server to build', ('-w', '--wait'))
326
    )
327

328
329
330
331
332
    @errors.generic.all
    @errors.cyclades.connection
    @errors.plankton.id
    @errors.cyclades.flavor_id
    def _run(self, name, flavor_id, image_id):
333
        r = self.client.create_server(
334
            name, int(flavor_id), image_id, personality=self['personality'])
335
336
337
        usernames = self._uuids2usernames([r['user_id'], r['tenant_id']])
        r['user_id'] += ' (%s)' % usernames[r['user_id']]
        r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
338
        self._print(r, self.print_dict)
339
340
        if self['wait']:
            self._wait(r['id'], r['status'])
341

342
    def main(self, name, flavor_id, image_id):
343
344
        super(self.__class__, self)._run()
        self._run(name=name, flavor_id=flavor_id, image_id=image_id)
345

Stavros Sachtouris's avatar
Stavros Sachtouris committed
346

347
@command(server_cmds)
348
class server_rename(_init_cyclades, _optional_output_cmd):
349
    """Set/update a server (VM) name
Stavros Sachtouris's avatar
Stavros Sachtouris committed
350
    VM names are not unique, therefore multiple servers may share the same name
351
    """
352

Stavros Sachtouris's avatar
Stavros Sachtouris committed
353
354
355
356
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id, new_name):
357
358
        self._optional_output(
            self.client.update_server_name(int(server_id), new_name))
Stavros Sachtouris's avatar
Stavros Sachtouris committed
359

360
    def main(self, server_id, new_name):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
361
362
        super(self.__class__, self)._run()
        self._run(server_id=server_id, new_name=new_name)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
363

364

365
@command(server_cmds)
366
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
367
    """Delete a server (VM)"""
368

369
370
371
372
    arguments = dict(
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
373
374
375
376
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
377
378
379
380
381
382
383
384
385
386
            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
387
388
389
390

    def main(self, server_id):
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
391

392

393
@command(server_cmds)
394
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
395
    """Reboot a server (VM)"""
396

397
    arguments = dict(
398
399
        hard=FlagArgument('perform a hard reboot', ('-f', '--force')),
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
400
    )
401

Stavros Sachtouris's avatar
Stavros Sachtouris committed
402
403
404
405
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
406
407
408
409
410
        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
411

412
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
413
414
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
415

416

417
@command(server_cmds)
418
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
419
    """Start an existing server (VM)"""
420

421
422
423
424
    arguments = dict(
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
425
426
427
428
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
429
430
431
432
433
434
435
436
437
438
439
440
        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
441

442
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
443
444
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
445

446

447
@command(server_cmds)
448
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
449
    """Shutdown an active server (VM)"""
450

451
452
453
454
    arguments = dict(
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
455
456
457
458
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
459
460
461
462
463
464
465
466
467
468
469
470
        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
471

472
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
473
474
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
475

476

477
@command(server_cmds)
478
class server_console(_init_cyclades, _optional_json):
479
480
    """Get a VNC console to access an existing server (VM)
    Console connection information provided (at least):
481
482
483
    - host: (url or address) a VNC host
    - port: (int) the gateway to enter VM on host
    - password: for VNC authorization
484
    """
485

Stavros Sachtouris's avatar
Stavros Sachtouris committed
486
487
488
489
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
490
        self._print(
491
            self.client.get_server_console(int(server_id)), self.print_dict)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
492

493
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
494
495
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
496

Stavros Sachtouris's avatar
Stavros Sachtouris committed
497

498
499
500
501
@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:
502
503
    /server list
    /flavor list
504
505
506
507
508
509
510
511
512
513
514
515
516
517
    """

    @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)


518
@command(server_cmds)
519
class server_firewall(_init_cyclades):
520
521
522
523
524
    """Manage server (VM) firewall profiles for public networks"""


@command(server_cmds)
class server_firewall_set(_init_cyclades, _optional_output_cmd):
525
526
    """Set the server (VM) firewall profile on VMs public network
    Values for profile:
527
528
529
    - DISABLED: Shutdown firewall
    - ENABLED: Firewall in normal mode
    - PROTECTED: Firewall in secure mode
530
    """
531

Stavros Sachtouris's avatar
Stavros Sachtouris committed
532
533
534
535
536
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    @errors.cyclades.firewall
    def _run(self, server_id, profile):
537
538
        self._optional_output(self.client.set_firewall_profile(
            server_id=int(server_id), profile=('%s' % profile).upper()))
Stavros Sachtouris's avatar
Stavros Sachtouris committed
539

540
    def main(self, server_id, profile):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
541
542
        super(self.__class__, self)._run()
        self._run(server_id=server_id, profile=profile)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
543

544

545
@command(server_cmds)
546
547
548
549
550
551
552
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):
553
        self.writeln(self.client.get_firewall_profile(server_id))
554
555
556
557
558
559

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


560
@command(server_cmds)
561
class server_addr(_init_cyclades, _optional_json):
562
    """List the addresses of all network interfaces on a server (VM)"""
563

564
    arguments = dict(
565
        enum=FlagArgument('Enumerate results', '--enumerate')
566
567
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
568
569
570
571
572
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
        reply = self.client.list_server_nics(int(server_id))
573
        self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
574

Stavros Sachtouris's avatar
Stavros Sachtouris committed
575
576
577
578
    def main(self, server_id):
        super(self.__class__, self)._run()
        self._run(server_id=server_id)

Stavros Sachtouris's avatar
Stavros Sachtouris committed
579

580
@command(server_cmds)
581
582
583
584
585
class server_metadata(_init_cyclades):
    """Manage Server metadata (key:value pairs of server attributes)"""


@command(server_cmds)
586
class server_metadata_list(_init_cyclades, _optional_json):
587
    """Get server metadata"""
588

Stavros Sachtouris's avatar
Stavros Sachtouris committed
589
590
591
592
593
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    @errors.cyclades.metadata
    def _run(self, server_id, key=''):
594
        self._print(
595
596
            self.client.get_server_metadata(int(server_id), key),
            self.print_dict)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
597

598
    def main(self, server_id, key=''):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
599
600
        super(self.__class__, self)._run()
        self._run(server_id=server_id, key=key)
601

Stavros Sachtouris's avatar
Stavros Sachtouris committed
602

603
@command(server_cmds)
604
class server_metadata_set(_init_cyclades, _optional_json):
605
606
    """Set / update server(VM) metadata
    Metadata should be given in key/value pairs in key=value format
607
    For example: /server metadata set <server id> key1=value1 key2=value2
608
    Old, unreferenced metadata will remain intact
609
    """
610

Stavros Sachtouris's avatar
Stavros Sachtouris committed
611
612
613
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
614
    def _run(self, server_id, keyvals):
615
        assert keyvals, 'Please, add some metadata ( key=value)'
616
617
618
619
620
621
622
623
624
625
626
627
628
        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'])
629
        self._print(
630
            self.client.update_server_metadata(int(server_id), **metadata),
631
            self.print_dict)
632
633

    def main(self, server_id, *key_equals_val):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
634
        super(self.__class__, self)._run()
635
        self._run(server_id=server_id, keyvals=key_equals_val)
636

Stavros Sachtouris's avatar
Stavros Sachtouris committed
637

638
@command(server_cmds)
639
class server_metadata_delete(_init_cyclades, _optional_output_cmd):
640
    """Delete server (VM) metadata"""
641

Stavros Sachtouris's avatar
Stavros Sachtouris committed
642
643
644
645
646
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    @errors.cyclades.metadata
    def _run(self, server_id, key):
647
648
        self._optional_output(
            self.client.delete_server_metadata(int(server_id), key))
Stavros Sachtouris's avatar
Stavros Sachtouris committed
649

650
    def main(self, server_id, key):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
651
652
        super(self.__class__, self)._run()
        self._run(server_id=server_id, key=key)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
653

654

655
@command(server_cmds)
656
class server_stats(_init_cyclades, _optional_json):
657
    """Get server (VM) statistics"""
658

Stavros Sachtouris's avatar
Stavros Sachtouris committed
659
660
661
662
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
663
664
        self._print(
            self.client.get_server_stats(int(server_id)), self.print_dict)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
665

666
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
667
668
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
669

Stavros Sachtouris's avatar
Stavros Sachtouris committed
670

671
@command(server_cmds)
672
class server_wait(_init_cyclades, _server_wait):
673
674
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""

Stavros Sachtouris's avatar
Stavros Sachtouris committed
675
676
677
678
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id, currect_status):
679
        self._wait(server_id, currect_status)
680

Stavros Sachtouris's avatar
Stavros Sachtouris committed
681
682
683
684
    def main(self, server_id, currect_status='BUILD'):
        super(self.__class__, self)._run()
        self._run(server_id=server_id, currect_status=currect_status)

685

686
@command(flavor_cmds)
687
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
688
    """List available hardware flavors"""
689

690
691
    PERMANENTS = ('id', 'name')

692
    arguments = dict(
693
694
        detail=FlagArgument('show detailed output', ('-l', '--details')),
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
695
        more=FlagArgument(
696
            'output results in pages (-n to set items per page, default 10)',
697
            '--more'),
698
699
700
701
702
703
        enum=FlagArgument('Enumerate results', '--enumerate'),
        ram=ValueArgument('filter by ram', ('--ram')),
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
        disk_template=ValueArgument(
            'filter by disk_templace', ('--disk-template'))
704
    )
705

706
707
708
709
710
711
712
713
714
715
716
717
    def _apply_common_filters(self, flavors):
        common_filters = dict()
        if self['ram']:
            common_filters['ram'] = self['ram']
        if self['vcpus']:
            common_filters['vcpus'] = self['vcpus']
        if self['disk']:
            common_filters['disk'] = self['disk']
        if self['disk_template']:
            common_filters['SNF:disk_template'] = self['disk_template']
        return filter_dicts_by_dict(flavors, common_filters)

Stavros Sachtouris's avatar
Stavros Sachtouris committed
718
719
720
    @errors.generic.all
    @errors.cyclades.connection
    def _run(self):
721
722
723
724
        withcommons = self['ram'] or self['vcpus'] or (
            self['disk'] or self['disk_template'])
        detail = self['detail'] or withcommons
        flavors = self.client.list_flavors(detail)
725
726
        flavors = self._filter_by_name(flavors)
        flavors = self._filter_by_id(flavors)
727
728
        if withcommons:
            flavors = self._apply_common_filters(flavors)
729
730
        if not (self['detail'] or self['json_output']):
            remove_from_items(flavors, 'links')
731
732
733
734
        if detail and not self['detail']:
            for flv in flavors:
                for key in set(flv).difference(self.PERMANENTS):
                    flv.pop(key)
735
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
736
        self._print(
737
            flavors,
738
739
740
741
            with_redundancy=self['detail'], with_enumeration=self['enum'],
            **kwargs)
        if self['more']:
            pager(kwargs['out'].getvalue())
Stavros Sachtouris's avatar
Stavros Sachtouris committed
742

743
    def main(self):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
744
745
        super(self.__class__, self)._run()
        self._run()
746

Stavros Sachtouris's avatar
Stavros Sachtouris committed
747

748
@command(flavor_cmds)
749
class flavor_info(_init_cyclades, _optional_json):
750
    """Detailed information on a hardware flavor
751
752
    To get a list of available flavors and flavor ids, try /flavor list
    """
753

Stavros Sachtouris's avatar
Stavros Sachtouris committed
754
755
756
757
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.flavor_id
    def _run(self, flavor_id):
758
        self._print(
759
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
760

Stavros Sachtouris's avatar
Stavros Sachtouris committed
761
762
763
764
    def main(self, flavor_id):
        super(self.__class__, self)._run()
        self._run(flavor_id=flavor_id)

Stavros Sachtouris's avatar
Stavros Sachtouris committed
765

766
767
768
769
770
771
772
773
774
775
776
777
778
779
def _add_name(self, net):
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
        if user_id:
            uuids.append(user_id)
        if tenant_id:
            uuids.append(tenant_id)
        if uuids:
            usernames = self._uuids2usernames(uuids)
            if user_id:
                net['user_id'] += ' (%s)' % usernames[user_id]
            if tenant_id:
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]


780
@command(network_cmds)
781
class network_info(_init_cyclades, _optional_json):
782
783
784
    """Detailed information on a network
    To get a list of available networks and network ids, try /network list
    """
785

Stavros Sachtouris's avatar
Stavros Sachtouris committed
786
787
788
789
790
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.network_id
    def _run(self, network_id):
        network = self.client.get_network_details(int(network_id))
791
        _add_name(self, network)
792
        self._print(network, self.print_dict, exclude=('id'))
Stavros Sachtouris's avatar
Stavros Sachtouris committed
793

794
    def main(self, network_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
795
796
        super(self.__class__, self)._run()
        self._run(network_id=network_id)
797
798


799
@command(network_cmds)
800
class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
801
802
    """List networks"""

803
804
    PERMANENTS = ('id', 'name')

805
    arguments = dict(
806
807
        detail=FlagArgument('show detailed output', ('-l', '--details')),
        limit=IntArgument('limit # of listed networks', ('-n', '--number')),
808
809
        more=FlagArgument(
            'output results in pages (-n to set items per page, default 10)',
810
            '--more'),
811
812
813
814
815
816
817
818
819
820
821
822
823
        enum=FlagArgument('Enumerate results', '--enumerate'),
        status=ValueArgument('filter by status', ('--status')),
        public=FlagArgument('only public networks', ('--public')),
        private=FlagArgument('only private networks', ('--private')),
        dhcp=FlagArgument('show networks with dhcp', ('--with-dhcp')),
        no_dhcp=FlagArgument('show networks without dhcp', ('--without-dhcp')),
        user_id=ValueArgument('filter by user id', ('--user-id')),
        user_name=ValueArgument('filter by user name', ('--user-name')),
        gateway=ValueArgument('filter by gateway (IPv4)', ('--gateway')),
        gateway6=ValueArgument('filter by gateway (IPv6)', ('--gateway6')),
        cidr=ValueArgument('filter by cidr (IPv4)', ('--cidr')),
        cidr6=ValueArgument('filter by cidr (IPv6)', ('--cidr6')),
        type=ValueArgument('filter by type', ('--type')),
824
    )
825

826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
    def _apply_common_filters(self, networks):
        common_filter = dict()
        if self['public']:
            if self['private']:
                return []
            common_filter['public'] = self['public']
        elif self['private']:
            common_filter['public'] = False
        if self['dhcp']:
            if self['no_dhcp']:
                return []
            common_filter['dhcp'] = True
        elif self['no_dhcp']:
            common_filter['dhcp'] = False
        if self['user_id'] or self['user_name']:
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
            common_filter['user_id'] = uuid
        for term in ('status', 'gateway', 'gateway6', 'cidr', 'cidr6', 'type'):
            if self[term]:
                common_filter[term] = self[term]
        return filter_dicts_by_dict(networks, common_filter)

    def _add_name(self, networks, key='user_id'):
        uuids = self._uuids2usernames(
            list(set([net[key] for net in networks])))
        for net in networks:
            v = net.get(key, None)
            if v:
854
                net[key] += ' (%s)' % uuids[v]
855
856
        return networks

Stavros Sachtouris's avatar
Stavros Sachtouris committed
857
858
859
    @errors.generic.all
    @errors.cyclades.connection
    def _run(self):
860
861
862
863
864
865
866
867
868
        withcommons = False
        for term in (
                'status', 'public', 'private', 'user_id', 'user_name', 'type',
                'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
            if self[term]:
                withcommons = True
                break
        detail = self['detail'] or withcommons
        networks = self.client.list_networks(detail)
869
870
        networks = self._filter_by_name(networks)
        networks = self._filter_by_id(networks)
871
872
        if withcommons:
            networks = self._apply_common_filters(networks)
873
874
        if not (self['detail'] or self['json_output']):
            remove_from_items(networks, 'links')
875
876
877
878
879
880
881
        if detail and not self['detail']:
            for net in networks:
                for key in set(net).difference(self.PERMANENTS):
                    net.pop(key)
        if self['detail'] and not self['json_output']:
            self._add_name(networks)
            self._add_name(networks, 'tenant_id')
882
        kwargs = dict(with_enumeration=self['enum'])
883
        if self['more']:
884
885
886
            kwargs['out'] = StringIO()
            kwargs['title'] = ()
        if self['limit']:
887
888
            networks = networks[:self['limit']]
        self._print(networks, **kwargs)
889
890
        if self['more']:
            pager(kwargs['out'].getvalue())
891

Stavros Sachtouris's avatar
Stavros Sachtouris committed
892
893
894
895
    def main(self):
        super(self.__class__, self)._run()
        self._run()

Stavros Sachtouris's avatar
Stavros Sachtouris committed
896

897
@command(network_cmds)
898
class network_create(_init_cyclades, _optional_json, _network_wait):
899
    """Create an (unconnected) network"""
900