Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades_cli.py @ c5b9380c

History | View | Annotate | Download (21.7 kB)

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

    
34
from kamaki.cli import command
35
from kamaki.cli.command_tree import CommandTree
36
from kamaki.cli.utils import print_dict, print_list, print_items
37
from kamaki.cli.errors import raiseCLIError, CLISyntaxError
38
from kamaki.clients.cyclades import CycladesClient, ClientError
39
from kamaki.cli.argument import FlagArgument, ValueArgument, KeyValueArgument
40
from kamaki.cli.argument import ProgressBarArgument, DateArgument, IntArgument
41
from kamaki.cli.commands import _command_init, errors
42

    
43
from base64 import b64encode
44
from os.path import exists
45

    
46

    
47
server_cmds = CommandTree('server', 'Compute/Cyclades API server commands')
48
flavor_cmds = CommandTree('flavor', 'Compute/Cyclades API flavor commands')
49
network_cmds = CommandTree('network', 'Compute/Cyclades API network commands')
50
_commands = [server_cmds, flavor_cmds, network_cmds]
51

    
52

    
53
about_authentication = '\nUser Authentication:\
54
    \n* to check authentication: /astakos authenticate\
55
    \n* to set authentication token: /config set token <token>'
56

    
57
howto_personality = [
58
    'Defines a file to be injected to VMs personality.',
59
    'Personality value syntax: PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
60
    '  PATH: of local file to be injected',
61
    '  SERVER_PATH: destination location inside server Image',
62
    '  OWNER: user id of destination file owner',
63
    '  GROUP: group id or name to own destination file',
64
    '  MODEL: permition in octal (e.g. 0777 or o+rwx)']
65

    
66

    
67
class _init_cyclades(_command_init):
68
    @errors.generic.all
69
    def _run(self, service='compute'):
70
        token = self.config.get(service, 'token')\
71
            or self.config.get('global', 'token')
72
        base_url = self.config.get(service, 'url')\
73
            or self.config.get('global', 'url')
74
        self.client = CycladesClient(base_url=base_url, token=token)
75
        self._update_low_level_log()
76
        self._update_max_threads()
77

    
78
    def main(self):
79
        self._run()
80

    
81

    
82
@command(server_cmds)
83
class server_list(_init_cyclades):
84
    """List Virtual Machines accessible by user"""
85

    
86
    __doc__ += about_authentication
87

    
88
    arguments = dict(
89
        detail=FlagArgument('show detailed output', ('-l', '--details')),
90
        since=DateArgument(
91
            'show only items since date (\' d/m/Y H:M:S \')',
92
            '--since'),
93
        limit=IntArgument('limit number of listed VMs', ('-n', '--number')),
94
        more=FlagArgument(
95
            'output results in pages (-n to set items per page, default 10)',
96
            '--more')
97
    )
98

    
99
    def _make_results_pretty(self, servers):
100
        for server in servers:
101
            addr_dict = {}
102
            if 'attachments' in server:
103
                for addr in server['attachments']['values']:
104
                    ips = addr.pop('values', [])
105
                    for ip in ips:
106
                        addr['IPv%s' % ip['version']] = ip['addr']
107
                    if 'firewallProfile' in addr:
108
                        addr['firewall'] = addr.pop('firewallProfile')
109
                    addr_dict[addr.pop('id')] = addr
110
                server['attachments'] = addr_dict if addr_dict else None
111
            if 'metadata' in server:
112
                server['metadata'] = server['metadata']['values']
113

    
114
    @errors.generic.all
115
    @errors.cyclades.connection
116
    @errors.cyclades.date
117
    def _run(self):
118
        servers = self.client.list_servers(self['detail'], self['since'])
119
        if self['detail']:
120
            self._make_results_pretty(servers)
121

    
122
        if self['more']:
123
            print_items(
124
                servers,
125
                page_size=self['limit'] if self['limit'] else 10)
126
        else:
127
            print_items(
128
                servers[:self['limit'] if self['limit'] else len(servers)])
129

    
130
    def main(self):
131
        super(self.__class__, self)._run()
132
        self._run()
133

    
134

    
135
@command(server_cmds)
136
class server_info(_init_cyclades):
137
    """Detailed information on a Virtual Machine
138
    Contains:
139
    - name, id, status, create/update dates
140
    - network interfaces
141
    - metadata (e.g. os, superuser) and diagnostics
142
    - hardware flavor and os image ids
143
    """
144

    
145
    def _print(self, server):
146
        addr_dict = {}
147
        if 'attachments' in server:
148
            atts = server.pop('attachments')
149
            for addr in atts['values']:
150
                ips = addr.pop('values', [])
151
                for ip in ips:
152
                    addr['IPv%s' % ip['version']] = ip['addr']
153
                if 'firewallProfile' in addr:
154
                    addr['firewall'] = addr.pop('firewallProfile')
155
                addr_dict[addr.pop('id')] = addr
156
            server['attachments'] = addr_dict if addr_dict else None
157
        if 'metadata' in server:
158
            server['metadata'] = server['metadata']['values']
159
        print_dict(server, ident=1)
160

    
161
    @errors.generic.all
162
    @errors.cyclades.connection
163
    @errors.cyclades.server_id
164
    def _run(self, server_id):
165
        server = self.client.get_server_details(server_id)
166
        self._print(server)
167

    
168
    def main(self, server_id):
169
        super(self.__class__, self)._run()
170
        self._run(server_id=server_id)
171

    
172

    
173
class PersonalityArgument(KeyValueArgument):
174
    @property
175
    def value(self):
176
        return self._value if hasattr(self, '_value') else []
177

    
178
    @value.setter
179
    def value(self, newvalue):
180
        if newvalue == self.default:
181
            return self.value
182
        self._value = []
183
        for i, terms in enumerate(newvalue):
184
            termlist = terms.split(',')
185
            if len(termlist) > 5:
186
                msg = 'Wrong number of terms (should be 1 to 5)'
187
                raiseCLIError(CLISyntaxError(msg), details=howto_personality)
188
            path = termlist[0]
189
            if not exists(path):
190
                raiseCLIError(
191
                    None,
192
                    '--personality: File %s does not exist' % path,
193
                    importance=1,
194
                    details=howto_personality)
195
            self._value.append(dict(path=path))
196
            with open(path) as f:
197
                self._value[i]['contents'] = b64encode(f.read())
198
            try:
199
                self._value[i]['path'] = termlist[1]
200
                self._value[i]['owner'] = termlist[2]
201
                self._value[i]['group'] = termlist[3]
202
                self._value[i]['mode'] = termlist[4]
203
            except IndexError:
204
                pass
205

    
206

    
207
@command(server_cmds)
208
class server_create(_init_cyclades):
209
    """Create a server (aka Virtual Machine)
210
    Parameters:
211
    - name: (single quoted text)
212
    - flavor id: Hardware flavor. Pick one from: /flavor list
213
    - image id: OS images. Pick one from: /image list
214
    """
215

    
216
    arguments = dict(
217
        personality=PersonalityArgument(
218
            ' /// '.join(howto_personality),
219
            ('-p', '--personality'))
220
    )
221

    
222
    @errors.generic.all
223
    @errors.cyclades.connection
224
    @errors.plankton.id
225
    @errors.cyclades.flavor_id
226
    def _run(self, name, flavor_id, image_id):
227
        r = self.client.create_server(
228
            name,
229
            int(flavor_id),
230
            image_id,
231
            self['personality'])
232
        print_dict(r)
233

    
234
    def main(self, name, flavor_id, image_id):
235
        super(self.__class__, self)._run()
236
        self._run(name=name, flavor_id=flavor_id, image_id=image_id)
237

    
238

    
239
@command(server_cmds)
240
class server_rename(_init_cyclades):
241
    """Set/update a server (VM) name
242
    VM names are not unique, therefore multiple servers may share the same name
243
    """
244

    
245
    @errors.generic.all
246
    @errors.cyclades.connection
247
    @errors.cyclades.server_id
248
    def _run(self, server_id, new_name):
249
        self.client.update_server_name(int(server_id), new_name)
250

    
251
    def main(self, server_id, new_name):
252
        super(self.__class__, self)._run()
253
        self._run(server_id=server_id, new_name=new_name)
254

    
255

    
256
@command(server_cmds)
257
class server_delete(_init_cyclades):
258
    """Delete a server (VM)"""
259

    
260
    @errors.generic.all
261
    @errors.cyclades.connection
262
    @errors.cyclades.server_id
263
    def _run(self, server_id):
264
            self.client.delete_server(int(server_id))
265

    
266
    def main(self, server_id):
267
        super(self.__class__, self)._run()
268
        self._run(server_id=server_id)
269

    
270

    
271
@command(server_cmds)
272
class server_reboot(_init_cyclades):
273
    """Reboot a server (VM)"""
274

    
275
    arguments = dict(
276
        hard=FlagArgument('perform a hard reboot', ('-f', '--force'))
277
    )
278

    
279
    @errors.generic.all
280
    @errors.cyclades.connection
281
    @errors.cyclades.server_id
282
    def _run(self, server_id):
283
        self.client.reboot_server(int(server_id), self['hard'])
284

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

    
289

    
290
@command(server_cmds)
291
class server_start(_init_cyclades):
292
    """Start an existing server (VM)"""
293

    
294
    @errors.generic.all
295
    @errors.cyclades.connection
296
    @errors.cyclades.server_id
297
    def _run(self, server_id):
298
        self.client.start_server(int(server_id))
299

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

    
304

    
305
@command(server_cmds)
306
class server_shutdown(_init_cyclades):
307
    """Shutdown an active server (VM)"""
308

    
309
    @errors.generic.all
310
    @errors.cyclades.connection
311
    @errors.cyclades.server_id
312
    def _run(self, server_id):
313
        self.client.shutdown_server(int(server_id))
314

    
315
    def main(self, server_id):
316
        super(self.__class__, self)._run()
317
        self._run(server_id=server_id)
318

    
319

    
320
@command(server_cmds)
321
class server_console(_init_cyclades):
322
    """Get a VNC console to access an existing server (VM)
323
    Console connection information provided (at least):
324
    - host: (url or address) a VNC host
325
    - port: (int) the gateway to enter VM on host
326
    - password: for VNC authorization
327
    """
328

    
329
    @errors.generic.all
330
    @errors.cyclades.connection
331
    @errors.cyclades.server_id
332
    def _run(self, server_id):
333
        r = self.client.get_server_console(int(server_id))
334
        print_dict(r)
335

    
336
    def main(self, server_id):
337
        super(self.__class__, self)._run()
338
        self._run(server_id=server_id)
339

    
340

    
341
@command(server_cmds)
342
class server_firewall(_init_cyclades):
343
    """Set the server (VM) firewall profile on VMs public network
344
    Values for profile:
345
    - DISABLED: Shutdown firewall
346
    - ENABLED: Firewall in normal mode
347
    - PROTECTED: Firewall in secure mode
348
    """
349

    
350
    @errors.generic.all
351
    @errors.cyclades.connection
352
    @errors.cyclades.server_id
353
    @errors.cyclades.firewall
354
    def _run(self, server_id, profile):
355
        self.client.set_firewall_profile(
356
            server_id=int(server_id),
357
            profile=('%s' % profile).upper())
358

    
359
    def main(self, server_id, profile):
360
        super(self.__class__, self)._run()
361
        self._run(server_id=server_id, profile=profile)
362

    
363

    
364
@command(server_cmds)
365
class server_addr(_init_cyclades):
366
    """List the addresses of all network interfaces on a server (VM)"""
367

    
368
    @errors.generic.all
369
    @errors.cyclades.connection
370
    @errors.cyclades.server_id
371
    def _run(self, server_id):
372
        reply = self.client.list_server_nics(int(server_id))
373
        print_list(reply, with_enumeration=len(reply) > 1)
374

    
375
    def main(self, server_id):
376
        super(self.__class__, self)._run()
377
        self._run(server_id=server_id)
378

    
379

    
380
@command(server_cmds)
381
class server_meta(_init_cyclades):
382
    """Get a server's metadatum
383
    Metadata are formed as key:value pairs where key is used to retrieve them
384
    """
385

    
386
    @errors.generic.all
387
    @errors.cyclades.connection
388
    @errors.cyclades.server_id
389
    @errors.cyclades.metadata
390
    def _run(self, server_id, key=''):
391
        r = self.client.get_server_metadata(int(server_id), key)
392
        print_dict(r)
393

    
394
    def main(self, server_id, key=''):
395
        super(self.__class__, self)._run()
396
        self._run(server_id=server_id, key=key)
397

    
398

    
399
@command(server_cmds)
400
class server_setmeta(_init_cyclades):
401
    """set server (VM) metadata
402
    Metadata are formed as key:value pairs, both needed to set one
403
    """
404

    
405
    @errors.generic.all
406
    @errors.cyclades.connection
407
    @errors.cyclades.server_id
408
    def _run(self, server_id, key, val):
409
        metadata = {key: val}
410
        r = self.client.update_server_metadata(int(server_id), **metadata)
411
        print_dict(r)
412

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

    
417

    
418
@command(server_cmds)
419
class server_delmeta(_init_cyclades):
420
    """Delete server (VM) metadata"""
421

    
422
    @errors.generic.all
423
    @errors.cyclades.connection
424
    @errors.cyclades.server_id
425
    @errors.cyclades.metadata
426
    def _run(self, server_id, key):
427
        self.client.delete_server_metadata(int(server_id), key)
428

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

    
433

    
434
@command(server_cmds)
435
class server_stats(_init_cyclades):
436
    """Get server (VM) statistics"""
437

    
438
    @errors.generic.all
439
    @errors.cyclades.connection
440
    @errors.cyclades.server_id
441
    def _run(self, server_id):
442
        r = self.client.get_server_stats(int(server_id))
443
        print_dict(r, exclude=('serverRef',))
444

    
445
    def main(self, server_id):
446
        super(self.__class__, self)._run()
447
        self._run(server_id=server_id)
448

    
449

    
450
@command(server_cmds)
451
class server_wait(_init_cyclades):
452
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
453

    
454
    arguments = dict(
455
        progress_bar=ProgressBarArgument(
456
            'do not show progress bar',
457
            ('-N', '--no-progress-bar'),
458
            False
459
        )
460
    )
461

    
462
    @errors.generic.all
463
    @errors.cyclades.connection
464
    @errors.cyclades.server_id
465
    def _run(self, server_id, currect_status):
466
        (progress_bar, wait_cb) = self._safe_progress_bar(
467
            'Server %s still in %s mode' % (server_id, currect_status))
468

    
469
        try:
470
            new_mode = self.client.wait_server(
471
                server_id,
472
                currect_status,
473
                wait_cb=wait_cb)
474
        except Exception:
475
            self._safe_progress_bar_finish(progress_bar)
476
            raise
477
        finally:
478
            self._safe_progress_bar_finish(progress_bar)
479
        if new_mode:
480
            print('Server %s is now in %s mode' % (server_id, new_mode))
481
        else:
482
            raiseCLIError(None, 'Time out')
483

    
484
    def main(self, server_id, currect_status='BUILD'):
485
        super(self.__class__, self)._run()
486
        self._run(server_id=server_id, currect_status=currect_status)
487

    
488

    
489
@command(flavor_cmds)
490
class flavor_list(_init_cyclades):
491
    """List available hardware flavors"""
492

    
493
    arguments = dict(
494
        detail=FlagArgument('show detailed output', ('-l', '--details')),
495
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
496
        more=FlagArgument(
497
            'output results in pages (-n to set items per page, default 10)',
498
            '--more')
499
    )
500

    
501
    @errors.generic.all
502
    @errors.cyclades.connection
503
    def _run(self):
504
        flavors = self.client.list_flavors(self['detail'])
505
        pg_size = 10 if self['more'] and not self['limit'] else self['limit']
506
        print_items(flavors, with_redundancy=self['detail'], page_size=pg_size)
507

    
508
    def main(self):
509
        super(self.__class__, self)._run()
510
        self._run()
511

    
512

    
513
@command(flavor_cmds)
514
class flavor_info(_init_cyclades):
515
    """Detailed information on a hardware flavor
516
    To get a list of available flavors and flavor ids, try /flavor list
517
    """
518

    
519
    @errors.generic.all
520
    @errors.cyclades.connection
521
    @errors.cyclades.flavor_id
522
    def _run(self, flavor_id):
523
        flavor = self.client.get_flavor_details(int(flavor_id))
524
        print_dict(flavor)
525

    
526
    def main(self, flavor_id):
527
        super(self.__class__, self)._run()
528
        self._run(flavor_id=flavor_id)
529

    
530

    
531
@command(network_cmds)
532
class network_info(_init_cyclades):
533
    """Detailed information on a network
534
    To get a list of available networks and network ids, try /network list
535
    """
536

    
537
    @classmethod
538
    def _make_result_pretty(self, net):
539
        if 'attachments' in net:
540
            att = net['attachments']['values']
541
            count = len(att)
542
            net['attachments'] = att if count else None
543

    
544
    @errors.generic.all
545
    @errors.cyclades.connection
546
    @errors.cyclades.network_id
547
    def _run(self, network_id):
548
        network = self.client.get_network_details(int(network_id))
549
        self._make_result_pretty(network)
550
        print_dict(network, exclude=('id'))
551

    
552
    def main(self, network_id):
553
        super(self.__class__, self)._run()
554
        self._run(network_id=network_id)
555

    
556

    
557
@command(network_cmds)
558
class network_list(_init_cyclades):
559
    """List networks"""
560

    
561
    arguments = dict(
562
        detail=FlagArgument('show detailed output', ('-l', '--details')),
563
        limit=IntArgument('limit # of listed networks', ('-n', '--number')),
564
        more=FlagArgument(
565
            'output results in pages (-n to set items per page, default 10)',
566
            '--more')
567
    )
568

    
569
    def _make_results_pretty(self, nets):
570
        for net in nets:
571
            network_info._make_result_pretty(net)
572

    
573
    @errors.generic.all
574
    @errors.cyclades.connection
575
    def _run(self):
576
        networks = self.client.list_networks(self['detail'])
577
        if self['detail']:
578
            self._make_results_pretty(networks)
579
        if self['more']:
580
            print_items(networks, page_size=self['limit'] or 10)
581
        elif self['limit']:
582
            print_items(networks[:self['limit']])
583
        else:
584
            print_items(networks)
585

    
586
    def main(self):
587
        super(self.__class__, self)._run()
588
        self._run()
589

    
590

    
591
@command(network_cmds)
592
class network_create(_init_cyclades):
593
    """Create an (unconnected) network"""
594

    
595
    arguments = dict(
596
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
597
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
598
        dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
599
        type=ValueArgument('explicitly set type', '--with-type')
600
    )
601

    
602
    @errors.generic.all
603
    @errors.cyclades.connection
604
    @errors.cyclades.network_max
605
    def _run(self, name):
606
        r = self.client.create_network(
607
            name,
608
            cidr=self['cidr'],
609
            gateway=self['gateway'],
610
            dhcp=self['dhcp'],
611
            type=self['type'])
612
        print_items([r])
613

    
614
    def main(self, name):
615
        super(self.__class__, self)._run()
616
        self._run(name)
617

    
618

    
619
@command(network_cmds)
620
class network_rename(_init_cyclades):
621
    """Set the name of a network"""
622

    
623
    @errors.generic.all
624
    @errors.cyclades.connection
625
    @errors.cyclades.network_id
626
    def _run(self, network_id, new_name):
627
        self.client.update_network_name(int(network_id), new_name)
628

    
629
    def main(self, network_id, new_name):
630
        super(self.__class__, self)._run()
631
        self._run(network_id=network_id, new_name=new_name)
632

    
633

    
634
@command(network_cmds)
635
class network_delete(_init_cyclades):
636
    """Delete a network"""
637

    
638
    @errors.generic.all
639
    @errors.cyclades.connection
640
    @errors.cyclades.network_id
641
    @errors.cyclades.network_in_use
642
    def _run(self, network_id):
643
        self.client.delete_network(int(network_id))
644

    
645
    def main(self, network_id):
646
        super(self.__class__, self)._run()
647
        self._run(network_id=network_id)
648

    
649

    
650
@command(network_cmds)
651
class network_connect(_init_cyclades):
652
    """Connect a server to a network"""
653

    
654
    @errors.generic.all
655
    @errors.cyclades.connection
656
    @errors.cyclades.server_id
657
    @errors.cyclades.network_id
658
    def _run(self, server_id, network_id):
659
        self.client.connect_server(int(server_id), int(network_id))
660

    
661
    def main(self, server_id, network_id):
662
        super(self.__class__, self)._run()
663
        self._run(server_id=server_id, network_id=network_id)
664

    
665

    
666
@command(network_cmds)
667
class network_disconnect(_init_cyclades):
668
    """Disconnect a nic that connects a server to a network
669
    Nic ids are listed as "attachments" in detailed network information
670
    To get detailed network information: /network info <network id>
671
    """
672

    
673
    @errors.cyclades.nic_format
674
    def _server_id_from_nic(self, nic_id):
675
        return nic_id.split('-')[1]
676

    
677
    @errors.generic.all
678
    @errors.cyclades.connection
679
    @errors.cyclades.server_id
680
    @errors.cyclades.nic_id
681
    def _run(self, nic_id, server_id):
682
        if not self.client.disconnect_server(server_id, nic_id):
683
            raise ClientError(
684
                'Network Interface %s not found on server %s' % (
685
                    nic_id,
686
                    server_id),
687
                status=404)
688

    
689
    def main(self, nic_id):
690
        super(self.__class__, self)._run()
691
        server_id = self._server_id_from_nic(nic_id=nic_id)
692
        self._run(nic_id=nic_id, server_id=server_id)