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 |
|
print
|
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 |
|
print
|
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()'):
|