diff --git a/valence/api/route.py b/valence/api/route.py index eaa7673..57a633e 100644 --- a/valence/api/route.py +++ b/valence/api/route.py @@ -66,8 +66,10 @@ api.add_resource(api_root.Root, '/', endpoint='root') api.add_resource(v1_version.V1, '/v1', endpoint='v1') # Node(s) operations -api.add_resource(v1_nodes.NodesList, '/v1/nodes', endpoint='nodes') -api.add_resource(v1_nodes.Nodes, '/v1/nodes/', endpoint='node') +api.add_resource(v1_nodes.Nodes, '/v1/nodes', endpoint='nodes') +api.add_resource(v1_nodes.Node, + '/v1/nodes/', + endpoint='node') api.add_resource(v1_nodes.NodesStorage, '/v1/nodes//storages', endpoint='nodes_storages') diff --git a/valence/api/v1/nodes.py b/valence/api/v1/nodes.py index 7552b00..dd93e3e 100644 --- a/valence/api/v1/nodes.py +++ b/valence/api/v1/nodes.py @@ -12,39 +12,36 @@ # License for the specific language governing permissions and limitations # under the License. -import logging - from flask import request from flask_restful import abort from flask_restful import Resource from six.moves import http_client from valence.common import utils -from valence.redfish import redfish - -LOG = logging.getLogger(__name__) - - -class NodesList(Resource): - - def get(self): - return utils.make_response(http_client.OK, - redfish.list_nodes()) - - def post(self): - return utils.make_response( - http_client.OK, redfish.compose_node(request.get_json())) +from valence.controller import nodes class Nodes(Resource): - def get(self, nodeid): - return utils.make_response(http_client.OK, - redfish.get_node_by_id(nodeid)) + def get(self): + return utils.make_response( + http_client.OK, nodes.Node.list_composed_nodes()) - def delete(self, nodeid): - return utils.make_response(http_client.OK, - redfish.delete_composednode(nodeid)) + def post(self): + return utils.make_response( + http_client.OK, nodes.Node.compose_node(request.get_json())) + + +class Node(Resource): + + def get(self, node_uuid): + return utils.make_response( + http_client.OK, + nodes.Node.get_composed_node_by_uuid(node_uuid)) + + def delete(self, node_uuid): + return utils.make_response( + http_client.OK, nodes.Node.delete_composed_node(node_uuid)) class NodesStorage(Resource): diff --git a/valence/common/utils.py b/valence/common/utils.py index 612df5f..36b8410 100644 --- a/valence/common/utils.py +++ b/valence/common/utils.py @@ -21,6 +21,7 @@ import logging import flask +from oslo_utils import uuidutils import six LOG = logging.getLogger(__name__) @@ -106,3 +107,8 @@ def make_response(status_code, content="", headers=None): raise ValueError("Response headers should be dict object.") return response + + +def generate_uuid(): + """Generate uniform format uuid""" + return uuidutils.generate_uuid() diff --git a/valence/controller/nodes.py b/valence/controller/nodes.py new file mode 100644 index 0000000..f075c2a --- /dev/null +++ b/valence/controller/nodes.py @@ -0,0 +1,99 @@ +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import six + +from valence.common import utils +from valence.db import api as db_api +from valence.redfish import redfish + + +class Node(object): + + @staticmethod + def _show_node_brief_info(node_info): + return {key: node_info[key] for key in six.iterkeys(node_info) + if key in ["uuid", "name", "links"]} + + @classmethod + def compose_node(cls, request_body): + """Compose new node + + param request_body: parameter for node composition + return: brief info of this new composed node + """ + + # Call redfish to compose new node + composed_node = redfish.compose_node(request_body) + + composed_node["uuid"] = utils.generate_uuid() + + # Only store the minimum set of composed node info into backend db, + # since other fields like power status may be changed and valence is + # not aware. + node_db = {"uuid": composed_node["uuid"], + "name": composed_node["name"], + "index": composed_node["index"], + "links": composed_node["links"]} + db_api.Connection.create_composed_node(node_db) + + return cls._show_node_brief_info(composed_node) + + @classmethod + def get_composed_node_by_uuid(cls, node_uuid): + """Get composed node details + + Get the detail of specific composed node. In some cases db data may be + inconsistent with podm side, like user directly operate podm, not + through valence api. So compare it with node info from redfish, and + update db if it's inconsistent. + + param node_uuid: uuid of composed node + return: detail of this composed node + """ + + node_db = db_api.Connection.get_composed_node_by_uuid(node_uuid)\ + .as_dict() + node_hw = redfish.get_node_by_id(node_db["index"]) + + # Add those fields of composed node from db + node_hw.update(node_db) + + return node_hw + + @classmethod + def delete_composed_node(cls, node_uuid): + """Delete a composed node + + param node_uuid: uuid of composed node + return: message of this deletion + """ + + # Get node detail from db, and map node uuid to index + index = db_api.Connection.get_composed_node_by_uuid(node_uuid).index + + # Call redfish to delete node, and delete corresponding entry in db + message = redfish.delete_composed_node(index) + db_api.Connection.delete_composed_node(node_uuid) + + return message + + @classmethod + def list_composed_nodes(cls): + """List all composed node + + return: brief info of all composed node + """ + return [cls._show_node_brief_info(node_info.as_dict()) + for node_info in db_api.Connection.list_composed_nodes()] diff --git a/valence/db/api.py b/valence/db/api.py index 20b104a..a083939 100644 --- a/valence/db/api.py +++ b/valence/db/api.py @@ -115,3 +115,47 @@ class Connection(object): :returns: A list of all flavors. """ return cls.dbdriver.list_flavors() + + @classmethod + def create_composed_node(cls, values): + """Create a new composed node. + + :values: The properties for this new composed node. + :returns: A composed node created. + """ + return cls.dbdriver.create_composed_node(values) + + @classmethod + def get_composed_node_by_uuid(cls, composed_node_uuid): + """Get specific composed node by its uuid + + :param composed_node_uuid: The uuid of composed node. + :returns: A composed node with this uuid. + """ + return cls.dbdriver.get_composed_node_by_uuid(composed_node_uuid) + + @classmethod + def delete_composed_node(cls, composed_node_uuid): + """Delete specific composed node by its uuid + + :param composed_node_uuid: The uuid of composed node. + """ + cls.dbdriver.delete_composed_node(composed_node_uuid) + + @classmethod + def update_composed_node(cls, composed_node_uuid, values): + """Update properties of a composed node. + + :param composed_node_uuid: The uuid of composed node. + :values: The properties to be updated. + :returns: A composed node model after updated. + """ + return cls.dbdriver.update_composed_node(composed_node_uuid, values) + + @classmethod + def list_composed_nodes(cls): + """Get a list of all composed nodes. + + :returns: A list of all composed node. + """ + return cls.dbdriver.list_composed_nodes() diff --git a/valence/db/etcd_db.py b/valence/db/etcd_db.py index ea4ec33..078400f 100644 --- a/valence/db/etcd_db.py +++ b/valence/db/etcd_db.py @@ -20,7 +20,8 @@ from valence.db import models etcd_directories = [ models.PodManager.path, - models.Flavor.path + models.Flavor.path, + models.ComposedNode.path ] etcd_client = etcd.Client(config.etcd_host, config.etcd_port) diff --git a/valence/db/etcd_driver.py b/valence/db/etcd_driver.py index 1e94047..79715db 100644 --- a/valence/db/etcd_driver.py +++ b/valence/db/etcd_driver.py @@ -39,6 +39,8 @@ def translate_to_models(etcd_resp, model_type): ret = models.PodManager(**data) elif model_type == models.Flavor.path: ret = models.Flavor(**data) + elif model_type == models.ComposedNode.path: + ret = models.ComposedNode(**data) else: # TODO(lin.a.yang): after exception module got merged, raise # valence specific InvalidParameter exception here @@ -149,3 +151,51 @@ class EtcdDriver(object): flavor, models.Flavor.path)) return flavors + + def create_composed_node(self, values): + composed_node = models.ComposedNode(**values) + composed_node.save() + + return composed_node + + def get_composed_node_by_uuid(self, composed_node_uuid): + try: + resp = self.client.read(models.ComposedNode.etcd_path( + composed_node_uuid)) + except etcd.EtcdKeyNotFound: + # TODO(lin.a.yang): after exception module got merged, raise + # valence specific DBNotFound exception here + raise Exception( + 'Composed node not found {0} in database.'.format( + composed_node_uuid)) + + return translate_to_models(resp, models.ComposedNode.path) + + def delete_composed_node(self, composed_node_uuid): + composed_node = self.get_composed_node_by_uuid(composed_node_uuid) + composed_node.delete() + + def update_composed_node(self, composed_node_uuid, values): + composed_node = self.get_composed_node_by_uuid(composed_node_uuid) + composed_node.update(values) + + return composed_node + + def list_composed_nodes(self): + # TODO(lin.a.yang): support filter for listing composed_node + + try: + resp = getattr(self.client.read(models.ComposedNode.path), + 'children', None) + except etcd.EtcdKeyNotFound: + LOG.error("Path '/nodes' does not exist, seems etcd server " + "was not initialized appropriately.") + raise + + composed_nodes = [] + for node in resp: + if node.value is not None: + composed_nodes.append(translate_to_models( + node, models.ComposedNode.path)) + + return composed_nodes diff --git a/valence/db/models.py b/valence/db/models.py index 4e47a8c..4d4f40a 100644 --- a/valence/db/models.py +++ b/valence/db/models.py @@ -189,3 +189,23 @@ class Flavor(ModelBaseWithTimeStamp): 'validate': types.Dict.validate } } + + +class ComposedNode(ModelBaseWithTimeStamp): + + path = "/nodes" + + fields = { + 'uuid': { + 'validate': types.Text.validate + }, + 'name': { + 'validate': types.Text.validate + }, + 'index': { + 'validate': types.Text.validate + }, + 'links': { + 'validate': types.List(types.Dict).validate + } + } diff --git a/valence/redfish/redfish.py b/valence/redfish/redfish.py index 5b40ef5..fb42c64 100644 --- a/valence/redfish/redfish.py +++ b/valence/redfish/redfish.py @@ -490,7 +490,7 @@ def compose_node(request_body): if assemble_resp.status_code != http_client.NO_CONTENT: # Delete node if assemble failed - delete_composednode(node_index) + delete_composed_node(node_index) raise exception.RedfishException(assemble_resp.json(), status_code=resp.status_code) else: @@ -498,10 +498,10 @@ def compose_node(request_body): LOG.debug('Successfully assembled node: ' + node_url) # Return new composed node index - return get_node_by_id(node_index, show_detail=False) + return get_node_by_id(node_index) -def delete_composednode(nodeid): +def delete_composed_node(nodeid): nodes_url = get_base_resource_url("Nodes") delete_url = nodes_url + '/' + str(nodeid) resp = send_request(delete_url, "DELETE") diff --git a/valence/tests/unit/controller/fakes.py b/valence/tests/unit/controller/fakes.py new file mode 100644 index 0000000..2e45132 --- /dev/null +++ b/valence/tests/unit/controller/fakes.py @@ -0,0 +1,52 @@ +# copyright (c) 2016 Intel, Inc. +# +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +def get_test_composed_node(**kwargs): + return { + 'uuid': kwargs.get('uuid', 'ea8e2a25-2901-438d-8157-de7ffd68d051'), + 'name': kwargs.get('name', 'fake_name'), + 'description': kwargs.get('description', 'fake_description'), + 'boot_source': kwargs.get('boot_source', 'Hdd'), + 'health_status': kwargs.get('health_status', 'OK'), + 'index': kwargs.get('index', '1'), + 'node_power_state': kwargs.get('node_power_state', 'On'), + 'node_state': kwargs.get('node_state', 'Assembling'), + 'pooled_group_id': kwargs.get('bookmark_link', 'None'), + 'target_boot_source': kwargs.get('target_boot_source', 'Hdd'), + 'links': kwargs.get( + 'links', + [{'href': 'http://127.0.0.1:8181/v1/nodes/' + '7be5bc10-dcdf-11e6-bd86-934bc6947c55/', + 'rel': 'self'}, + {'href': 'http://127.0.0.1:8181/nodes/' + '7be5bc10-dcdf-11e6-bd86-934bc6947c55/', + 'rel': 'bookmark'}]), + 'metadata': kwargs.get( + 'metadata', + {'memory': [{'data_width_bit': 0, + 'speed_mhz': 2400, + 'total_memory_mb': 8192}], + 'network': [{'ipv4': [{'address': '192.168.0.10', + 'gateway': '192.168.0.1', + 'subnet_mask': '255.255.252.0'}], + 'mac': 'e9:47:d3:60:64:66', + 'speed_mbps': 0, + 'status': 'Enabled', + 'vlans': [{'status': 'Enabled', + 'vlanid': 99}]}], + 'processor': [{'instruction_set': None, + 'model': None, + 'speed_mhz': 3700, + 'total_core': 0}]}) + } diff --git a/valence/tests/unit/controller/test_nodes.py b/valence/tests/unit/controller/test_nodes.py new file mode 100644 index 0000000..bc488f3 --- /dev/null +++ b/valence/tests/unit/controller/test_nodes.py @@ -0,0 +1,117 @@ +# copyright (c) 2016 Intel, Inc. +# +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import unittest + +import mock + +from valence.controller import nodes +from valence.tests.unit.controller import fakes +from valence.tests.unit.db import utils as test_utils + + +class TestAPINodes(unittest.TestCase): + + def test_show_node_brief_info(self): + """Test only show node brief info""" + node_info = fakes.get_test_composed_node() + expected = { + "name": "fake_name", + "uuid": "ea8e2a25-2901-438d-8157-de7ffd68d051", + "links": [{'href': 'http://127.0.0.1:8181/v1/nodes/' + '7be5bc10-dcdf-11e6-bd86-934bc6947c55/', + 'rel': 'self'}, + {'href': 'http://127.0.0.1:8181/nodes/' + '7be5bc10-dcdf-11e6-bd86-934bc6947c55/', + 'rel': 'bookmark'}] + } + self.assertEqual(expected, + nodes.Node._show_node_brief_info(node_info)) + + @mock.patch("valence.db.api.Connection.create_composed_node") + @mock.patch("valence.common.utils.generate_uuid") + @mock.patch("valence.redfish.redfish.compose_node") + def test_compose_node(self, mock_redfish_compose_node, mock_generate_uuid, + mock_db_create_composed_node): + """Test compose node successfully""" + node_hw = fakes.get_test_composed_node() + node_db = {"uuid": node_hw["uuid"], + "index": node_hw["index"], + "name": node_hw["name"], + "links": node_hw["links"]} + + mock_redfish_compose_node.return_value = node_hw + uuid = 'ea8e2a25-2901-438d-8157-de7ffd68d051' + mock_generate_uuid.return_value = uuid + + result = nodes.Node.compose_node({"name": "test"}) + expected = nodes.Node._show_node_brief_info(node_hw) + + self.assertEqual(expected, result) + mock_db_create_composed_node.assert_called_once_with(node_db) + + @mock.patch("valence.redfish.redfish.get_node_by_id") + @mock.patch("valence.db.api.Connection.get_composed_node_by_uuid") + def test_get_composed_node_by_uuid( + self, mock_db_get_composed_node, mock_redfish_get_node): + """Test get composed node detail""" + node_hw = fakes.get_test_composed_node() + node_db = test_utils.get_test_composed_node_db_info() + + mock_db_model = mock.MagicMock() + mock_db_model.as_dict.return_value = node_db + mock_db_get_composed_node.return_value = mock_db_model + + mock_redfish_get_node.return_value = node_hw + + result = nodes.Node.get_composed_node_by_uuid("fake_uuid") + + expected = copy.deepcopy(node_hw) + expected.update(node_db) + self.assertEqual(expected, result) + + @mock.patch("valence.db.api.Connection.delete_composed_node") + @mock.patch("valence.redfish.redfish.delete_composed_node") + @mock.patch("valence.db.api.Connection.get_composed_node_by_uuid") + def test_delete_composed_node( + self, mock_db_get_composed_node, mock_redfish_delete_composed_node, + mock_db_delete_composed_node): + """Test delete composed node""" + node_db = test_utils.get_test_composed_node_db_info() + + mock_db_model = mock.MagicMock() + mock_db_model.index = node_db["index"] + mock_db_get_composed_node.return_value = mock_db_model + + nodes.Node.delete_composed_node(node_db["uuid"]) + + mock_redfish_delete_composed_node.assert_called_once_with( + node_db["index"]) + mock_db_delete_composed_node.assert_called_once_with( + node_db["uuid"]) + + @mock.patch("valence.db.api.Connection.list_composed_nodes") + def test_list_composed_nodes(self, mock_db_list_composed_nodes): + """Test list all composed nodes""" + node_db = test_utils.get_test_composed_node_db_info() + + mock_db_model = mock.MagicMock() + mock_db_model.as_dict.return_value = node_db + mock_db_list_composed_nodes.return_value = [mock_db_model] + + expected = [nodes.Node._show_node_brief_info(node_db)] + + result = nodes.Node.list_composed_nodes() + + self.assertEqual(expected, result) diff --git a/valence/tests/unit/db/test_db_api.py b/valence/tests/unit/db/test_db_api.py index 30be80c..c6f3ad0 100644 --- a/valence/tests/unit/db/test_db_api.py +++ b/valence/tests/unit/db/test_db_api.py @@ -178,3 +178,83 @@ class TestDBAPI(unittest.TestCase): mock_etcd_write.assert_called_with( '/flavors/' + flavor['uuid'], json.dumps(result.as_dict())) + + @freezegun.freeze_time("2017-01-01") + @mock.patch('etcd.Client.write') + @mock.patch('etcd.Client.read') + def test_create_composed_node(self, mock_etcd_read, mock_etcd_write): + node = utils.get_test_composed_node_db_info() + fake_utcnow = '2017-01-01 00:00:00 UTC' + node['created_at'] = fake_utcnow + node['updated_at'] = fake_utcnow + + # Mark this uuid don't exist in etcd db + mock_etcd_read.side_effect = etcd.EtcdKeyNotFound + + result = db_api.Connection.create_composed_node(node) + self.assertEqual(node, result.as_dict()) + mock_etcd_read.assert_called_once_with( + '/nodes/' + node['uuid']) + mock_etcd_write.assert_called_once_with( + '/nodes/' + node['uuid'], + json.dumps(result.as_dict())) + + @mock.patch('etcd.Client.read') + def test_get_composed_node_by_uuid(self, mock_etcd_read): + node = utils.get_test_composed_node_db_info() + + mock_etcd_read.return_value = utils.get_etcd_read_result( + node['uuid'], json.dumps(node)) + result = db_api.Connection.get_composed_node_by_uuid(node['uuid']) + + self.assertEqual(node, result.as_dict()) + mock_etcd_read.assert_called_once_with( + '/nodes/' + node['uuid']) + + @mock.patch('etcd.Client.read') + def test_get_composed_node_not_found(self, mock_etcd_read): + node = utils.get_test_composed_node_db_info() + mock_etcd_read.side_effect = etcd.EtcdKeyNotFound + + with self.assertRaises(Exception) as context: # noqa: H202 + db_api.Connection.get_composed_node_by_uuid(node['uuid']) + + self.assertTrue('Composed node not found {0} in database.'.format( + node['uuid']) in str(context.exception)) + mock_etcd_read.assert_called_once_with( + '/nodes/' + node['uuid']) + + @mock.patch('etcd.Client.delete') + @mock.patch('etcd.Client.read') + def test_delete_composed_node(self, mock_etcd_read, mock_etcd_delete): + node = utils.get_test_composed_node_db_info() + + mock_etcd_read.return_value = utils.get_etcd_read_result( + node['uuid'], json.dumps(node)) + db_api.Connection.delete_composed_node(node['uuid']) + + mock_etcd_delete.assert_called_with( + '/nodes/' + node['uuid']) + + @freezegun.freeze_time("2017-01-01") + @mock.patch('etcd.Client.write') + @mock.patch('etcd.Client.read') + def test_update_composed_node(self, mock_etcd_read, mock_etcd_write): + node = utils.get_test_composed_node_db_info() + + mock_etcd_read.return_value = utils.get_etcd_read_result( + node['uuid'], json.dumps(node)) + + fake_utcnow = '2017-01-01 00:00:00 UTC' + node['updated_at'] = fake_utcnow + node.update({'index': '2'}) + + result = db_api.Connection.update_composed_node( + node['uuid'], {'index': '2'}) + + self.assertEqual(node, result.as_dict()) + mock_etcd_read.assert_called_with( + '/nodes/' + node['uuid']) + mock_etcd_write.assert_called_with( + '/nodes/' + node['uuid'], + json.dumps(result.as_dict())) diff --git a/valence/tests/unit/db/utils.py b/valence/tests/unit/db/utils.py index faab582..d88c214 100644 --- a/valence/tests/unit/db/utils.py +++ b/valence/tests/unit/db/utils.py @@ -75,3 +75,21 @@ def get_test_flavor(**kwargs): 'created_at': kwargs.get('created_at', '2016-01-01 00:00:00 UTC'), 'updated_at': kwargs.get('updated_at', '2016-01-01 00:00:00 UTC'), } + + +def get_test_composed_node_db_info(**kwargs): + return { + 'uuid': kwargs.get('uuid', 'ea8e2a25-2901-438d-8157-de7ffd68d051'), + 'name': kwargs.get('name', 'fake_name'), + 'index': kwargs.get('index', '1'), + 'links': kwargs.get( + 'links', + [{'href': 'http://127.0.0.1:8181/v1/nodes/' + '7be5bc10-dcdf-11e6-bd86-934bc6947c55/', + 'rel': 'self'}, + {'href': 'http://127.0.0.1:8181/nodes/' + '7be5bc10-dcdf-11e6-bd86-934bc6947c55/', + 'rel': 'bookmark'}]), + 'created_at': kwargs.get('created_at', '2016-01-01 00:00:00 UTC'), + 'updated_at': kwargs.get('updated_at', '2016-01-01 00:00:00 UTC') + } diff --git a/valence/tests/unit/redfish/test_redfish.py b/valence/tests/unit/redfish/test_redfish.py index f4ad1be..2c94ab6 100644 --- a/valence/tests/unit/redfish/test_redfish.py +++ b/valence/tests/unit/redfish/test_redfish.py @@ -255,7 +255,7 @@ class TestRedfish(TestCase): fake_delete_response = fakes.mock_request_get(delete_result, http_client.NO_CONTENT) mock_request.return_value = fake_delete_response - result = redfish.delete_composednode(101) + result = redfish.delete_composed_node(101) mock_request.assert_called_with('/redfish/v1/Nodes/101', 'DELETE') expected = { "code": "DELETED", @@ -276,7 +276,7 @@ class TestRedfish(TestCase): http_client.INTERNAL_SERVER_ERROR) mock_request.return_value = fake_resp self.assertRaises(exception.RedfishException, - redfish.delete_composednode, 101) + redfish.delete_composed_node, 101) self.assertFalse(mock_make_response.called) @mock.patch('requests.get') @@ -340,7 +340,7 @@ class TestRedfish(TestCase): self.assertTrue("There are no computer systems available for this " "allocation request." in str(context.exception.detail)) - @mock.patch('valence.redfish.redfish.delete_composednode') + @mock.patch('valence.redfish.redfish.delete_composed_node') @mock.patch('valence.redfish.redfish.get_base_resource_url') @mock.patch('valence.redfish.redfish.send_request') def test_assemble_node_failed(self, mock_request, mock_get_url, @@ -375,7 +375,7 @@ class TestRedfish(TestCase): mock_delete_node.assert_called_once() @mock.patch('valence.redfish.redfish.get_node_by_id') - @mock.patch('valence.redfish.redfish.delete_composednode') + @mock.patch('valence.redfish.redfish.delete_composed_node') @mock.patch('valence.redfish.redfish.get_base_resource_url') @mock.patch('valence.redfish.redfish.send_request') def test_assemble_node_success(self, mock_request, mock_get_url,