diff --git a/sahara/cli/image_pack/api.py b/sahara/cli/image_pack/api.py index b90c1f07..c983ea43 100644 --- a/sahara/cli/image_pack/api.py +++ b/sahara/cli/image_pack/api.py @@ -104,10 +104,22 @@ class ImageRemote(remote.TerminalOnlyRemote): return self.guest.inspect_get_distro() -def pack_image(plugin_name, plugin_version, image_path, root_drive=None, - test_only=False, **kwargs): +def setup_plugins(): plugins_base.setup_plugins() + + +def get_plugin_arguments(plugin_name): + """Gets plugin arguments, as a dict of version to argument list.""" + plugin = plugins_base.PLUGINS.get_plugin(plugin_name) + versions = plugin.get_versions() + return {version: plugin.get_image_arguments(version) + for version in versions} + + +def pack_image(image_path, plugin_name, plugin_version, image_arguments, + root_drive=None, test_only=False): with ImageRemote(image_path, root_drive) as image_remote: - plugin = plugins_base.PLUGINS.get_plugin(plugin_name) reconcile = not test_only - plugin.pack_image(image_remote, reconcile=reconcile, env_map=kwargs) + plugin = plugins_base.PLUGINS.get_plugin(plugin_name) + plugin.pack_image(plugin_version, image_remote, reconcile=reconcile, + image_arguments=image_arguments) diff --git a/sahara/cli/image_pack/cli.py b/sahara/cli/image_pack/cli.py index ba95039a..c2a07140 100644 --- a/sahara/cli/image_pack/cli.py +++ b/sahara/cli/image_pack/cli.py @@ -16,50 +16,36 @@ import sys from oslo_config import cfg from oslo_log import log +import six from sahara.cli.image_pack import api +from sahara.i18n import _ from sahara.i18n import _LI LOG = log.getLogger(__name__) CONF = cfg.CONF - CONF.register_cli_opts([ - cfg.StrOpt( - 'plugin', - required=True, - help="The name of the Sahara plugin for which you would like to " - "generate an image. Use sahara-image-create -p PLUGIN -h to " - "see a set of versions for a specific plugin."), - cfg.StrOpt( - 'plugin-version', - dest='plugin_version', - required=True, - help="The version of the Sahara plugin for which you would like to " - "generate an image. Use sahara-image-create -p PLUGIN -v " - "VERSION -h to see a full set of arguments for a specific plugin " - "and version."), cfg.StrOpt( 'image', required=True, - help="The path to an image to modify. This image will be modified " - "in-place: be sure to target a copy if you wish to maintain a " - "clean master image."), + help=_("The path to an image to modify. This image will be modified " + "in-place: be sure to target a copy if you wish to maintain a " + "clean master image.")), cfg.StrOpt( 'root-filesystem', dest='root_fs', required=False, - help="The filesystem to mount as the root volume on the image. No" - "value is required if only one filesystem is detected."), + help=_("The filesystem to mount as the root volume on the image. No" + "value is required if only one filesystem is detected.")), cfg.BoolOpt( 'test-only', dest='test_only', default=False, - help="If this flag is set, no changes will be made to the image; " - "instead, the script will fail if discrepancies are found " - "between the image and the intended state."), -]) + help=_("If this flag is set, no changes will be made to the image; " + "instead, the script will fail if discrepancies are found " + "between the image and the intended state."))]) def unregister_extra_cli_opt(name): @@ -75,6 +61,47 @@ for extra_opt in ["log-exchange", "host", "port"]: unregister_extra_cli_opt(extra_opt) +def add_plugin_parsers(subparsers): + api.setup_plugins() + for plugin in CONF.plugins: + args_by_version = api.get_plugin_arguments(plugin) + if all(args is NotImplemented for version, args + in six.iteritems(args_by_version)): + continue + plugin_parser = subparsers.add_parser( + plugin, help=_('Image generation for the {plugin} plugin').format( + plugin=plugin)) + version_parsers = plugin_parser.add_subparsers( + title=_("Plugin version"), + dest="version", + help=_("Available versions")) + for version, args in six.iteritems(args_by_version): + if not args: + continue + version_parser = version_parsers.add_parser( + version, help=_('{plugin} version {version}').format( + plugin=plugin, version=version)) + for arg in args: + arg_token = ("--%s" % arg.name if len(arg.name) > 1 else + "-%s" % arg.name) + version_parser.add_argument(arg_token, + dest=arg.target_variable, + help=arg.description, + default=arg.default, + required=arg.required, + choices=arg.choices) + version_parser.set_defaults(args={arg.target_variable + for arg in args}) + + +command_opt = cfg.SubCommandOpt('plugin', + title=_('Plugin'), + help=_('Available plugins'), + handler=add_plugin_parsers) + +CONF.register_cli_opt(command_opt) + + def main(): CONF(project='sahara') @@ -86,8 +113,13 @@ def main(): api.set_logger(LOG) api.set_conf(CONF) - api.pack_image(CONF.plugin, CONF.plugin_version, CONF.image, + plugin = CONF.plugin.name + version = CONF.plugin.version + args = CONF.plugin.args + image_arguments = {arg: getattr(CONF.plugin, arg) for arg in args} + + api.pack_image(CONF.image, plugin, version, image_arguments, CONF.root_fs, CONF.test_only) - LOG.info(_LI("Finished packing image for {plugin} at version {version}" - ).format(plugin=CONF.plugin, version=CONF.plugin_version)) + LOG.info(_LI("Finished packing image for {plugin} at version {version}") + .format(plugin=plugin, version=version)) diff --git a/sahara/plugins/images.py b/sahara/plugins/images.py index 49b1129e..929fd3c0 100644 --- a/sahara/plugins/images.py +++ b/sahara/plugins/images.py @@ -70,6 +70,19 @@ def validate_instance(instance, validators, reconcile=True, **kwargs): validator.validate(remote, reconcile=reconcile, **kwargs) +class ImageArgument(object): + """An argument used by an image manifest.""" + + def __init__(self, name, target_variable, description=None, + default=None, required=False, choices=None): + self.name = name + self.target_variable = target_variable + self.description = description + self.default = default + self.required = required + self.choices = choices + + @six.add_metaclass(abc.ABCMeta) class ImageValidator(object): """Validates the image spawned to an instance via a set of rules.""" diff --git a/sahara/plugins/provisioning.py b/sahara/plugins/provisioning.py index dac28cf4..91fb1409 100644 --- a/sahara/plugins/provisioning.py +++ b/sahara/plugins/provisioning.py @@ -93,6 +93,16 @@ class ProvisioningPluginBase(plugins_base.PluginInterface): def decommission_nodes(self, cluster, instances): pass + @plugins_base.optional + def get_image_arguments(self, hadoop_version): + """Gets the argument set taken by the plugin's image generator""" + return NotImplemented + + @plugins_base.optional + def pack_image(self, hadoop_version, remote, + reconcile=True, image_arguments=None): + pass + @plugins_base.optional def validate_images(self, cluster, reconcile=True): pass diff --git a/sahara/tests/unit/cli/image_pack/test_image_pack_api.py b/sahara/tests/unit/cli/image_pack/test_image_pack_api.py index 10a72ede..68f6073b 100644 --- a/sahara/tests/unit/cli/image_pack/test_image_pack_api.py +++ b/sahara/tests/unit/cli/image_pack/test_image_pack_api.py @@ -43,8 +43,8 @@ class TestSaharaImagePackAPI(base.SaharaTestCase): get_plugin=mock.Mock(return_value=plugin)) api.pack_image( - "plugin_name", "plugin_version", "image_path", - root_drive=None, test_only=False) + "image_path", "plugin_name", "plugin_version", + {"anarg": "avalue"}, root_drive=None, test_only=False) guest.add_drive_opts.assert_called_with("image_path", format="qcow2") guest.set_network.assert_called_with(True) @@ -54,3 +54,19 @@ class TestSaharaImagePackAPI(base.SaharaTestCase): guest.sync.assert_called_once_with() guest.umount_all.assert_called_once_with() guest.close.assert_called_once_with() + + @mock.patch('sahara.cli.image_pack.api.plugins_base') + def test_get_plugin_arguments(self, mock_plugins_base): + api.setup_plugins() + mock_plugins_base.setup_plugins.assert_called_once_with() + mock_PLUGINS = mock.Mock() + mock_plugins_base.PLUGINS = mock_PLUGINS + mock_plugin = mock.Mock() + mock_plugin.get_versions = mock.Mock(return_value=['1']) + mock_plugin.get_image_arguments = mock.Mock( + return_value=["Argument!"]) + mock_PLUGINS.get_plugin = mock.Mock(return_value=mock_plugin) + result = api.get_plugin_arguments('Plugin!') + mock_plugin.get_versions.assert_called_once_with() + mock_plugin.get_image_arguments.assert_called_once_with('1') + self.assertEqual(result, {'1': ['Argument!']})