summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2016-09-07 15:02:53 +0000
committerGerrit Code Review <review@openstack.org>2016-09-07 15:02:53 +0000
commitcc5764751e9f1f2f6332b8982832dd2e63c86f46 (patch)
treeeb0e4e5cbae06e32980895b7e899f9842c41b2e0
parentd87125662f2f55204244e43ec6522fe36c6bf21e (diff)
parenta4e2a67e3e5024b0ae65f445355965a1263fef73 (diff)
Merge "Add support to re-assign a set of nodes" into stable/mitaka
-rw-r--r--cluster_upgrade/handlers.py28
-rw-r--r--cluster_upgrade/tests/test_handlers.py16
-rw-r--r--cluster_upgrade/tests/test_validators.py68
-rw-r--r--cluster_upgrade/validators.py30
4 files changed, 112 insertions, 30 deletions
diff --git a/cluster_upgrade/handlers.py b/cluster_upgrade/handlers.py
index 4e7f96d..ac590c3 100644
--- a/cluster_upgrade/handlers.py
+++ b/cluster_upgrade/handlers.py
@@ -73,16 +73,16 @@ class NodeReassignHandler(base.BaseHandler):
73 @base.handle_errors 73 @base.handle_errors
74 @base.validate 74 @base.validate
75 def POST(self, cluster_id): 75 def POST(self, cluster_id):
76 """Reassign node to the given cluster. 76 """Reassign nodes to the given cluster.
77 77
78 The given node will be assigned from the current cluster to the 78 The given nodes will be assigned from the current cluster to the
79 given cluster, by default it involves the reprovisioning of this 79 given cluster, by default it involves the reprovisioning of the
80 node. If the 'reprovision' flag is set to False, then the node 80 nodes. If the 'reprovision' flag is set to False, then the nodes
81 will be just reassigned. If the 'roles' list is specified, then 81 will be just reassigned. If the 'roles' list is specified, then
82 the given roles will be used as 'pending_roles' in case of 82 the given roles will be used as 'pending_roles' in case of
83 the reprovisioning or otherwise as 'roles'. 83 the reprovisioning or otherwise as 'roles'.
84 84
85 :param cluster_id: ID of the cluster node should be assigned to. 85 :param cluster_id: ID of the cluster nodes should be assigned to.
86 :returns: None 86 :returns: None
87 :http: * 202 (OK) 87 :http: * 202 (OK)
88 * 400 (Incorrect node state, problem with task execution, 88 * 400 (Incorrect node state, problem with task execution,
@@ -93,18 +93,22 @@ class NodeReassignHandler(base.BaseHandler):
93 self.get_object_or_404(self.single, cluster_id)) 93 self.get_object_or_404(self.single, cluster_id))
94 94
95 data = self.checked_data(cluster=cluster) 95 data = self.checked_data(cluster=cluster)
96 node = adapters.NailgunNodeAdapter(
97 self.get_object_or_404(objects.Node, data['node_id']))
98 reprovision = data.get('reprovision', True) 96 reprovision = data.get('reprovision', True)
99 given_roles = data.get('roles', []) 97 given_roles = data.get('roles', [])
100 98
101 roles, pending_roles = upgrade.UpgradeHelper.get_node_roles( 99 nodes_to_provision = []
102 reprovision, node.roles, given_roles) 100 for node_id in data['nodes_ids']:
103 upgrade.UpgradeHelper.assign_node_to_cluster( 101 node = adapters.NailgunNodeAdapter(
104 node, cluster, roles, pending_roles) 102 self.get_object_or_404(objects.Node, node_id))
103 nodes_to_provision.append(node.node)
104
105 roles, pending_roles = upgrade.UpgradeHelper.get_node_roles(
106 reprovision, node.roles, given_roles)
107 upgrade.UpgradeHelper.assign_node_to_cluster(
108 node, cluster, roles, pending_roles)
105 109
106 if reprovision: 110 if reprovision:
107 self.handle_task(cluster_id, [node.node]) 111 self.handle_task(cluster_id, nodes_to_provision)
108 112
109 113
110class CopyVIPsHandler(base.BaseHandler): 114class CopyVIPsHandler(base.BaseHandler):
diff --git a/cluster_upgrade/tests/test_handlers.py b/cluster_upgrade/tests/test_handlers.py
index 6e4382d..f3f5d1d 100644
--- a/cluster_upgrade/tests/test_handlers.py
+++ b/cluster_upgrade/tests/test_handlers.py
@@ -87,14 +87,14 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
87 resp = self.app.post( 87 resp = self.app.post(
88 reverse('NodeReassignHandler', 88 reverse('NodeReassignHandler',
89 kwargs={'cluster_id': seed_cluster['id']}), 89 kwargs={'cluster_id': seed_cluster['id']}),
90 jsonutils.dumps({'node_id': node_id}), 90 jsonutils.dumps({'nodes_ids': [node_id]}),
91 headers=self.default_headers) 91 headers=self.default_headers)
92 self.assertEqual(202, resp.status_code) 92 self.assertEqual(202, resp.status_code)
93 93
94 args, kwargs = mcast.call_args 94 args, kwargs = mcast.call_args
95 nodes = args[1]['args']['provisioning_info']['nodes'] 95 nodes = args[1]['args']['provisioning_info']['nodes']
96 provisioned_uids = [int(n['uid']) for n in nodes] 96 provisioned_uids = [int(n['uid']) for n in nodes]
97 self.assertEqual([node_id, ], provisioned_uids) 97 self.assertEqual([node_id], provisioned_uids)
98 98
99 @mock.patch('nailgun.task.task.rpc.cast') 99 @mock.patch('nailgun.task.task.rpc.cast')
100 def test_node_reassign_handler_with_roles(self, mcast): 100 def test_node_reassign_handler_with_roles(self, mcast):
@@ -108,7 +108,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
108 # NOTE(akscram): reprovision=True means that the node will be 108 # NOTE(akscram): reprovision=True means that the node will be
109 # re-provisioned during the reassigning. This is 109 # re-provisioned during the reassigning. This is
110 # a default behavior. 110 # a default behavior.
111 data = {'node_id': node.id, 111 data = {'nodes_ids': [node.id],
112 'reprovision': True, 112 'reprovision': True,
113 'roles': ['compute']} 113 'roles': ['compute']}
114 resp = self.app.post( 114 resp = self.app.post(
@@ -130,7 +130,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
130 node = cluster.nodes[0] 130 node = cluster.nodes[0]
131 seed_cluster = self.env.create_cluster(api=False) 131 seed_cluster = self.env.create_cluster(api=False)
132 132
133 data = {'node_id': node.id, 133 data = {'nodes_ids': [node.id],
134 'reprovision': False, 134 'reprovision': False,
135 'roles': ['compute']} 135 'roles': ['compute']}
136 resp = self.app.post( 136 resp = self.app.post(
@@ -148,7 +148,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
148 resp = self.app.post( 148 resp = self.app.post(
149 reverse('NodeReassignHandler', 149 reverse('NodeReassignHandler',
150 kwargs={'cluster_id': cluster['id']}), 150 kwargs={'cluster_id': cluster['id']}),
151 jsonutils.dumps({'node_id': 42}), 151 jsonutils.dumps({'nodes_ids': [42]}),
152 headers=self.default_headers, 152 headers=self.default_headers,
153 expect_errors=True) 153 expect_errors=True)
154 self.assertEqual(404, resp.status_code) 154 self.assertEqual(404, resp.status_code)
@@ -163,7 +163,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
163 resp = self.app.post( 163 resp = self.app.post(
164 reverse('NodeReassignHandler', 164 reverse('NodeReassignHandler',
165 kwargs={'cluster_id': cluster['id']}), 165 kwargs={'cluster_id': cluster['id']}),
166 jsonutils.dumps({'node_id': cluster.nodes[0]['id']}), 166 jsonutils.dumps({'nodes_ids': [cluster.nodes[0]['id']]}),
167 headers=self.default_headers, 167 headers=self.default_headers,
168 expect_errors=True) 168 expect_errors=True)
169 self.assertEqual(400, resp.status_code) 169 self.assertEqual(400, resp.status_code)
@@ -179,7 +179,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
179 resp = self.app.post( 179 resp = self.app.post(
180 reverse('NodeReassignHandler', 180 reverse('NodeReassignHandler',
181 kwargs={'cluster_id': cluster['id']}), 181 kwargs={'cluster_id': cluster['id']}),
182 jsonutils.dumps({'node_id': cluster.nodes[0]['id']}), 182 jsonutils.dumps({'nodes_ids': [cluster.nodes[0]['id']]}),
183 headers=self.default_headers, 183 headers=self.default_headers,
184 expect_errors=True) 184 expect_errors=True)
185 self.assertEqual(400, resp.status_code) 185 self.assertEqual(400, resp.status_code)
@@ -196,7 +196,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
196 resp = self.app.post( 196 resp = self.app.post(
197 reverse('NodeReassignHandler', 197 reverse('NodeReassignHandler',
198 kwargs={'cluster_id': cluster_id}), 198 kwargs={'cluster_id': cluster_id}),
199 jsonutils.dumps({'node_id': node_id}), 199 jsonutils.dumps({'nodes_ids': [node_id]}),
200 headers=self.default_headers, 200 headers=self.default_headers,
201 expect_errors=True) 201 expect_errors=True)
202 self.assertEqual(400, resp.status_code) 202 self.assertEqual(400, resp.status_code)
diff --git a/cluster_upgrade/tests/test_validators.py b/cluster_upgrade/tests/test_validators.py
index 43da11c..2afd9bc 100644
--- a/cluster_upgrade/tests/test_validators.py
+++ b/cluster_upgrade/tests/test_validators.py
@@ -137,7 +137,7 @@ class TestNodeReassignValidator(base.BaseTestCase):
137 node = self.env.create_node(cluster_id=cluster.id, 137 node = self.env.create_node(cluster_id=cluster.id,
138 roles=["compute"], 138 roles=["compute"],
139 status="ready") 139 status="ready")
140 msg = "^'node_id' is a required property" 140 msg = "^'nodes_ids' is a required property"
141 with self.assertRaisesRegexp(errors.InvalidData, msg): 141 with self.assertRaisesRegexp(errors.InvalidData, msg):
142 self.validator.validate("{}", node) 142 self.validator.validate("{}", node)
143 143
@@ -164,7 +164,7 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
164 roles=["compute"], status="ready") 164 roles=["compute"], status="ready")
165 165
166 def test_validate_defaults(self): 166 def test_validate_defaults(self):
167 request = {"node_id": self.node.id} 167 request = {"nodes_ids": [self.node.id]}
168 data = jsonutils.dumps(request) 168 data = jsonutils.dumps(request)
169 parsed = self.validator.validate(data, self.dst_cluster) 169 parsed = self.validator.validate(data, self.dst_cluster)
170 self.assertEqual(parsed, request) 170 self.assertEqual(parsed, request)
@@ -172,7 +172,7 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
172 172
173 def test_validate_with_roles(self): 173 def test_validate_with_roles(self):
174 request = { 174 request = {
175 "node_id": self.node.id, 175 "nodes_ids": [self.node.id],
176 "reprovision": True, 176 "reprovision": True,
177 "roles": ['controller'], 177 "roles": ['controller'],
178 } 178 }
@@ -182,7 +182,7 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
182 182
183 def test_validate_not_unique_roles(self): 183 def test_validate_not_unique_roles(self):
184 data = jsonutils.dumps({ 184 data = jsonutils.dumps({
185 "node_id": self.node.id, 185 "nodes_ids": [self.node.id],
186 "roles": ['compute', 'compute'], 186 "roles": ['compute', 'compute'],
187 }) 187 })
188 msg = "has non-unique elements" 188 msg = "has non-unique elements"
@@ -191,7 +191,7 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
191 191
192 def test_validate_no_reprovision_with_conflicts(self): 192 def test_validate_no_reprovision_with_conflicts(self):
193 data = jsonutils.dumps({ 193 data = jsonutils.dumps({
194 "node_id": self.node.id, 194 "nodes_ids": [self.node.id],
195 "reprovision": False, 195 "reprovision": False,
196 "roles": ['controller', 'compute'], 196 "roles": ['controller', 'compute'],
197 }) 197 })
@@ -203,6 +203,64 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
203 "Role 'controller' in conflict with role 'compute'." 203 "Role 'controller' in conflict with role 'compute'."
204 ) 204 )
205 205
206 def test_validate_empty_nodes_ids_error(self):
207 data = jsonutils.dumps({
208 "nodes_ids": [],
209 "roles": ['controller'],
210 })
211 msg = "minItems.*nodes_ids"
212 with self.assertRaisesRegexp(errors.InvalidData, msg):
213 self.validator.validate(data, self.dst_cluster)
214
215 def test_validate_several_nodes_ids(self):
216 node = self.env.create_node(cluster_id=self.src_cluster.id,
217 roles=["compute"], status="ready")
218 request = {
219 "nodes_ids": [self.node.id, node.id],
220 }
221 data = jsonutils.dumps(request)
222 parsed = self.validator.validate(data, self.dst_cluster)
223 self.assertEqual(parsed, request)
224
225 def test_validate_mixed_two_not_sorted_roles(self):
226 self.node.roles = ["compute", "ceph-osd"]
227 node = self.env.create_node(cluster_id=self.src_cluster.id,
228 roles=["ceph-osd", "compute"],
229 status="ready")
230 request = {
231 "nodes_ids": [self.node.id, node.id],
232 "roles": ["compute"],
233 }
234 data = jsonutils.dumps(request)
235 parsed = self.validator.validate(data, self.dst_cluster)
236 self.assertEqual(parsed, request)
237
238 def test_validate_mixed_roles_error(self):
239 node = self.env.create_node(cluster_id=self.src_cluster.id,
240 roles=["ceph-osd"], status="ready")
241 self._assert_validate_nodes_roles([self.node.id, node.id],
242 ["controller"])
243
244 def test_validate_mixed_two_roles_error(self):
245 # Two nodes have two roles each, the first ones are the same
246 # while the second ones differ.
247 self.node.roles = ["compute", "ceph-osd"]
248 node = self.env.create_node(cluster_id=self.src_cluster.id,
249 roles=["compute", "mongo"],
250 status="ready")
251 self._assert_validate_nodes_roles([self.node.id, node.id],
252 ["compute"])
253
254 def _assert_validate_nodes_roles(self, nodes_ids, roles):
255 data = jsonutils.dumps({
256 "nodes_ids": nodes_ids,
257 "roles": roles,
258 })
259 msg = "Only nodes with the same set of assigned roles are supported " \
260 "for the operation."
261 with self.assertRaisesRegexp(errors.InvalidData, msg):
262 self.validator.validate(data, self.dst_cluster)
263
206 264
207class TestCopyVIPsValidator(base.BaseTestCase): 265class TestCopyVIPsValidator(base.BaseTestCase):
208 validator = validators.CopyVIPsValidator 266 validator = validators.CopyVIPsValidator
diff --git a/cluster_upgrade/validators.py b/cluster_upgrade/validators.py
index 95f2f6d..4bc2831 100644
--- a/cluster_upgrade/validators.py
+++ b/cluster_upgrade/validators.py
@@ -98,13 +98,18 @@ class NodeReassignValidator(assignment.NodeAssignmentValidator):
98 "description": "Serialized parameters to assign node", 98 "description": "Serialized parameters to assign node",
99 "type": "object", 99 "type": "object",
100 "properties": { 100 "properties": {
101 "node_id": {"type": "number"}, 101 "nodes_ids": {
102 "type": "array",
103 "items": {"type": "number"},
104 "uniqueItems": True,
105 "minItems": 1,
106 },
102 "reprovision": {"type": "boolean", "default": True}, 107 "reprovision": {"type": "boolean", "default": True},
103 "roles": {"type": "array", 108 "roles": {"type": "array",
104 "items": {"type": "string"}, 109 "items": {"type": "string"},
105 "uniqueItems": True}, 110 "uniqueItems": True},
106 }, 111 },
107 "required": ["node_id"], 112 "required": ["nodes_ids"],
108 } 113 }
109 114
110 @classmethod 115 @classmethod
@@ -112,14 +117,19 @@ class NodeReassignValidator(assignment.NodeAssignmentValidator):
112 parsed = super(NodeReassignValidator, cls).validate(data) 117 parsed = super(NodeReassignValidator, cls).validate(data)
113 cls.validate_schema(parsed, cls.schema) 118 cls.validate_schema(parsed, cls.schema)
114 119
115 node = cls.validate_node(parsed['node_id']) 120 nodes = []
116 cls.validate_node_cluster(node, cluster) 121 for node_id in parsed['nodes_ids']:
122 node = cls.validate_node(node_id)
123 cls.validate_node_cluster(node, cluster)
124 nodes.append(node)
117 125
118 roles = parsed.get('roles', []) 126 roles = parsed.get('roles', [])
119 if roles: 127 if roles:
128 cls.validate_nodes_roles(nodes)
120 cls.validate_roles(cluster, roles) 129 cls.validate_roles(cluster, roles)
121 else: 130 else:
122 cls.validate_roles(cluster, node.roles) 131 for node in nodes:
132 cls.validate_roles(cluster, node.roles)
123 return parsed 133 return parsed
124 134
125 @classmethod 135 @classmethod
@@ -148,6 +158,16 @@ class NodeReassignValidator(assignment.NodeAssignmentValidator):
148 return node 158 return node
149 159
150 @classmethod 160 @classmethod
161 def validate_nodes_roles(cls, nodes):
162 roles = set(nodes[0].roles)
163 if all(roles == set(n.roles) for n in nodes[1:]):
164 return
165 raise errors.InvalidData(
166 "Only nodes with the same set of assigned roles are supported "
167 "for the operation.",
168 log_message=True)
169
170 @classmethod
151 def validate_node_cluster(cls, node, cluster): 171 def validate_node_cluster(cls, node, cluster):
152 if node.cluster_id == cluster.id: 172 if node.cluster_id == cluster.id:
153 raise errors.InvalidData("Node {0} is already assigned to cluster" 173 raise errors.InvalidData("Node {0} is already assigned to cluster"