diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index 5cc5deab..5d8ed2a6 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -20,6 +20,7 @@ from cliff import show from openstackclient.common import exceptions as exc from openstackclient.common import parseractions from openstackclient.common import utils +from six.moves.urllib import request from heatclient.common import http from heatclient.common import template_utils @@ -566,3 +567,88 @@ def _list(client, args=None): columns, (utils.get_item_properties(s, columns) for s in data) ) + + +class AdoptStack(show.ShowOne): + """Adopt a stack.""" + + log = logging.getLogger(__name__ + '.AdoptStack') + + def get_parser(self, prog_name): + parser = super(AdoptStack, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_('Name of the stack to adopt') + ) + parser.add_argument( + '-e', '--environment', + metavar='', + action='append', + help=_('Path to the environment. Can be specified multiple times') + ) + parser.add_argument( + '--timeout', + metavar='', + type=int, + help=_('Stack creation timeout in minutes') + ) + parser.add_argument( + '--adopt-file', + metavar='', + required=True, + help=_('Path to adopt stack data file') + ) + parser.add_argument( + '--enable-rollback', + action='store_true', + help=_('Enable rollback on create/update failure') + ) + parser.add_argument( + '--parameter', + metavar='', + action='append', + help=_('Parameter values used to create the stack. Can be ' + 'specified multiple times') + ) + parser.add_argument( + '--wait', + action='store_true', + help=_('Wait until stack adopt completes') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + client = self.app.client_manager.orchestration + + env_files, env = ( + template_utils.process_multiple_environments_and_files( + env_paths=parsed_args.environment)) + + adopt_url = heat_utils.normalise_file_path_to_url( + parsed_args.adopt_file) + adopt_data = request.urlopen(adopt_url).read().decode('utf-8') + + fields = { + 'stack_name': parsed_args.name, + 'disable_rollback': not parsed_args.enable_rollback, + 'adopt_stack_data': adopt_data, + 'parameters': heat_utils.format_parameters(parsed_args.parameter), + 'files': dict(list(env_files.items())), + 'environment': env, + 'timeout': parsed_args.timeout + } + + stack = client.stacks.create(**fields)['stack'] + + if parsed_args.wait: + if not utils.wait_for_status(client.stacks.get, parsed_args.name, + status_field='stack_status', + success_status='create_complete', + error_status=['create_failed']): + msg = _('Stack %s failed to create.') % parsed_args.name + raise exc.CommandError(msg) + + return _show_stack(client, stack['id'], format='table', short=True) diff --git a/heatclient/tests/test_templates/adopt.json b/heatclient/tests/test_templates/adopt.json new file mode 100644 index 00000000..ac57ec76 --- /dev/null +++ b/heatclient/tests/test_templates/adopt.json @@ -0,0 +1,32 @@ +{ + 'files': {}, + 'status': 'COMPLETE', + 'name': 'my_stack', + 'tags': None, + 'stack_user_project_id': '123456', + 'environment': {}, + 'template': { + 'heat_template_version': '2016-04-08', + 'resources': { + 'thing': { + 'type': 'OS::Heat::TestResource' + } + } + }, + 'action': 'CREATE', + 'project_id': '56789', + 'id': '2468', + 'resources': { + 'thing': { + 'status': 'COMPLETE', + 'name': 'thing', + 'resource_data': { + 'value': 'test_string', + }, + 'resource_id': 'my_stack-thing-1234', + 'action': 'CREATE', + 'type': 'OS::Heat::TestResource', + 'metadata': {} + } + } +} diff --git a/heatclient/tests/unit/osc/v1/test_stack.py b/heatclient/tests/unit/osc/v1/test_stack.py index b6fd99c4..25e297f9 100644 --- a/heatclient/tests/unit/osc/v1/test_stack.py +++ b/heatclient/tests/unit/osc/v1/test_stack.py @@ -520,3 +520,68 @@ class TestStackList(TestStack): parsed_args = self.check_parser(self.cmd, arglist, []) self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + +class TestStackAdopt(TestStack): + + adopt_file = 'heatclient/tests/test_templates/adopt.json' + + with open(adopt_file, 'r') as f: + adopt_data = f.read() + + defaults = { + 'stack_name': 'my_stack', + 'disable_rollback': True, + 'adopt_stack_data': adopt_data, + 'parameters': {}, + 'files': {}, + 'environment': {}, + 'timeout': None + } + + def setUp(self): + super(TestStackAdopt, self).setUp() + self.cmd = stack.AdoptStack(self.app, None) + self.stack_client.create = mock.MagicMock( + return_value={'stack': {'id': '1234'}}) + + def test_stack_adopt_defaults(self): + arglist = ['my_stack', '--adopt-file', self.adopt_file] + cols = ['id', 'stack_name', 'description', 'creation_time', + 'updated_time', 'stack_status', 'stack_status_reason'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.stack_client.create.assert_called_with(**self.defaults) + self.assertEqual(cols, columns) + + def test_stack_adopt_enable_rollback(self): + arglist = ['my_stack', '--adopt-file', self.adopt_file, + '--enable-rollback'] + kwargs = copy.deepcopy(self.defaults) + kwargs['disable_rollback'] = False + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.create.assert_called_with(**kwargs) + + def test_stack_adopt_wait(self): + arglist = ['my_stack', '--adopt-file', self.adopt_file, '--wait'] + self.stack_client.get = mock.MagicMock(return_value=( + stacks.Stack(None, {'stack_status': 'CREATE_COMPLETE'}))) + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.cmd.take_action(parsed_args) + + self.stack_client.create.assert_called_with(**self.defaults) + self.stack_client.get.assert_called_with(**{'stack_id': '1234'}) + + def test_stack_adopt_wait_fail(self): + arglist = ['my_stack', '--adopt-file', self.adopt_file, '--wait'] + self.stack_client.get = mock.MagicMock(return_value=( + stacks.Stack(None, {'stack_status': 'CREATE_FAILED'}))) + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) diff --git a/setup.cfg b/setup.cfg index 1d034e45..19e12b7b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,6 +35,7 @@ openstack.orchestration.v1 = stack_create = heatclient.osc.v1.stack:CreateStack stack_update = heatclient.osc.v1.stack:UpdateStack stack_snapshot_list = heatclient.osc.v1.snapshot:ListSnapshot + stack_adopt = heatclient.osc.v1.stack:AdoptStack [global]