diff --git a/openstack/cluster/v1/_proxy.py b/openstack/cluster/v1/_proxy.py index d2b0f2466..15eb99acc 100644 --- a/openstack/cluster/v1/_proxy.py +++ b/openstack/cluster/v1/_proxy.py @@ -12,6 +12,7 @@ from openstack.cluster.v1 import action from openstack.cluster.v1 import cluster +from openstack.cluster.v1 import node from openstack.cluster.v1 import policy from openstack import proxy @@ -83,6 +84,7 @@ class Proxy(proxy.BaseProxy): whether a cluster should be included in the list result. * sort_keys: A list of key names for sorting the resulted list. * sort_dir: Direction for sorting, and its valid values are 'asc' + and 'desc'. * limit: Requests a specified size of returned items from the query. Returns a number of items up to the specified limit value. @@ -108,6 +110,92 @@ class Proxy(proxy.BaseProxy): """ return self._update(cluster.Cluster, value, **attrs) + def create_node(self, **attrs): + """Create a new node from attributes. + + :param dict attrs: Keyword arguments that will be used to create a + :class:`~openstack.cluster.v1.node.Node`, it is comprised + of the properties on the Node class. + + :returns: The results of node creation. + :rtype: :class:`~openstack.cluster.v1.node.Node`. + """ + return self._create(node.Node, **attrs) + + def delete_node(self, value, ignore_missing=True): + """Delete a node. + + :param value: The value can be either the name or ID of a node or a + :class:`~openstack.cluster.v1.node.Node` instance. + :param bool ignore_missing: When set to ``False``, an exception + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the node could not be found. When set to ``True``, no exception + will be raised when attempting to delete a non-existent node. + + :returns: ``None`` + """ + self._delete(node.Node, value, ignore_missing=ignore_missing) + + def find_node(self, value, ignore_missing=True): + """Find a single node. + + :param value: The name or ID of a node. + :returns: One :class:`~openstack.cluster.v1.node.Node` object or None. + """ + return node.Node.find(self.session, value, + ignore_missing=ignore_missing) + + def get_node(self, value): + """Get a single node. + + :param value: The value can be the name or ID of a node or a + :class:`~openstack.cluster.v1.node.Node` instance. + + :returns: One :class:`~openstack.cluster.v1.node.Node` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + node matching the name or ID could be found. + """ + return self._get(node.Node, value) + + def nodes(self, **query): + """Retrieve a generator of nodes. + + :param kwargs \*\*query: Optional query parameters to be sent to + restrict the nodes to be returned. Available parameters include: + + * cluster_id: A string including the name or ID of a cluster to + which the resulted node(s) is a member. + * show_deleted: A boolean value indicating whether soft-deleted + nodes should be returned as well. + * filters: A list of key-value pairs for server to determine + whether a node should be included in the list result. + * sort_keys: A list of key names for sorting the resulted list. + * sort_dir: Direction for sorting, and its valid values are 'asc' + and 'desc'. + * limit: Requests at most the specified number of items be + returned from the query. + * marker: Specifies the ID of the last-seen node. Use the limit + parameter to make an initial limited request and use the ID of + the last-seen node from the response as the marker parameter + value in a subsequent limited request. + + :returns: A generator of node instances. + """ + return self._list(node.Node, paginated=True, **query) + + def update_node(self, value, **attrs): + """Update a node. + + :param value: Either the name or the ID of the node, or an instance + of :class:`~openstack.cluster.v1.node.Node`. + :param attrs: The attributes to update on the node represented by + the ``value`` parameter. + + :returns: The updated node. + :rtype: :class:`~openstack.cluster.v1.node.Node` + """ + return self._update(node.Node, value, **attrs) + def create_policy(self, **attrs): """Create a new policy from attributes. diff --git a/openstack/cluster/v1/node.py b/openstack/cluster/v1/node.py new file mode 100644 index 000000000..197bcc947 --- /dev/null +++ b/openstack/cluster/v1/node.py @@ -0,0 +1,106 @@ +# 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. + +from openstack.cluster import cluster_service +from openstack import resource +from openstack import utils + + +class Node(resource.Resource): + resource_key = 'node' + resources_key = 'nodes' + base_path = '/nodes' + service = cluster_service.ClusterService() + + # capabilities + allow_create = True + allow_retrieve = True + allow_update = True + allow_delete = True + allow_list = True + + # Properties + #: The name of the node. + name = resource.prop('name') + #: The ID of the physical object that backs the node. + physical_id = resource.prop('physical_id') + #: The ID of the cluster in which this node is a member. + #: A node is an orphan node if this field is empty. + cluster_id = resource.prop('cluster_id') + #: The ID of the profile used by this node. + profile_id = resource.prop('profile_id') + #: The ID of the project this node belongs to. + project = resource.prop('project') + #: The name of the profile used by this node. + profile_name = resource.prop('profile_name') + #: An integer that is unique inside the owning cluster. + #: A value of -1 means this node is an orphan node. + index = resource.prop('index', type=int) + #: A string indicating the role the node plays in a cluster. + role = resource.prop('role') + #: The timestamp of the node object's initialization. + init_time = resource.prop('init_time') + #: The timestamp of the node's creation, i.e. the physical object + #: represented by this node is also created. + created_time = resource.prop('created_time') + #: The timestamp the node was last updated. + updated_time = resource.prop('updated_time') + #: The timestamp the node was deleted. This is only used for node + #: which has been soft deleted. + deleted_time = resource.prop('deleted_time') + #: A string indicating the node's status. + status = resource.prop('status') + #: A string describing why the node entered its current status. + status_reason = resource.prop('status_reason') + #: A map containing key-value pairs attached to the node. + metadata = resource.prop('tags', type=dict) + #: A map containing some runtime data for this node. + data = resource.prop('data', type=dict) + #: A map containing the details of the physical object this node + #: represents + details = resource.prop('details', type=dict) + + def _action(self, session, body): + """Procedure the invoke an action API. + + :param session: A session object used for sending request. + :param body: The body of action to be sent. + """ + url = utils.urljoin(self.base_path, self.id, 'action') + resp = session.put(url, service=self.service, json=body).body + return resp + + def join(self, session, cluster_id): + """An action procedure for the node to join a cluster. + + :param session: A session object used for sending request. + :param cluster_id: The ID, name or short ID of a cluster the + node is about to join. + :returns: A dictionary containing the action ID. + """ + body = { + 'join': { + 'cluster_id': cluster_id, + } + } + return self._action(session, body) + + def leave(self, session): + """An action procedure for the node to leave its current cluster. + + :param session: A session object used for sending request. + :returns: A dictionary containing the action ID. + """ + body = { + 'leave': {} + } + return self._action(session, body) diff --git a/openstack/tests/unit/cluster/v1/test_node.py b/openstack/tests/unit/cluster/v1/test_node.py new file mode 100644 index 000000000..66ecd396a --- /dev/null +++ b/openstack/tests/unit/cluster/v1/test_node.py @@ -0,0 +1,108 @@ +# 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 mock +import testtools + +from openstack.cluster.v1 import node + + +FAKE_ID = '123d0955-0099-aabb-b8fa-6a44655ceeff' +FAKE_NAME = 'test_node' + +FAKE = { + 'id': FAKE_ID, + 'cluster_id': 'clusterA', + 'metadata': {}, + 'name': FAKE_NAME, + 'profile_id': 'myserver', + 'index': 1, + 'role': 'master', +} + +FAKE_CREATE_RESP = { + 'node': { + 'id': FAKE_ID, + 'name': FAKE_NAME, + 'cluster_id': '99001122-aabb-ccdd-ffff-efdcab124567', + 'action': '1122aabb-eeff-7755-2222-00991234dcba', + 'created_time': None, + 'deleted_time': None, + 'updated_time': None, + 'data': {}, + 'role': 'master', + 'index': 1, + 'init_time': None, + 'metadata': {}, + 'profile_id': '560a8f9d-7596-4a32-85e8-03645fa7be13', + 'profile_name': 'myserver', + 'project': '333acb15a43242f4a609a27cb097a8f2', + 'status': 'INIT', + 'status_reason': 'Initializing', + } +} + + +class TestNode(testtools.TestCase): + + def setUp(self): + super(TestNode, self).setUp() + + def test_basic(self): + sot = node.Node() + self.assertEqual('node', sot.resource_key) + self.assertEqual('nodes', sot.resources_key) + self.assertEqual('/nodes', sot.base_path) + self.assertEqual('clustering', sot.service.service_type) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_retrieve) + self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_instantiate(self): + sot = node.Node(FAKE) + self.assertEqual(FAKE['id'], sot.id) + self.assertEqual(FAKE['name'], sot.name) + + self.assertEqual(FAKE['profile_id'], sot.profile_id) + self.assertEqual(FAKE['cluster_id'], sot.cluster_id) + self.assertEqual(FAKE['name'], sot.name) + self.assertEqual(FAKE['index'], sot.index) + self.assertEqual(FAKE['role'], sot.role) + self.assertEqual(FAKE['metadata'], sot.metadata) + + def test_join(self): + sot = node.Node(FAKE) + sot['id'] = 'IDENTIFIER' + + resp = mock.Mock() + resp.body = {'action': '1234-5678-abcd'} + sess = mock.Mock() + sess.put = mock.MagicMock(return_value=resp) + self.assertEqual(resp.body, sot.join(sess, 'cluster-b')) + url = 'nodes/%s/action' % sot.id + body = {'join': {'cluster_id': 'cluster-b'}} + sess.put.assert_called_once_with(url, service=sot.service, json=body) + + def test_leave(self): + sot = node.Node(FAKE) + sot['id'] = 'IDENTIFIER' + + resp = mock.Mock() + resp.body = {'action': '2345-6789-bbbb'} + sess = mock.Mock() + sess.put = mock.MagicMock(return_value=resp) + self.assertEqual(resp.body, sot.leave(sess)) + url = 'nodes/%s/action' % sot.id + body = {'leave': {}} + sess.put.assert_called_once_with(url, service=sot.service, json=body) diff --git a/openstack/tests/unit/cluster/v1/test_proxy.py b/openstack/tests/unit/cluster/v1/test_proxy.py index 445ea702a..05d64b036 100644 --- a/openstack/tests/unit/cluster/v1/test_proxy.py +++ b/openstack/tests/unit/cluster/v1/test_proxy.py @@ -13,6 +13,7 @@ from openstack.cluster.v1 import _proxy from openstack.cluster.v1 import action from openstack.cluster.v1 import cluster +from openstack.cluster.v1 import node from openstack.cluster.v1 import policy from openstack.tests.unit import test_proxy_base @@ -47,6 +48,31 @@ class TestClusterProxy(test_proxy_base.TestProxyBase): def test_cluster_update(self): self.verify_update(self.proxy.update_cluster, cluster.Cluster) + def test_node_create(self): + self.verify_create(self.proxy.create_node, node.Node) + + def test_node_delete(self): + self.verify_delete(self.proxy.delete_node, node.Node, False) + + def test_node_delete_ignore(self): + self.verify_delete(self.proxy.delete_node, node.Node, True) + + def test_node_find(self): + self.verify_find('openstack.cluster.v1.node.Node.find', + self.proxy.find_node) + + def test_node_get(self): + self.verify_get(self.proxy.get_node, node.Node) + + def test_nodes(self): + self.verify_list(self.proxy.nodes, node.Node, + paginated=True, + method_kwargs={'limit': 2}, + expected_kwargs={'limit': 2}) + + def test_node_update(self): + self.verify_update(self.proxy.update_node, node.Node) + def test_policy_create(self): self.verify_create(self.proxy.create_policy, policy.Policy)