Revision cae67d7b kamaki/cli.py
b/kamaki/cli.py | ||
---|---|---|
73 | 73 |
from base64 import b64encode |
74 | 74 |
from grp import getgrgid |
75 | 75 |
from optparse import OptionParser |
76 |
from os.path import abspath, basename, exists |
|
76 |
from os.path import abspath, basename, exists, expanduser
|
|
77 | 77 |
from pwd import getpwuid |
78 | 78 |
from sys import argv, exit, stdout |
79 | 79 |
|
80 |
from clint.textui import puts, puts_err, indent |
|
81 |
from clint.textui.cols import columns |
|
82 |
|
|
80 | 83 |
from kamaki import clients |
81 |
from kamaki.config import Config, ConfigError
|
|
84 |
from kamaki.config import Config |
|
82 | 85 |
from kamaki.utils import OrderedDict, print_addresses, print_dict, print_items |
83 | 86 |
|
84 | 87 |
|
85 |
log = logging.getLogger('kamaki') |
|
88 |
# Path to the file that stores the configuration |
|
89 |
CONFIG_PATH = expanduser('~/.kamakirc') |
|
90 |
|
|
91 |
# Name of a shell variable to bypass the CONFIG_PATH value |
|
92 |
CONFIG_ENV = 'KAMAKI_CONFIG' |
|
93 |
|
|
86 | 94 |
|
87 | 95 |
_commands = OrderedDict() |
88 | 96 |
|
89 | 97 |
|
90 |
def command(api=None, group=None, name=None, description=None, syntax=None): |
|
98 |
GROUPS = { |
|
99 |
'config': "Configuration commands", |
|
100 |
'server': "Compute API server commands", |
|
101 |
'flavor': "Compute API flavor commands", |
|
102 |
'image': "Compute API image commands", |
|
103 |
'network': "Compute API network commands (Cyclades extension)", |
|
104 |
'glance': "Image API commands", |
|
105 |
'store': "Storage API commands"} |
|
106 |
|
|
107 |
|
|
108 |
def command(api=None, group=None, name=None, syntax=None): |
|
91 | 109 |
"""Class decorator that registers a class as a CLI command.""" |
92 | 110 |
|
93 | 111 |
def decorator(cls): |
... | ... | |
101 | 119 |
cls.description = description or cls.__doc__ |
102 | 120 |
cls.syntax = syntax |
103 | 121 |
|
122 |
short_description, sep, long_description = cls.__doc__.partition('\n') |
|
123 |
cls.description = short_description |
|
124 |
cls.long_description = long_description or short_description |
|
125 |
|
|
126 |
cls.syntax = syntax |
|
104 | 127 |
if cls.syntax is None: |
105 | 128 |
# Generate a syntax string based on main's arguments |
106 | 129 |
spec = inspect.getargspec(cls.main.im_func) |
... | ... | |
119 | 142 |
return decorator |
120 | 143 |
|
121 | 144 |
|
122 |
@command() |
|
145 |
@command(api='config')
|
|
123 | 146 |
class config_list(object): |
124 |
"""list configuration options"""
|
|
147 |
"""List configuration options"""
|
|
125 | 148 |
|
126 |
@classmethod |
|
127 | 149 |
def update_parser(cls, parser): |
128 | 150 |
parser.add_option('-a', dest='all', action='store_true', |
129 |
default=False, help='include empty values')
|
|
151 |
default=False, help='include default values')
|
|
130 | 152 |
|
131 | 153 |
def main(self): |
132 |
for key, val in sorted(self.config.items()): |
|
133 |
if not val and not self.options.all: |
|
134 |
continue |
|
135 |
print '%s=%s' % (key, val) |
|
154 |
include_defaults = self.options.all |
|
155 |
for section in sorted(self.config.sections()): |
|
156 |
items = self.config.items(section, include_defaults) |
|
157 |
for key, val in sorted(items): |
|
158 |
puts('%s.%s = %s' % (section, key, val)) |
|
136 | 159 |
|
137 | 160 |
|
138 |
@command() |
|
161 |
@command(api='config')
|
|
139 | 162 |
class config_get(object): |
140 |
"""get a configuration option"""
|
|
163 |
"""Show a configuration option"""
|
|
141 | 164 |
|
142 |
def main(self, key): |
|
143 |
val = self.config.get(key) |
|
144 |
if val is not None: |
|
145 |
print val |
|
165 |
def main(self, option): |
|
166 |
section, sep, key = option.rpartition('.') |
|
167 |
section = section or 'global' |
|
168 |
value = self.config.get(section, key) |
|
169 |
if value is not None: |
|
170 |
print value |
|
146 | 171 |
|
147 | 172 |
|
148 |
@command() |
|
173 |
@command(api='config')
|
|
149 | 174 |
class config_set(object): |
150 |
"""set a configuration option"""
|
|
175 |
"""Set a configuration option"""
|
|
151 | 176 |
|
152 |
def main(self, key, val): |
|
153 |
self.config.set(key, val) |
|
177 |
def main(self, option, value): |
|
178 |
section, sep, key = option.rpartition('.') |
|
179 |
section = section or 'global' |
|
180 |
self.config.set(section, key, value) |
|
181 |
self.config.write() |
|
154 | 182 |
|
155 | 183 |
|
156 |
@command() |
|
157 |
class config_del(object): |
|
158 |
"""delete a configuration option"""
|
|
184 |
@command(api='config')
|
|
185 |
class config_delete(object):
|
|
186 |
"""Delete a configuration option (and use the default value)"""
|
|
159 | 187 |
|
160 |
def main(self, key): |
|
161 |
self.config.delete(key) |
|
188 |
def main(self, option): |
|
189 |
section, sep, key = option.rpartition('.') |
|
190 |
section = section or 'global' |
|
191 |
self.config.remove_option(section, key) |
|
192 |
self.config.write() |
|
162 | 193 |
|
163 | 194 |
|
164 | 195 |
@command(api='compute') |
165 | 196 |
class server_list(object): |
166 | 197 |
"""list servers""" |
167 | 198 |
|
168 |
@classmethod |
|
169 | 199 |
def update_parser(cls, parser): |
170 | 200 |
parser.add_option('-l', dest='detail', action='store_true', |
171 | 201 |
default=False, help='show detailed output') |
... | ... | |
188 | 218 |
class server_create(object): |
189 | 219 |
"""create server""" |
190 | 220 |
|
191 |
@classmethod |
|
192 | 221 |
def update_parser(cls, parser): |
193 | 222 |
parser.add_option('--personality', dest='personalities', |
194 | 223 |
action='append', default=[], |
... | ... | |
248 | 277 |
class server_reboot(object): |
249 | 278 |
"""reboot server""" |
250 | 279 |
|
251 |
@classmethod |
|
252 | 280 |
def update_parser(cls, parser): |
253 | 281 |
parser.add_option('-f', dest='hard', action='store_true', |
254 | 282 |
default=False, help='perform a hard reboot') |
... | ... | |
349 | 377 |
class flavor_list(object): |
350 | 378 |
"""list flavors""" |
351 | 379 |
|
352 |
@classmethod |
|
353 | 380 |
def update_parser(cls, parser): |
354 | 381 |
parser.add_option('-l', dest='detail', action='store_true', |
355 | 382 |
default=False, help='show detailed output') |
... | ... | |
372 | 399 |
class image_list(object): |
373 | 400 |
"""list images""" |
374 | 401 |
|
375 |
@classmethod |
|
376 | 402 |
def update_parser(cls, parser): |
377 | 403 |
parser.add_option('-l', dest='detail', action='store_true', |
378 | 404 |
default=False, help='show detailed output') |
... | ... | |
439 | 465 |
class network_list(object): |
440 | 466 |
"""list networks""" |
441 | 467 |
|
442 |
@classmethod |
|
443 | 468 |
def update_parser(cls, parser): |
444 | 469 |
parser.add_option('-l', dest='detail', action='store_true', |
445 | 470 |
default=False, help='show detailed output') |
... | ... | |
503 | 528 |
class glance_list(object): |
504 | 529 |
"""list images""" |
505 | 530 |
|
506 |
@classmethod |
|
507 | 531 |
def update_parser(cls, parser): |
508 | 532 |
parser.add_option('-l', dest='detail', action='store_true', |
509 | 533 |
default=False, help='show detailed output') |
... | ... | |
549 | 573 |
class glance_register(object): |
550 | 574 |
"""register an image""" |
551 | 575 |
|
552 |
@classmethod |
|
553 | 576 |
def update_parser(cls, parser): |
554 | 577 |
parser.add_option('--checksum', dest='checksum', metavar='CHECKSUM', |
555 | 578 |
help='set image checksum') |
... | ... | |
635 | 658 |
class store_command(object): |
636 | 659 |
"""base class for all store_* commands""" |
637 | 660 |
|
638 |
@classmethod |
|
639 | 661 |
def update_parser(cls, parser): |
640 | 662 |
parser.add_option('--account', dest='account', metavar='NAME', |
641 | 663 |
help='use account NAME') |
... | ... | |
655 | 677 |
class store_create(object): |
656 | 678 |
"""create a container""" |
657 | 679 |
|
658 |
@classmethod |
|
659 | 680 |
def update_parser(cls, parser): |
660 | 681 |
parser.add_option('--account', dest='account', metavar='ACCOUNT', |
661 | 682 |
help='use account ACCOUNT') |
... | ... | |
711 | 732 |
self.client.delete_object(path) |
712 | 733 |
|
713 | 734 |
|
714 |
def print_groups(groups): |
|
715 |
|
|
716 |
print 'Groups:' |
|
717 |
for group in groups: |
|
718 |
print ' %s' % group |
|
735 |
def print_groups(): |
|
736 |
puts('\nGroups:') |
|
737 |
with indent(2): |
|
738 |
for group in _commands: |
|
739 |
description = GROUPS.get(group, '') |
|
740 |
puts(columns([group, 12], [description, 60])) |
|
719 | 741 |
|
720 | 742 |
|
721 |
def print_commands(group, commands): |
|
722 |
|
|
723 |
print 'Commands:' |
|
724 |
for name, cls in _commands[group].items(): |
|
725 |
if name in commands: |
|
726 |
print ' %s %s' % (name.ljust(10), cls.description) |
|
743 |
def print_commands(group): |
|
744 |
description = GROUPS.get(group, '') |
|
745 |
if description: |
|
746 |
puts('\n' + description) |
|
747 |
|
|
748 |
puts('\nCommands:') |
|
749 |
with indent(2): |
|
750 |
for name, cls in _commands[group].items(): |
|
751 |
puts(columns([name, 12], [cls.description, 60])) |
|
727 | 752 |
|
728 | 753 |
|
729 | 754 |
def main(): |
730 |
ch = logging.StreamHandler() |
|
731 |
ch.setFormatter(logging.Formatter('%(message)s')) |
|
732 |
log.addHandler(ch) |
|
733 |
|
|
734 | 755 |
parser = OptionParser(add_help_option=False) |
735 | 756 |
parser.usage = '%prog <group> <command> [options]' |
736 |
parser.add_option('--help', dest='help', action='store_true', |
|
737 |
default=False, help='show this help message and exit') |
|
738 |
parser.add_option('-v', dest='verbose', action='store_true', default=False, |
|
739 |
help='use verbose output') |
|
740 |
parser.add_option('-d', dest='debug', action='store_true', default=False, |
|
741 |
help='use debug output') |
|
757 |
parser.add_option('-h', '--help', dest='help', action='store_true', |
|
758 |
default=False, |
|
759 |
help="Show this help message and exit") |
|
760 |
parser.add_option('--config', dest='config', metavar='PATH', |
|
761 |
help="Specify the path to the configuration file") |
|
762 |
parser.add_option('-i', '--include', dest='include', action='store_true', |
|
763 |
default=False, |
|
764 |
help="Include protocol headers in the output") |
|
765 |
parser.add_option('-s', '--silent', dest='silent', action='store_true', |
|
766 |
default=False, |
|
767 |
help="Silent mode, don't output anything") |
|
768 |
parser.add_option('-v', '--verbose', dest='verbose', action='store_true', |
|
769 |
default=False, |
|
770 |
help="Make the operation more talkative") |
|
771 |
parser.add_option('-V', '--version', dest='version', action='store_true', |
|
772 |
default=False, |
|
773 |
help="Show version number and quit") |
|
742 | 774 |
parser.add_option('-o', dest='options', action='append', |
743 |
metavar='KEY=VAL', |
|
744 |
help='override a config value (can be used multiple times)') |
|
745 |
|
|
746 |
# Do a preliminary parsing, ignore any errors since we will print help |
|
747 |
# anyway if we don't reach the main parsing. |
|
748 |
_error = parser.error |
|
749 |
parser.error = lambda msg: None |
|
750 |
options, args = parser.parse_args(argv) |
|
751 |
parser.error = _error |
|
752 |
|
|
753 |
if options.debug: |
|
754 |
log.setLevel(logging.DEBUG) |
|
755 |
elif options.verbose: |
|
756 |
log.setLevel(logging.INFO) |
|
775 |
default=[], metavar="KEY=VAL", |
|
776 |
help="Override a config values") |
|
777 |
|
|
778 |
if args.contains(['-V', '--version']): |
|
779 |
import kamaki |
|
780 |
print "kamaki %s" % kamaki.__version__ |
|
781 |
exit(0) |
|
782 |
|
|
783 |
if args.contains(['-s', '--silent']): |
|
784 |
level = logging.CRITICAL |
|
785 |
elif args.contains(['-v', '--verbose']): |
|
786 |
level = logging.INFO |
|
757 | 787 |
else: |
758 |
log.setLevel(logging.WARNING)
|
|
788 |
level = logging.WARNING
|
|
759 | 789 |
|
760 |
try: |
|
761 |
config = Config() |
|
762 |
except ConfigError, e: |
|
763 |
log.error('%s', e.args[0]) |
|
764 |
exit(1) |
|
790 |
logging.basicConfig(level=level, format='%(message)s') |
|
765 | 791 |
|
766 |
for option in options.options or []: |
|
767 |
key, sep, val = option.partition('=') |
|
768 |
if not sep: |
|
769 |
log.error('Invalid option "%s"', option) |
|
770 |
exit(1) |
|
771 |
config.override(key.strip(), val.strip()) |
|
792 |
if '--config' in args: |
|
793 |
config_path = args.grouped['--config'].get(0) |
|
794 |
else: |
|
795 |
config_path = os.environ.get(CONFIG_ENV, CONFIG_PATH) |
|
772 | 796 |
|
773 |
apis = config.get('apis').split()
|
|
797 |
config = Config(config_path)
|
|
774 | 798 |
|
775 |
# Find available groups based on the given APIs |
|
776 |
available_groups = [] |
|
799 |
for option in args.grouped.get('-o', []): |
|
800 |
keypath, sep, val = option.partition('=') |
|
801 |
if not sep: |
|
802 |
log.error("Invalid option '%s'", option) |
|
803 |
exit(1) |
|
804 |
section, sep, key = keypath.partition('.') |
|
805 |
if not sep: |
|
806 |
log.error("Invalid option '%s'", option) |
|
807 |
exit(1) |
|
808 |
config.override(section.strip(), key.strip(), val.strip()) |
|
809 |
|
|
810 |
apis = set(['config']) |
|
811 |
for api in ('compute', 'image', 'storage'): |
|
812 |
if config.getboolean(api, 'enable'): |
|
813 |
apis.add(api) |
|
814 |
if config.getboolean('compute', 'cyclades_extensions'): |
|
815 |
apis.add('cyclades') |
|
816 |
if config.getboolean('storage', 'pithos_extensions'): |
|
817 |
apis.add('pithos') |
|
818 |
|
|
819 |
# Remove commands that belong to APIs that are not included |
|
777 | 820 |
for group, group_commands in _commands.items(): |
778 | 821 |
for name, cls in group_commands.items(): |
779 |
if cls.api is None or cls.api in apis: |
|
780 |
available_groups.append(group) |
|
781 |
break |
|
822 |
if cls.api not in apis: |
|
823 |
del group_commands[name] |
|
824 |
if not group_commands: |
|
825 |
del _commands[group] |
|
782 | 826 |
|
783 |
if len(args) < 2:
|
|
827 |
if not args.grouped['_']:
|
|
784 | 828 |
parser.print_help() |
785 |
print_groups(available_groups)
|
|
829 |
print_groups() |
|
786 | 830 |
exit(0) |
787 | 831 |
|
788 |
group = args[1]
|
|
832 |
group = args.grouped['_'][0]
|
|
789 | 833 |
|
790 |
if group not in available_groups:
|
|
834 |
if group not in _commands:
|
|
791 | 835 |
parser.print_help() |
792 |
print_groups(available_groups)
|
|
836 |
print_groups() |
|
793 | 837 |
exit(1) |
794 | 838 |
|
795 |
# Find available commands based on the given APIs |
|
796 |
available_commands = [] |
|
797 |
for name, cls in _commands[group].items(): |
|
798 |
if cls.api is None or cls.api in apis: |
|
799 |
available_commands.append(name) |
|
800 |
continue |
|
801 |
|
|
802 | 839 |
parser.usage = '%%prog %s <command> [options]' % group |
803 | 840 |
|
804 |
if len(args) < 3:
|
|
841 |
if len(args.grouped['_']) == 1:
|
|
805 | 842 |
parser.print_help() |
806 |
print_commands(group, available_commands)
|
|
843 |
print_commands(group) |
|
807 | 844 |
exit(0) |
808 | 845 |
|
809 |
name = args[2]
|
|
846 |
name = args.grouped['_'][1]
|
|
810 | 847 |
|
811 |
if name not in available_commands:
|
|
848 |
if name not in _commands[group]:
|
|
812 | 849 |
parser.print_help() |
813 |
print_commands(group, available_commands)
|
|
850 |
print_commands(group) |
|
814 | 851 |
exit(1) |
815 | 852 |
|
816 |
cls = _commands[group][name]
|
|
853 |
cmd = _commands[group][name]()
|
|
817 | 854 |
|
818 |
syntax = '%s [options]' % cls.syntax if cls.syntax else '[options]'
|
|
855 |
syntax = '%s [options]' % cmd.syntax if cmd.syntax else '[options]'
|
|
819 | 856 |
parser.usage = '%%prog %s %s %s' % (group, name, syntax) |
857 |
parser.description = cmd.description |
|
820 | 858 |
parser.epilog = '' |
821 |
if hasattr(cls, 'update_parser'):
|
|
822 |
cls.update_parser(parser)
|
|
859 |
if hasattr(cmd, 'update_parser'):
|
|
860 |
cmd.update_parser(parser)
|
|
823 | 861 |
|
824 |
options, args = parser.parse_args(argv) |
|
825 |
if options.help: |
|
862 |
if args.contains(['-h', '--help']): |
|
826 | 863 |
parser.print_help() |
827 | 864 |
exit(0) |
828 | 865 |
|
829 |
cmd = cls() |
|
830 |
cmd.config = config |
|
831 |
cmd.options = options |
|
832 |
|
|
833 |
if cmd.api: |
|
834 |
client_name = cmd.api.capitalize() + 'Client' |
|
835 |
client = getattr(clients, client_name, None) |
|
836 |
if client: |
|
837 |
cmd.client = client(config) |
|
866 |
cmd.options, cmd.args = parser.parse_args(argv) |
|
838 | 867 |
|
868 |
api = cmd.api |
|
869 |
if api == 'config': |
|
870 |
cmd.config = config |
|
871 |
elif api in ('compute', 'image', 'storage'): |
|
872 |
token = config.get(api, 'token') or config.get('gobal', 'token') |
|
873 |
url = config.get(api, 'url') |
|
874 |
client_cls = getattr(clients, api) |
|
875 |
kwargs = dict(base_url=url, token=token) |
|
876 |
|
|
877 |
# Special cases |
|
878 |
if api == 'compute' and config.getboolean(api, 'cyclades_extensions'): |
|
879 |
client_cls = clients.cyclades |
|
880 |
elif api == 'storage': |
|
881 |
kwargs['account'] = config.get(api, 'account') |
|
882 |
kwargs['container'] = config.get(api, 'container') |
|
883 |
if config.getboolean(api, 'pithos_extensions'): |
|
884 |
client_cls = clients.pithos |
|
885 |
|
|
886 |
cmd.client = client_cls(**kwargs) |
|
887 |
|
|
839 | 888 |
try: |
840 |
ret = cmd.main(*args[3:])
|
|
889 |
ret = cmd.main(*args.grouped['_'][2:])
|
|
841 | 890 |
exit(ret) |
842 | 891 |
except TypeError as e: |
843 | 892 |
if e.args and e.args[0].startswith('main()'): |
Also available in: Unified diff