Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades_cli.py @ 2005b18e

History | View | Annotate | Download (21.6 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
image_cmds = CommandTree('image', 'Cyclades/Plankton API image commands')
50
network_cmds = CommandTree('network', 'Compute/Cyclades API network commands')
51
_commands = [server_cmds, flavor_cmds, image_cmds, network_cmds]
52

    
53

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

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

    
67

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

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

    
80

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

    
85
    __doc__ += about_authentication
86

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

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

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

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

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

    
133

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

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

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

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

    
171

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

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

    
205

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

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

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

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

    
237

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

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

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

    
254

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

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

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

    
269

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

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

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

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

    
288

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

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

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

    
303

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

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

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

    
318

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

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

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

    
339

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

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

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

    
362

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

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

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

    
378

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

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

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

    
397

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

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

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

    
416

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

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

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

    
432

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

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

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

    
448

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

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

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

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

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

    
487

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

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

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

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

    
511

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

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

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

    
529

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

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

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

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

    
555

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

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

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

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

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

    
589

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

    
594
    arguments = dict(
595
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
596
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
597
        dhcp=ValueArgument('explicitly set dhcp', '--with-dhcp'),
598
        type=ValueArgument('explicitly set type', '--with-type')
599
    )
600

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

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

    
617

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

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

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

    
632

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

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

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

    
648

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

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

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

    
664

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

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

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

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