diff --git a/senlinclient/common/sdk.py b/senlinclient/common/sdk.py index ceccae77..876831ac 100644 --- a/senlinclient/common/sdk.py +++ b/senlinclient/common/sdk.py @@ -27,7 +27,7 @@ def create_connection(prof=None, user_agent=None, **kwargs): if region_name: prof.set_region('clustering', region_name) - prof.set_api_version('clustering', '1.5') + prof.set_api_version('clustering', '1.7') try: conn = connection.Connection(profile=prof, user_agent=user_agent, **kwargs) diff --git a/senlinclient/common/utils.py b/senlinclient/common/utils.py index fbbae17a..39c20e25 100644 --- a/senlinclient/common/utils.py +++ b/senlinclient/common/utils.py @@ -225,6 +225,22 @@ def format_parameters(params, parse_semicolon=True): return parameters +def format_json_parameter(param): + '''Return JSON dict from JSON formatted param. + + :parameter param JSON formatted string + :return JSON dict + ''' + if not param: + return {} + + try: + return jsonutils.loads(param) + except ValueError: + msg = _('Malformed parameter(%s). Use the JSON format.') % param + raise exc.CommandError(msg) + + def get_spec_content(filename): with open(filename, 'r') as f: try: diff --git a/senlinclient/plugin.py b/senlinclient/plugin.py index 4741703e..0f60b2c2 100644 --- a/senlinclient/plugin.py +++ b/senlinclient/plugin.py @@ -23,7 +23,7 @@ LOG = logging.getLogger(__name__) DEFAULT_CLUSTERING_API_VERSION = '1' API_VERSION_OPTION = 'os_clustering_api_version' API_NAME = 'clustering' -CURRENT_API_VERSION = '1.5' +CURRENT_API_VERSION = '1.7' def make_client(instance): diff --git a/senlinclient/tests/unit/v1/test_node.py b/senlinclient/tests/unit/v1/test_node.py index 24335cc0..ed014eec 100644 --- a/senlinclient/tests/unit/v1/test_node.py +++ b/senlinclient/tests/unit/v1/test_node.py @@ -458,6 +458,158 @@ class TestNodeRecover(TestNode): self.assertIn('Node not found: node1', str(error)) +class TestNodeAdopt(TestNode): + defaults = { + "identity": "fake-resource-id", + "metadata": {}, + "name": "my_node", + "overrides": {}, + "role": None, + "snapshot": False, + "type": "os.nova.server-1.0" + } + + def setUp(self): + super(TestNodeAdopt, self).setUp() + self.cmd = osc_node.AdoptNode(self.app, None) + fake_node = mock.Mock( + action="2366d440-c73e-4961-9254-6d1c3af7c167", + cluster_id="", + created_at=None, + data={}, + domain=None, + id="0df0931b-e251-4f2e-8719-4ebfda3627ba", + index=-1, + init_time="2015-03-05T08:53:15", + metadata={}, + physical_id=None, + profile_id="edc63d0a-2ca4-48fa-9854-27926da76a4a", + profile_name="mystack", + project_id="6e18cc2bdbeb48a5b3cad2dc499f6804", + role="master", + status="INIT", + status_reason="Initializing", + updated_at=None, + user_id="5e5bf8027826429c96af157f68dc9072" + ) + fake_node.name = "my_node" + fake_node.to_dict = mock.Mock(return_value={}) + + self.mock_client.adopt_node = mock.Mock(return_value=fake_node) + self.mock_client.get_node = mock.Mock(return_value=fake_node) + + def test_node_adopt_defaults(self): + arglist = ['--identity', 'fake-resource-id', + '--type', 'os.nova.server-1.0', + '--name', 'my_node'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.adopt_node.assert_called_with(False, **self.defaults) + + def test_node_adopt_with_metadata(self): + arglist = ['--identity', 'fake-resource-id', + '--type', 'os.nova.server-1.0', + '--metadata', 'key1=value1;key2=value2', + '--name', 'my_node'] + kwargs = copy.deepcopy(self.defaults) + kwargs['metadata'] = {'key1': 'value1', 'key2': 'value2'} + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.adopt_node.assert_called_with(False, **kwargs) + + def test_node_adopt_with_override(self): + arglist = ['--identity', 'fake-resource-id', + '--type', 'os.nova.server-1.0', + '--overrides', + '{"networks": [{"network": "fake-net-name"}]}', + '--name', 'my_node'] + kwargs = copy.deepcopy(self.defaults) + kwargs['overrides'] = {'networks': [{'network': 'fake-net-name'}]} + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.adopt_node.assert_called_with(False, **kwargs) + + def test_node_adopt_with_role(self): + arglist = ['--identity', 'fake-resource-id', + '--type', 'os.nova.server-1.0', + '--role', 'master', + '--name', 'my_node'] + kwargs = copy.deepcopy(self.defaults) + kwargs['role'] = 'master' + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.adopt_node.assert_called_with(False, **kwargs) + + def test_node_adopt_with_snapshot(self): + arglist = ['--identity', 'fake-resource-id', + '--type', 'os.nova.server-1.0', + '--snapshot', + '--name', 'my_node'] + parsed_args = self.check_parser(self.cmd, arglist, []) + kwargs = copy.deepcopy(self.defaults) + kwargs['snapshot'] = True + self.cmd.take_action(parsed_args) + self.mock_client.adopt_node.assert_called_with(False, **kwargs) + + +class TestNodeAdoptPreview(TestNode): + defaults = { + "identity": "fake-resource-id", + "overrides": {}, + "snapshot": False, + "type": "os.nova.server-1.0" + } + + def setUp(self): + super(TestNodeAdoptPreview, self).setUp() + self.cmd = osc_node.AdoptNode(self.app, None) + self.fake_node_preview = { + "node_profile": { + "node_preview": { + "properties": { + + }, + "type": "os.nova.server", + "version": "1.0"} + } + } + + self.mock_client.adopt_node = mock.Mock( + return_value=self.fake_node_preview) + self.mock_client.get_node = mock.Mock( + return_value=self.fake_node_preview) + + def test_node_adopt_preview_default(self): + arglist = ['--identity', 'fake-resource-id', + '--type', 'os.nova.server-1.0', + '--preview'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.mock_client.adopt_node.assert_called_with(True, **self.defaults) + + def test_node_adopt_preview_with_overrides(self): + arglist = ['--identity', 'fake-resource-id', + '--type', 'os.nova.server-1.0', + '--overrides', + '{"networks": [{"network": "fake-net-name"}]}', + '--preview'] + parsed_args = self.check_parser(self.cmd, arglist, []) + kwargs = copy.deepcopy(self.defaults) + kwargs['overrides'] = {'networks': [{'network': 'fake-net-name'}]} + self.cmd.take_action(parsed_args) + self.mock_client.adopt_node.assert_called_with(True, **kwargs) + + def test_node_adopt_preview_with_snapshot(self): + arglist = ['--identity', 'fake-resource-id', + '--type', 'os.nova.server-1.0', + '--snapshot', '--preview'] + parsed_args = self.check_parser(self.cmd, arglist, []) + kwargs = copy.deepcopy(self.defaults) + kwargs['snapshot'] = True + self.cmd.take_action(parsed_args) + self.mock_client.adopt_node.assert_called_with(True, **kwargs) + + class TestNodeOp(TestNode): response = {"action": "1db0f5c5-9183-4c47-9ef1-a5a97402a2c1"} diff --git a/senlinclient/tests/unit/v1/test_shell.py b/senlinclient/tests/unit/v1/test_shell.py index 5f9dc8ea..6b79c7b4 100644 --- a/senlinclient/tests/unit/v1/test_shell.py +++ b/senlinclient/tests/unit/v1/test_shell.py @@ -1561,6 +1561,76 @@ class ShellTest(testtools.TestCase): service.create_node.assert_called_once_with(**attrs) mock_show.assert_called_once_with(service, 'node_id') + @mock.patch.object(sh, '_show_node') + def test_do_node_adopt(self, mock_show): + args = { + 'identity': 'fake-resoruce-id', + 'name': 'adopt-node1', + 'role': 'master', + 'metadata': ['user=demo'], + 'snapshot': None, + 'overrides': '{"networks": [{"network": "fake-net-name"}]}', + 'type': 'os.nova.server-1.0', + 'preview': False + } + args = self._make_args(args) + attrs = { + 'identity': 'fake-resoruce-id', + 'name': 'adopt-node1', + 'role': 'master', + 'metadata': {'user': 'demo'}, + 'overrides': {'networks': [{'network': 'fake-net-name'}]}, + 'snapshot': None, + 'type': 'os.nova.server-1.0', + } + service = mock.Mock() + node = mock.Mock() + node.id = 'node_id' + service.adopt_node.return_value = node + sh.do_node_adopt(service, args) + service.adopt_node.assert_called_once_with(**attrs) + mock_show.assert_called_once_with(service, 'node_id') + + @mock.patch.object(utils, 'print_dict') + @mock.patch.object(utils, 'nested_dict_formatter') + def test_do_node_adopt_preview(self, mock_nest, mock_print): + args = { + 'identity': 'fake-resoruce-id', + 'snapshot': None, + 'overrides': '{"networks": [{"network": "fake-net-name"}]}', + 'type': 'os.nova.server-1.0', + 'preview': True + } + args = self._make_args(args) + attrs = { + 'identity': 'fake-resoruce-id', + 'overrides': {'networks': [{'network': 'fake-net-name'}]}, + 'snapshot': None, + 'type': 'os.nova.server-1.0', + } + + fake_preview = { + "node_profile": { + "node_preview": { + "properties": { + }, + "type": "os.nova.server", + "version": "1.0"} + } + } + + service = mock.Mock() + service.adopt_node.return_value = fake_preview + sh.do_node_adopt(service, args) + service.adopt_node.assert_called_once_with(True, **attrs) + + formatters = {} + formatters['node_preview'] = utils.nested_dict_formatter( + ['type', 'version', 'properties'], + ['property', 'value']) + mock_print.assert_called_once_with(fake_preview['node_profile'], + formatters=formatters) + @mock.patch.object(sh, '_show_node') def test_do_node_show(self, mock_show): service = mock.Mock() diff --git a/senlinclient/v1/client.py b/senlinclient/v1/client.py index 9c1f2c69..bf0b3a51 100644 --- a/senlinclient/v1/client.py +++ b/senlinclient/v1/client.py @@ -346,6 +346,15 @@ class Client(object): """ return self.service.create_node(**attrs) + def adopt_node(self, preview=False, **attrs): + """Adopt a node + + Doc link: + https://developer.openstack.org/api-ref/clustering/#adopt-node + https://developer.openstack.org/api-ref/clustering/#adopt-node-preview + """ + return self.service.adopt_node(preview, **attrs) + def get_node(self, node, details=False): """Show node details diff --git a/senlinclient/v1/node.py b/senlinclient/v1/node.py index 6c3eb235..04d95b39 100644 --- a/senlinclient/v1/node.py +++ b/senlinclient/v1/node.py @@ -163,6 +163,7 @@ def _show_node(senlin_client, node_id, show_details=False): if show_details and data['details']: formatters['details'] = senlin_utils.nested_dict_formatter( list(data['details'].keys()), ['property', 'value']) + columns = sorted(data.keys()) return columns, utils.get_dict_properties(data, columns, formatters=formatters) @@ -398,6 +399,102 @@ class RecoverNode(command.Command): % {'nid': nid, 'action': resp['action']}) +class AdoptNode(command.ShowOne): + """Adopt (or preview) the node.""" + + log = logging.getLogger(__name__ + ".AdoptNode") + + def get_parser(self, prog_name): + parser = super(AdoptNode, self).get_parser(prog_name) + parser.add_argument( + '--identity', + metavar='', + required=True, + help=_('Physical resource id.')) + parser.add_argument( + '--type', + metavar='', + required=True, + help=_('The name of the profile type.') + ) + parser.add_argument( + '--role', + metavar='', + help=_('Role for this node in the specific cluster.') + ) + parser.add_argument( + '--metadata', + metavar='<"key1=value1;key2=value2...">', + help=_('Metadata values to be attached to the node. ' + 'This can be specified multiple times, or once with ' + 'key-value pairs separated by a semicolon.'), + action='append' + ) + parser.add_argument( + '--name', + metavar='', + help=_('Name of the node to adopt.') + ) + parser.add_argument( + '--overrides', + metavar='', + help=_('JSON formatted specification for overriding this node ' + 'properties.') + ) + parser.add_argument( + '--preview', + default=False, + help=_('Whether preview the node adopt request. If set, ' + 'only previewing this node and do not adopt.'), + action='store_true', + ) + parser.add_argument( + '--snapshot', + default=False, + help=_('Whether a shapshot of the existing physical object ' + 'should be created before the object is adopted as ' + 'a node.'), + action='store_true' + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + senlin_client = self.app.client_manager.clustering + + preview = True if parsed_args.preview else False + attrs = { + 'identity': parsed_args.identity, + 'overrides': senlin_utils.format_json_parameter( + parsed_args.overrides), + 'snapshot': parsed_args.snapshot, + 'type': parsed_args.type + } + + if not preview: + attrs.update({ + 'name': parsed_args.name, + 'role': parsed_args.role, + 'metadata': senlin_utils.format_parameters( + parsed_args.metadata), + }) + + node = senlin_client.adopt_node(preview, **attrs) + + if not preview: + return _show_node(senlin_client, node.id) + else: + formatters = {} + formatters['node_preview'] = senlin_utils.nested_dict_formatter( + ['type', 'version', 'properties'], + ['property', 'value']) + data = node['node_profile'] + columns = sorted(data.keys()) + return columns, utils.get_dict_properties(data, columns, + formatters=formatters) + + class NodeOp(command.Lister): """Perform an operation on a node.""" log = logging.getLogger(__name__ + ".NodeOp") diff --git a/senlinclient/v1/shell.py b/senlinclient/v1/shell.py index b48646f0..0b5b5ca8 100644 --- a/senlinclient/v1/shell.py +++ b/senlinclient/v1/shell.py @@ -1329,6 +1329,71 @@ def do_node_create(service, args): _show_node(service, node.id) +@utils.arg('-i', '--identity', metavar='', required=True, + help=_('Physical resource id.')) +@utils.arg('-t', '--type', metavar='', required=True, + help=_('The name of the profile type.')) +@utils.arg('-r', '--role', metavar='', + help=_('Role for this node in the specific cluster.')) +@utils.arg('-M', '--metadata', metavar='<"KEY1=VALUE1;KEY2=VALUE2...">', + help=_('Metadata values to be attached to the node. ' + 'This can be specified multiple times, or once with ' + 'key-value pairs separated by a semicolon.'), + action='append') +@utils.arg('-n', '--name', metavar='', + help=_('The name for the node.')) +@utils.arg('-o', '--overrides', metavar='', + help=_('JSON formatted specification for overriding this node ' + 'properties.')) +@utils.arg('-p', '--preview', default=False, + help=_('Whether preview the node adopt request. If set, ' + 'only previewing this node and do not adopt.'), + action='store_true') +@utils.arg('-s', '--snapshot', default=False, + help=_('Whether a shapshot of the existing physical object should ' + 'be created before the object is adopted as a node.'), + action='store_true') +def do_node_adopt(service, args): + """Adopt (or preview) a node.""" + show_deprecated('senlin node-adopt', 'openstack cluster node adopt') + if args.preview: + _do_node_adopt_preview(service, args) + else: + _do_node_adopt(service, args) + + +def _do_node_adopt_preview(service, args): + attrs = { + 'identity': args.identity, + 'overrides': utils.format_json_parameter(args.overrides), + 'snapshot': args.snapshot, + 'type': args.type + } + + node = service.adopt_node(True, **attrs) + + formatters = {} + formatters['node_preview'] = utils.nested_dict_formatter( + ['type', 'version', 'properties'], + ['property', 'value']) + utils.print_dict(node['node_profile'], formatters=formatters) + + +def _do_node_adopt(service, args): + attrs = { + 'identity': args.identity, + 'name': args.name, + 'role': args.role, + 'metadata': utils.format_parameters(args.metadata), + 'overrides': utils.format_json_parameter(args.overrides), + 'snapshot': args.snapshot, + 'type': args.type + } + + node = service.adopt_node(**attrs) + _show_node(service, node.id) + + @utils.arg('-D', '--details', default=False, action="store_true", help=_('Include physical object details.')) @utils.arg('id', metavar='', diff --git a/setup.cfg b/setup.cfg index c1e23ffe..df870407 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,6 +43,7 @@ openstack.clustering.v1 = cluster_members_add = senlinclient.v1.cluster:ClusterNodeAdd cluster_members_del = senlinclient.v1.cluster:ClusterNodeDel cluster_members_replace = senlinclient.v1.cluster:ClusterNodeReplace + cluster_node_adopt = senlinclient.v1.node:AdoptNode cluster_node_check = senlinclient.v1.node:CheckNode cluster_node_create = senlinclient.v1.node:CreateNode cluster_node_delete = senlinclient.v1.node:DeleteNode