Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (42.4 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 base64 import b64encode
35
from os.path import exists
36
from io import StringIO
37
from pydoc import pager
38

    
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 (
43
    raiseCLIError, CLISyntaxError, CLIBaseUrlError, CLIInvalidArgument)
44
from kamaki.clients.cyclades import CycladesClient, ClientError
45
from kamaki.cli.argument import FlagArgument, ValueArgument, KeyValueArgument
46
from kamaki.cli.argument import ProgressBarArgument, DateArgument, IntArgument
47
from kamaki.cli.commands import _command_init, errors, addLogSettings
48
from kamaki.cli.commands import (
49
    _optional_output_cmd, _optional_json, _name_filter, _id_filter)
50

    
51

    
52
server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
53
flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
54
network_cmds = CommandTree('network', 'Cyclades/Compute API network commands')
55
ip_cmds = CommandTree('ip', 'Cyclades/Compute API floating ip commands')
56
_commands = [server_cmds, flavor_cmds, network_cmds, ip_cmds]
57

    
58

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

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

    
72

    
73
class _service_wait(object):
74

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

    
80
    def _wait(
81
            self, service, service_id, status_method, current_status,
82
            countdown=True, timeout=60):
83
        (progress_bar, wait_cb) = self._safe_progress_bar(
84
            '%s %s: status is still %s' % (
85
                service, service_id, current_status),
86
            countdown=countdown, timeout=timeout)
87

    
88
        try:
89
            new_mode = status_method(
90
                service_id, current_status, max_wait=timeout, wait_cb=wait_cb)
91
            if new_mode:
92
                self.error('%s %s: status is now %s' % (
93
                    service, service_id, new_mode))
94
            else:
95
                self.error('%s %s: status is still %s' % (
96
                    service, service_id, current_status))
97
        except KeyboardInterrupt:
98
            self.error('\n- canceled')
99
        finally:
100
            self._safe_progress_bar_finish(progress_bar)
101

    
102

    
103
class _server_wait(_service_wait):
104

    
105
    def _wait(self, server_id, current_status, timeout=60):
106
        super(_server_wait, self)._wait(
107
            'Server', server_id, self.client.wait_server, current_status,
108
            countdown=(current_status not in ('BUILD', )),
109
            timeout=timeout if current_status not in ('BUILD', ) else 100)
110

    
111

    
112
class _network_wait(_service_wait):
113

    
114
    def _wait(self, net_id, current_status, timeout=60):
115
        super(_network_wait, self)._wait(
116
            'Network', net_id, self.client.wait_network, current_status,
117
            timeout=timeout)
118

    
119

    
120
class _firewall_wait(_service_wait):
121

    
122
    def _wait(self, server_id, current_status, timeout=60):
123
        super(_firewall_wait, self)._wait(
124
            'Firewall of server',
125
            server_id, self.client.wait_firewall, current_status,
126
            timeout=timeout)
127

    
128

    
129
class _init_cyclades(_command_init):
130
    @errors.generic.all
131
    @addLogSettings
132
    def _run(self, service='compute'):
133
        if getattr(self, 'cloud', None):
134
            base_url = self._custom_url(service) or self._custom_url(
135
                'cyclades')
136
            if base_url:
137
                token = self._custom_token(service) or self._custom_token(
138
                    'cyclades') or self.config.get_cloud('token')
139
                self.client = CycladesClient(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, _name_filter, _id_filter):
159
    """List virtual servers accessible by user
160
    Use filtering arguments (e.g., --name-like) to manage long server lists
161
    """
162

    
163
    PERMANENTS = ('id', 'name')
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(
171
            'limit number of listed virtual servers', ('-n', '--number')),
172
        more=FlagArgument(
173
            'output results in pages (-n to set items per page, default 10)',
174
            '--more'),
175
        enum=FlagArgument('Enumerate results', '--enumerate'),
176
        flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
177
        image_id=ValueArgument('filter by image id', ('--image-id')),
178
        user_id=ValueArgument('filter by user id', ('--user-id')),
179
        user_name=ValueArgument('filter by user name', ('--user-name')),
180
        status=ValueArgument(
181
            'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
182
            ('--status')),
183
        meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
184
        meta_like=KeyValueArgument(
185
            'print only if in key=value, the value is part of actual value',
186
            ('--metadata-like')),
187
    )
188

    
189
    def _add_user_name(self, servers):
190
        uuids = self._uuids2usernames(list(set(
191
                [srv['user_id'] for srv in servers] +
192
                [srv['tenant_id'] for srv in servers])))
193
        for srv in servers:
194
            srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
195
            srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
196
        return servers
197

    
198
    def _apply_common_filters(self, servers):
199
        common_filters = dict()
200
        if self['status']:
201
            common_filters['status'] = self['status']
202
        if self['user_id'] or self['user_name']:
203
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
204
            common_filters['user_id'] = uuid
205
        return filter_dicts_by_dict(servers, common_filters)
206

    
207
    def _filter_by_image(self, servers):
208
        iid = self['image_id']
209
        return [srv for srv in servers if srv['image']['id'] == iid]
210

    
211
    def _filter_by_flavor(self, servers):
212
        fid = self['flavor_id']
213
        return [srv for srv in servers if (
214
            '%s' % srv['image']['id'] == '%s' % fid)]
215

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

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

    
244
        servers = self._filter_by_name(servers)
245
        servers = self._filter_by_id(servers)
246
        servers = self._apply_common_filters(servers)
247
        if withimage:
248
            servers = self._filter_by_image(servers)
249
        if withflavor:
250
            servers = self._filter_by_flavor(servers)
251
        if withmeta:
252
            servers = self._filter_by_metadata(servers)
253

    
254
        if self['detail'] and not (
255
                self['json_output'] or self['output_format']):
256
            servers = self._add_user_name(servers)
257
        elif not (self['detail'] or (
258
                self['json_output'] or self['output_format'])):
259
            remove_from_items(servers, 'links')
260
        if detail and not self['detail']:
261
            for srv in servers:
262
                for key in set(srv).difference(self.PERMANENTS):
263
                    srv.pop(key)
264
        kwargs = dict(with_enumeration=self['enum'])
265
        if self['more']:
266
            kwargs['out'] = StringIO()
267
            kwargs['title'] = ()
268
        if self['limit']:
269
            servers = servers[:self['limit']]
270
        self._print(servers, **kwargs)
271
        if self['more']:
272
            pager(kwargs['out'].getvalue())
273

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

    
278

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

    
289
    @errors.generic.all
290
    @errors.cyclades.connection
291
    @errors.cyclades.server_id
292
    def _run(self, server_id):
293
        vm = self.client.get_server_details(server_id)
294
        uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
295
        vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
296
        vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
297
        self._print(vm, self.print_dict)
298

    
299
    def main(self, server_id):
300
        super(self.__class__, self)._run()
301
        self._run(server_id=server_id)
302

    
303

    
304
class PersonalityArgument(KeyValueArgument):
305
    @property
306
    def value(self):
307
        return self._value if hasattr(self, '_value') else []
308

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

    
336

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

    
346
    arguments = dict(
347
        personality=PersonalityArgument(
348
            (80 * ' ').join(howto_personality), ('-p', '--personality')),
349
        wait=FlagArgument('Wait server to build', ('-w', '--wait'))
350
    )
351

    
352
    @errors.generic.all
353
    @errors.cyclades.connection
354
    @errors.plankton.id
355
    @errors.cyclades.flavor_id
356
    def _run(self, name, flavor_id, image_id):
357
        r = self.client.create_server(
358
            name, int(flavor_id), image_id, personality=self['personality'])
359
        usernames = self._uuids2usernames([r['user_id'], r['tenant_id']])
360
        r['user_id'] += ' (%s)' % usernames[r['user_id']]
361
        r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
362
        self._print(r, self.print_dict)
363
        if self['wait']:
364
            self._wait(r['id'], r['status'])
365

    
366
    def main(self, name, flavor_id, image_id):
367
        super(self.__class__, self)._run()
368
        self._run(name=name, flavor_id=flavor_id, image_id=image_id)
369

    
370

    
371
@command(server_cmds)
372
class server_rename(_init_cyclades, _optional_output_cmd):
373
    """Set/update a virtual server name
374
    virtual server names are not unique, therefore multiple servers may share
375
    the same name
376
    """
377

    
378
    @errors.generic.all
379
    @errors.cyclades.connection
380
    @errors.cyclades.server_id
381
    def _run(self, server_id, new_name):
382
        self._optional_output(
383
            self.client.update_server_name(int(server_id), new_name))
384

    
385
    def main(self, server_id, new_name):
386
        super(self.__class__, self)._run()
387
        self._run(server_id=server_id, new_name=new_name)
388

    
389

    
390
@command(server_cmds)
391
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
392
    """Delete a virtual server"""
393

    
394
    arguments = dict(
395
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
396
    )
397

    
398
    @errors.generic.all
399
    @errors.cyclades.connection
400
    @errors.cyclades.server_id
401
    def _run(self, server_id):
402
            status = 'DELETED'
403
            if self['wait']:
404
                details = self.client.get_server_details(server_id)
405
                status = details['status']
406

    
407
            r = self.client.delete_server(int(server_id))
408
            self._optional_output(r)
409

    
410
            if self['wait']:
411
                self._wait(server_id, status)
412

    
413
    def main(self, server_id):
414
        super(self.__class__, self)._run()
415
        self._run(server_id=server_id)
416

    
417

    
418
@command(server_cmds)
419
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
420
    """Reboot a virtual server"""
421

    
422
    arguments = dict(
423
        hard=FlagArgument(
424
            'perform a hard reboot (deprecated)', ('-f', '--force')),
425
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
426
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
427
    )
428

    
429
    @errors.generic.all
430
    @errors.cyclades.connection
431
    @errors.cyclades.server_id
432
    def _run(self, server_id):
433
        hard_reboot = self['hard']
434
        if hard_reboot:
435
            self.error(
436
                'WARNING: -f/--force will be deprecated in version 0.12\n'
437
                '\tIn the future, please use --type=hard instead')
438
        if self['type']:
439
            if self['type'].lower() in ('soft', ):
440
                hard_reboot = False
441
            elif self['type'].lower() in ('hard', ):
442
                hard_reboot = True
443
            else:
444
                raise CLISyntaxError(
445
                    'Invalid reboot type %s' % self['type'],
446
                    importance=2, details=[
447
                        '--type values are either SOFT (default) or HARD'])
448

    
449
        r = self.client.reboot_server(int(server_id), hard_reboot)
450
        self._optional_output(r)
451

    
452
        if self['wait']:
453
            self._wait(server_id, 'REBOOT')
454

    
455
    def main(self, server_id):
456
        super(self.__class__, self)._run()
457
        self._run(server_id=server_id)
458

    
459

    
460
@command(server_cmds)
461
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
462
    """Start an existing virtual server"""
463

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

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

    
479
        r = self.client.start_server(int(server_id))
480
        self._optional_output(r)
481

    
482
        if self['wait']:
483
            self._wait(server_id, status)
484

    
485
    def main(self, server_id):
486
        super(self.__class__, self)._run()
487
        self._run(server_id=server_id)
488

    
489

    
490
@command(server_cmds)
491
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
492
    """Shutdown an active virtual server"""
493

    
494
    arguments = dict(
495
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
496
    )
497

    
498
    @errors.generic.all
499
    @errors.cyclades.connection
500
    @errors.cyclades.server_id
501
    def _run(self, server_id):
502
        status = 'STOPPED'
503
        if self['wait']:
504
            details = self.client.get_server_details(server_id)
505
            status = details['status']
506
            if status in ('STOPPED', ):
507
                return
508

    
509
        r = self.client.shutdown_server(int(server_id))
510
        self._optional_output(r)
511

    
512
        if self['wait']:
513
            self._wait(server_id, status)
514

    
515
    def main(self, server_id):
516
        super(self.__class__, self)._run()
517
        self._run(server_id=server_id)
518

    
519

    
520
@command(server_cmds)
521
class server_console(_init_cyclades, _optional_json):
522
    """Get a VNC console to access an existing virtual server
523
    Console connection information provided (at least):
524
    - host: (url or address) a VNC host
525
    - port: (int) the gateway to enter virtual server on host
526
    - password: for VNC authorization
527
    """
528

    
529
    @errors.generic.all
530
    @errors.cyclades.connection
531
    @errors.cyclades.server_id
532
    def _run(self, server_id):
533
        self._print(
534
            self.client.get_server_console(int(server_id)), self.print_dict)
535

    
536
    def main(self, server_id):
537
        super(self.__class__, self)._run()
538
        self._run(server_id=server_id)
539

    
540

    
541
@command(server_cmds)
542
class server_resize(_init_cyclades, _optional_output_cmd):
543
    """Set a different flavor for an existing server
544
    To get server ids and flavor ids:
545
    /server list
546
    /flavor list
547
    """
548

    
549
    @errors.generic.all
550
    @errors.cyclades.connection
551
    @errors.cyclades.server_id
552
    @errors.cyclades.flavor_id
553
    def _run(self, server_id, flavor_id):
554
        self._optional_output(self.client.resize_server(server_id, flavor_id))
555

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

    
560

    
561
@command(server_cmds)
562
class server_firewall(_init_cyclades):
563
    """Manage virtual server firewall profiles for public networks"""
564

    
565

    
566
@command(server_cmds)
567
class server_firewall_set(
568
        _init_cyclades, _optional_output_cmd, _firewall_wait):
569
    """Set the firewall profile on virtual server public network
570
    Values for profile:
571
    - DISABLED: Shutdown firewall
572
    - ENABLED: Firewall in normal mode
573
    - PROTECTED: Firewall in secure mode
574
    """
575

    
576
    arguments = dict(
577
        wait=FlagArgument('Wait server firewall to build', ('-w', '--wait')),
578
        timeout=IntArgument(
579
            'Set wait timeout in seconds (default: 60)', '--timeout',
580
            default=60)
581
    )
582

    
583
    @errors.generic.all
584
    @errors.cyclades.connection
585
    @errors.cyclades.server_id
586
    @errors.cyclades.firewall
587
    def _run(self, server_id, profile):
588
        if self['timeout'] and not self['wait']:
589
            raise CLIInvalidArgument('Invalid use of --timeout', details=[
590
                'Timeout is used only along with -w/--wait'])
591
        old_profile = self.client.get_firewall_profile(server_id)
592
        if old_profile.lower() == profile.lower():
593
            self.error('Firewall of server %s: allready in status %s' % (
594
                server_id, old_profile))
595
        else:
596
            self._optional_output(self.client.set_firewall_profile(
597
                server_id=int(server_id), profile=('%s' % profile).upper()))
598
            if self['wait']:
599
                self._wait(server_id, old_profile, timeout=self['timeout'])
600

    
601
    def main(self, server_id, profile):
602
        super(self.__class__, self)._run()
603
        self._run(server_id=server_id, profile=profile)
604

    
605

    
606
@command(server_cmds)
607
class server_firewall_get(_init_cyclades):
608
    """Get the firewall profile for a virtual servers' public network"""
609

    
610
    @errors.generic.all
611
    @errors.cyclades.connection
612
    @errors.cyclades.server_id
613
    def _run(self, server_id):
614
        self.writeln(self.client.get_firewall_profile(server_id))
615

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

    
620

    
621
@command(server_cmds)
622
class server_addr(_init_cyclades, _optional_json):
623
    """List the addresses of all network interfaces on a virtual server"""
624

    
625
    arguments = dict(
626
        enum=FlagArgument('Enumerate results', '--enumerate')
627
    )
628

    
629
    @errors.generic.all
630
    @errors.cyclades.connection
631
    @errors.cyclades.server_id
632
    def _run(self, server_id):
633
        reply = self.client.list_server_nics(int(server_id))
634
        self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
635

    
636
    def main(self, server_id):
637
        super(self.__class__, self)._run()
638
        self._run(server_id=server_id)
639

    
640

    
641
@command(server_cmds)
642
class server_metadata(_init_cyclades):
643
    """Manage Server metadata (key:value pairs of server attributes)"""
644

    
645

    
646
@command(server_cmds)
647
class server_metadata_list(_init_cyclades, _optional_json):
648
    """Get server metadata"""
649

    
650
    @errors.generic.all
651
    @errors.cyclades.connection
652
    @errors.cyclades.server_id
653
    @errors.cyclades.metadata
654
    def _run(self, server_id, key=''):
655
        self._print(
656
            self.client.get_server_metadata(int(server_id), key),
657
            self.print_dict)
658

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

    
663

    
664
@command(server_cmds)
665
class server_metadata_set(_init_cyclades, _optional_json):
666
    """Set / update virtual server metadata
667
    Metadata should be given in key/value pairs in key=value format
668
    For example: /server metadata set <server id> key1=value1 key2=value2
669
    Old, unreferenced metadata will remain intact
670
    """
671

    
672
    @errors.generic.all
673
    @errors.cyclades.connection
674
    @errors.cyclades.server_id
675
    def _run(self, server_id, keyvals):
676
        assert keyvals, 'Please, add some metadata ( key=value)'
677
        metadata = dict()
678
        for keyval in keyvals:
679
            k, sep, v = keyval.partition('=')
680
            if sep and k:
681
                metadata[k] = v
682
            else:
683
                raiseCLIError(
684
                    'Invalid piece of metadata %s' % keyval,
685
                    importance=2, details=[
686
                        'Correct metadata format: key=val',
687
                        'For example:',
688
                        '/server metadata set <server id>'
689
                        'key1=value1 key2=value2'])
690
        self._print(
691
            self.client.update_server_metadata(int(server_id), **metadata),
692
            self.print_dict)
693

    
694
    def main(self, server_id, *key_equals_val):
695
        super(self.__class__, self)._run()
696
        self._run(server_id=server_id, keyvals=key_equals_val)
697

    
698

    
699
@command(server_cmds)
700
class server_metadata_delete(_init_cyclades, _optional_output_cmd):
701
    """Delete virtual server metadata"""
702

    
703
    @errors.generic.all
704
    @errors.cyclades.connection
705
    @errors.cyclades.server_id
706
    @errors.cyclades.metadata
707
    def _run(self, server_id, key):
708
        self._optional_output(
709
            self.client.delete_server_metadata(int(server_id), key))
710

    
711
    def main(self, server_id, key):
712
        super(self.__class__, self)._run()
713
        self._run(server_id=server_id, key=key)
714

    
715

    
716
@command(server_cmds)
717
class server_stats(_init_cyclades, _optional_json):
718
    """Get virtual server statistics"""
719

    
720
    @errors.generic.all
721
    @errors.cyclades.connection
722
    @errors.cyclades.server_id
723
    def _run(self, server_id):
724
        self._print(
725
            self.client.get_server_stats(int(server_id)), self.print_dict)
726

    
727
    def main(self, server_id):
728
        super(self.__class__, self)._run()
729
        self._run(server_id=server_id)
730

    
731

    
732
@command(server_cmds)
733
class server_wait(_init_cyclades, _server_wait):
734
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
735

    
736
    arguments = dict(
737
        timeout=IntArgument(
738
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
739
    )
740

    
741
    @errors.generic.all
742
    @errors.cyclades.connection
743
    @errors.cyclades.server_id
744
    def _run(self, server_id, current_status):
745
        r = self.client.get_server_details(server_id)
746
        if r['status'].lower() == current_status.lower():
747
            self._wait(server_id, current_status, timeout=self['timeout'])
748
        else:
749
            self.error(
750
                'Server %s: Cannot wait for status %s, '
751
                'status is already %s' % (
752
                    server_id, current_status, r['status']))
753

    
754
    def main(self, server_id, current_status='BUILD'):
755
        super(self.__class__, self)._run()
756
        self._run(server_id=server_id, current_status=current_status)
757

    
758

    
759
@command(flavor_cmds)
760
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
761
    """List available hardware flavors"""
762

    
763
    PERMANENTS = ('id', 'name')
764

    
765
    arguments = dict(
766
        detail=FlagArgument('show detailed output', ('-l', '--details')),
767
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
768
        more=FlagArgument(
769
            'output results in pages (-n to set items per page, default 10)',
770
            '--more'),
771
        enum=FlagArgument('Enumerate results', '--enumerate'),
772
        ram=ValueArgument('filter by ram', ('--ram')),
773
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
774
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
775
        disk_template=ValueArgument(
776
            'filter by disk_templace', ('--disk-template'))
777
    )
778

    
779
    def _apply_common_filters(self, flavors):
780
        common_filters = dict()
781
        if self['ram']:
782
            common_filters['ram'] = self['ram']
783
        if self['vcpus']:
784
            common_filters['vcpus'] = self['vcpus']
785
        if self['disk']:
786
            common_filters['disk'] = self['disk']
787
        if self['disk_template']:
788
            common_filters['SNF:disk_template'] = self['disk_template']
789
        return filter_dicts_by_dict(flavors, common_filters)
790

    
791
    @errors.generic.all
792
    @errors.cyclades.connection
793
    def _run(self):
794
        withcommons = self['ram'] or self['vcpus'] or (
795
            self['disk'] or self['disk_template'])
796
        detail = self['detail'] or withcommons
797
        flavors = self.client.list_flavors(detail)
798
        flavors = self._filter_by_name(flavors)
799
        flavors = self._filter_by_id(flavors)
800
        if withcommons:
801
            flavors = self._apply_common_filters(flavors)
802
        if not (self['detail'] or (
803
                self['json_output'] or self['output_format'])):
804
            remove_from_items(flavors, 'links')
805
        if detail and not self['detail']:
806
            for flv in flavors:
807
                for key in set(flv).difference(self.PERMANENTS):
808
                    flv.pop(key)
809
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
810
        self._print(
811
            flavors,
812
            with_redundancy=self['detail'], with_enumeration=self['enum'],
813
            **kwargs)
814
        if self['more']:
815
            pager(kwargs['out'].getvalue())
816

    
817
    def main(self):
818
        super(self.__class__, self)._run()
819
        self._run()
820

    
821

    
822
@command(flavor_cmds)
823
class flavor_info(_init_cyclades, _optional_json):
824
    """Detailed information on a hardware flavor
825
    To get a list of available flavors and flavor ids, try /flavor list
826
    """
827

    
828
    @errors.generic.all
829
    @errors.cyclades.connection
830
    @errors.cyclades.flavor_id
831
    def _run(self, flavor_id):
832
        self._print(
833
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
834

    
835
    def main(self, flavor_id):
836
        super(self.__class__, self)._run()
837
        self._run(flavor_id=flavor_id)
838

    
839

    
840
def _add_name(self, net):
841
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
842
        if user_id:
843
            uuids.append(user_id)
844
        if tenant_id:
845
            uuids.append(tenant_id)
846
        if uuids:
847
            usernames = self._uuids2usernames(uuids)
848
            if user_id:
849
                net['user_id'] += ' (%s)' % usernames[user_id]
850
            if tenant_id:
851
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]
852

    
853

    
854
@command(network_cmds)
855
class network_info(_init_cyclades, _optional_json):
856
    """Detailed information on a network
857
    To get a list of available networks and network ids, try /network list
858
    """
859

    
860
    @errors.generic.all
861
    @errors.cyclades.connection
862
    @errors.cyclades.network_id
863
    def _run(self, network_id):
864
        network = self.client.get_network_details(int(network_id))
865
        _add_name(self, network)
866
        self._print(network, self.print_dict, exclude=('id'))
867

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

    
872

    
873
@command(network_cmds)
874
class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
875
    """List networks"""
876

    
877
    PERMANENTS = ('id', 'name')
878

    
879
    arguments = dict(
880
        detail=FlagArgument('show detailed output', ('-l', '--details')),
881
        limit=IntArgument('limit # of listed networks', ('-n', '--number')),
882
        more=FlagArgument(
883
            'output results in pages (-n to set items per page, default 10)',
884
            '--more'),
885
        enum=FlagArgument('Enumerate results', '--enumerate'),
886
        status=ValueArgument('filter by status', ('--status')),
887
        public=FlagArgument('only public networks', ('--public')),
888
        private=FlagArgument('only private networks', ('--private')),
889
        dhcp=FlagArgument('show networks with dhcp', ('--with-dhcp')),
890
        no_dhcp=FlagArgument('show networks without dhcp', ('--without-dhcp')),
891
        user_id=ValueArgument('filter by user id', ('--user-id')),
892
        user_name=ValueArgument('filter by user name', ('--user-name')),
893
        gateway=ValueArgument('filter by gateway (IPv4)', ('--gateway')),
894
        gateway6=ValueArgument('filter by gateway (IPv6)', ('--gateway6')),
895
        cidr=ValueArgument('filter by cidr (IPv4)', ('--cidr')),
896
        cidr6=ValueArgument('filter by cidr (IPv6)', ('--cidr6')),
897
        type=ValueArgument('filter by type', ('--type')),
898
    )
899

    
900
    def _apply_common_filters(self, networks):
901
        common_filter = dict()
902
        if self['public']:
903
            if self['private']:
904
                return []
905
            common_filter['public'] = self['public']
906
        elif self['private']:
907
            common_filter['public'] = False
908
        if self['dhcp']:
909
            if self['no_dhcp']:
910
                return []
911
            common_filter['dhcp'] = True
912
        elif self['no_dhcp']:
913
            common_filter['dhcp'] = False
914
        if self['user_id'] or self['user_name']:
915
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
916
            common_filter['user_id'] = uuid
917
        for term in ('status', 'gateway', 'gateway6', 'cidr', 'cidr6', 'type'):
918
            if self[term]:
919
                common_filter[term] = self[term]
920
        return filter_dicts_by_dict(networks, common_filter)
921

    
922
    def _add_name(self, networks, key='user_id'):
923
        uuids = self._uuids2usernames(
924
            list(set([net[key] for net in networks])))
925
        for net in networks:
926
            v = net.get(key, None)
927
            if v:
928
                net[key] += ' (%s)' % uuids[v]
929
        return networks
930

    
931
    @errors.generic.all
932
    @errors.cyclades.connection
933
    def _run(self):
934
        withcommons = False
935
        for term in (
936
                'status', 'public', 'private', 'user_id', 'user_name', 'type',
937
                'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
938
            if self[term]:
939
                withcommons = True
940
                break
941
        detail = self['detail'] or withcommons
942
        networks = self.client.list_networks(detail)
943
        networks = self._filter_by_name(networks)
944
        networks = self._filter_by_id(networks)
945
        if withcommons:
946
            networks = self._apply_common_filters(networks)
947
        if not (self['detail'] or (
948
                self['json_output'] or self['output_format'])):
949
            remove_from_items(networks, 'links')
950
        if detail and not self['detail']:
951
            for net in networks:
952
                for key in set(net).difference(self.PERMANENTS):
953
                    net.pop(key)
954
        if self['detail'] and not (
955
                self['json_output'] or self['output_format']):
956
            self._add_name(networks)
957
            self._add_name(networks, 'tenant_id')
958
        kwargs = dict(with_enumeration=self['enum'])
959
        if self['more']:
960
            kwargs['out'] = StringIO()
961
            kwargs['title'] = ()
962
        if self['limit']:
963
            networks = networks[:self['limit']]
964
        self._print(networks, **kwargs)
965
        if self['more']:
966
            pager(kwargs['out'].getvalue())
967

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

    
972

    
973
@command(network_cmds)
974
class network_create(_init_cyclades, _optional_json, _network_wait):
975
    """Create an (unconnected) network"""
976

    
977
    arguments = dict(
978
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
979
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
980
        dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
981
        type=ValueArgument(
982
            'Valid network types are '
983
            'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
984
            '--with-type',
985
            default='MAC_FILTERED'),
986
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
987
    )
988

    
989
    @errors.generic.all
990
    @errors.cyclades.connection
991
    @errors.cyclades.network_max
992
    def _run(self, name):
993
        r = self.client.create_network(
994
            name,
995
            cidr=self['cidr'],
996
            gateway=self['gateway'],
997
            dhcp=self['dhcp'],
998
            type=self['type'])
999
        _add_name(self, r)
1000
        self._print(r, self.print_dict)
1001
        if self['wait'] and r['status'] in ('PENDING', ):
1002
            self._wait(r['id'], 'PENDING')
1003

    
1004
    def main(self, name):
1005
        super(self.__class__, self)._run()
1006
        self._run(name)
1007

    
1008

    
1009
@command(network_cmds)
1010
class network_rename(_init_cyclades, _optional_output_cmd):
1011
    """Set the name of a network"""
1012

    
1013
    @errors.generic.all
1014
    @errors.cyclades.connection
1015
    @errors.cyclades.network_id
1016
    def _run(self, network_id, new_name):
1017
        self._optional_output(
1018
                self.client.update_network_name(int(network_id), new_name))
1019

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

    
1024

    
1025
@command(network_cmds)
1026
class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
1027
    """Delete a network"""
1028

    
1029
    arguments = dict(
1030
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
1031
    )
1032

    
1033
    @errors.generic.all
1034
    @errors.cyclades.connection
1035
    @errors.cyclades.network_in_use
1036
    @errors.cyclades.network_id
1037
    def _run(self, network_id):
1038
        status = 'DELETED'
1039
        if self['wait']:
1040
            r = self.client.get_network_details(network_id)
1041
            status = r['status']
1042
            if status in ('DELETED', ):
1043
                return
1044

    
1045
        r = self.client.delete_network(int(network_id))
1046
        self._optional_output(r)
1047

    
1048
        if self['wait']:
1049
            self._wait(network_id, status)
1050

    
1051
    def main(self, network_id):
1052
        super(self.__class__, self)._run()
1053
        self._run(network_id=network_id)
1054

    
1055

    
1056
@command(network_cmds)
1057
class network_connect(_init_cyclades, _optional_output_cmd):
1058
    """Connect a server to a network"""
1059

    
1060
    @errors.generic.all
1061
    @errors.cyclades.connection
1062
    @errors.cyclades.server_id
1063
    @errors.cyclades.network_id
1064
    def _run(self, server_id, network_id):
1065
        self._optional_output(
1066
                self.client.connect_server(int(server_id), int(network_id)))
1067

    
1068
    def main(self, server_id, network_id):
1069
        super(self.__class__, self)._run()
1070
        self._run(server_id=server_id, network_id=network_id)
1071

    
1072

    
1073
@command(network_cmds)
1074
class network_disconnect(_init_cyclades):
1075
    """Disconnect a nic that connects a server to a network
1076
    Nic ids are listed as "attachments" in detailed network information
1077
    To get detailed network information: /network info <network id>
1078
    """
1079

    
1080
    @errors.cyclades.nic_format
1081
    def _server_id_from_nic(self, nic_id):
1082
        return nic_id.split('-')[1]
1083

    
1084
    @errors.generic.all
1085
    @errors.cyclades.connection
1086
    @errors.cyclades.server_id
1087
    @errors.cyclades.nic_id
1088
    def _run(self, nic_id, server_id):
1089
        num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
1090
        if not num_of_disconnected:
1091
            raise ClientError(
1092
                'Network Interface %s not found on server %s' % (
1093
                    nic_id, server_id),
1094
                status=404)
1095
        print('Disconnected %s connections' % num_of_disconnected)
1096

    
1097
    def main(self, nic_id):
1098
        super(self.__class__, self)._run()
1099
        server_id = self._server_id_from_nic(nic_id=nic_id)
1100
        self._run(nic_id=nic_id, server_id=server_id)
1101

    
1102

    
1103
@command(network_cmds)
1104
class network_wait(_init_cyclades, _network_wait):
1105
    """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1106

    
1107
    arguments = dict(
1108
        timeout=IntArgument(
1109
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
1110
    )
1111

    
1112
    @errors.generic.all
1113
    @errors.cyclades.connection
1114
    @errors.cyclades.network_id
1115
    def _run(self, network_id, current_status):
1116
        net = self.client.get_network_details(network_id)
1117
        if net['status'].lower() == current_status.lower():
1118
            self._wait(network_id, current_status, timeout=self['timeout'])
1119
        else:
1120
            self.error(
1121
                'Network %s: Cannot wait for status %s, '
1122
                'status is already %s' % (
1123
                    network_id, current_status, net['status']))
1124

    
1125
    def main(self, network_id, current_status='PENDING'):
1126
        super(self.__class__, self)._run()
1127
        self._run(network_id=network_id, current_status=current_status)
1128

    
1129

    
1130
@command(ip_cmds)
1131
class ip_pools(_init_cyclades, _optional_json):
1132
    """List pools of floating IPs"""
1133

    
1134
    @errors.generic.all
1135
    @errors.cyclades.connection
1136
    def _run(self):
1137
        r = self.client.get_floating_ip_pools()
1138
        self._print(r if self['json_output'] or self['output_format'] else r[
1139
            'floating_ip_pools'])
1140

    
1141
    def main(self):
1142
        super(self.__class__, self)._run()
1143
        self._run()
1144

    
1145

    
1146
@command(ip_cmds)
1147
class ip_list(_init_cyclades, _optional_json):
1148
    """List reserved floating IPs"""
1149

    
1150
    @errors.generic.all
1151
    @errors.cyclades.connection
1152
    def _run(self):
1153
        r = self.client.get_floating_ips()
1154
        self._print(r if self['json_output'] or self['output_format'] else r[
1155
            'floating_ips'])
1156

    
1157
    def main(self):
1158
        super(self.__class__, self)._run()
1159
        self._run()
1160

    
1161

    
1162
@command(ip_cmds)
1163
class ip_info(_init_cyclades, _optional_json):
1164
    """Details for an IP"""
1165

    
1166
    @errors.generic.all
1167
    @errors.cyclades.connection
1168
    def _run(self, ip):
1169
        self._print(self.client.get_floating_ip(ip), self.print_dict)
1170

    
1171
    def main(self, IP):
1172
        super(self.__class__, self)._run()
1173
        self._run(ip=IP)
1174

    
1175

    
1176
@command(ip_cmds)
1177
class ip_reserve(_init_cyclades, _optional_json):
1178
    """Reserve a floating IP
1179
    An IP is reserved from an IP pool. The default IP pool is chosen
1180
    automatically, but there is the option if specifying an explicit IP pool.
1181
    """
1182

    
1183
    arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
1184

    
1185
    @errors.generic.all
1186
    @errors.cyclades.connection
1187
    def _run(self, ip=None):
1188
        self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1189

    
1190
    def main(self, requested_IP=None):
1191
        super(self.__class__, self)._run()
1192
        self._run(ip=requested_IP)
1193

    
1194

    
1195
@command(ip_cmds)
1196
class ip_release(_init_cyclades, _optional_output_cmd):
1197
    """Release a floating IP
1198
    The release IP is "returned" to the IP pool it came from.
1199
    """
1200

    
1201
    @errors.generic.all
1202
    @errors.cyclades.connection
1203
    def _run(self, ip):
1204
        self._optional_output(self.client.delete_floating_ip(ip))
1205

    
1206
    def main(self, IP):
1207
        super(self.__class__, self)._run()
1208
        self._run(ip=IP)
1209

    
1210

    
1211
@command(ip_cmds)
1212
class ip_attach(_init_cyclades, _optional_output_cmd):
1213
    """Attach a floating IP to a server
1214
    """
1215

    
1216
    @errors.generic.all
1217
    @errors.cyclades.connection
1218
    @errors.cyclades.server_id
1219
    def _run(self, server_id, ip):
1220
        self._optional_output(self.client.attach_floating_ip(server_id, ip))
1221

    
1222
    def main(self, server_id, IP):
1223
        super(self.__class__, self)._run()
1224
        self._run(server_id=server_id, ip=IP)
1225

    
1226

    
1227
@command(ip_cmds)
1228
class ip_detach(_init_cyclades, _optional_output_cmd):
1229
    """Detach a floating IP from a server
1230
    """
1231

    
1232
    @errors.generic.all
1233
    @errors.cyclades.connection
1234
    @errors.cyclades.server_id
1235
    def _run(self, server_id, ip):
1236
        self._optional_output(self.client.detach_floating_ip(server_id, ip))
1237

    
1238
    def main(self, server_id, IP):
1239
        super(self.__class__, self)._run()
1240
        self._run(server_id=server_id, ip=IP)