Browse Source

Add support to re-assign a set of nodes

This patch adds an ability to re-assign a set of the given nodes at
once. This feature was technically available but not exposed to the
client. A groupped re-assigning allows to effectively re-provision nodes
by creating an atomic task in Astute.

Change-Id: I4a7c7e35d844683ef73ad7f8459d1892e80e0a64
Related-Bug: #1616925
Ilya Kharin 2 years ago
parent
commit
a4e2a67e3e

+ 16
- 12
cluster_upgrade/handlers.py View File

@@ -73,16 +73,16 @@ class NodeReassignHandler(base.BaseHandler):
73 73
     @base.handle_errors
74 74
     @base.validate
75 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
79
-        given cluster, by default it involves the reprovisioning of this
80
-        node. If the 'reprovision' flag is set to False, then the node
78
+        The given nodes will be assigned from the current cluster to the
79
+        given cluster, by default it involves the reprovisioning of the
80
+        nodes. If the 'reprovision' flag is set to False, then the nodes
81 81
         will be just reassigned. If the 'roles' list is specified, then
82 82
         the given roles will be used as 'pending_roles' in case of
83 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 86
         :returns: None
87 87
         :http: * 202 (OK)
88 88
                * 400 (Incorrect node state, problem with task execution,
@@ -93,18 +93,22 @@ class NodeReassignHandler(base.BaseHandler):
93 93
             self.get_object_or_404(self.single, cluster_id))
94 94
 
95 95
         data = self.checked_data(cluster=cluster)
96
-        node = adapters.NailgunNodeAdapter(
97
-            self.get_object_or_404(objects.Node, data['node_id']))
98 96
         reprovision = data.get('reprovision', True)
99 97
         given_roles = data.get('roles', [])
100 98
 
101
-        roles, pending_roles = upgrade.UpgradeHelper.get_node_roles(
102
-            reprovision, node.roles, given_roles)
103
-        upgrade.UpgradeHelper.assign_node_to_cluster(
104
-            node, cluster, roles, pending_roles)
99
+        nodes_to_provision = []
100
+        for node_id in data['nodes_ids']:
101
+            node = adapters.NailgunNodeAdapter(
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 110
         if reprovision:
107
-            self.handle_task(cluster_id, [node.node])
111
+            self.handle_task(cluster_id, nodes_to_provision)
108 112
 
109 113
 
110 114
 class CopyVIPsHandler(base.BaseHandler):

+ 8
- 8
cluster_upgrade/tests/test_handlers.py View File

@@ -87,14 +87,14 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
87 87
         resp = self.app.post(
88 88
             reverse('NodeReassignHandler',
89 89
                     kwargs={'cluster_id': seed_cluster['id']}),
90
-            jsonutils.dumps({'node_id': node_id}),
90
+            jsonutils.dumps({'nodes_ids': [node_id]}),
91 91
             headers=self.default_headers)
92 92
         self.assertEqual(202, resp.status_code)
93 93
 
94 94
         args, kwargs = mcast.call_args
95 95
         nodes = args[1]['args']['provisioning_info']['nodes']
96 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 99
     @mock.patch('nailgun.task.task.rpc.cast')
100 100
     def test_node_reassign_handler_with_roles(self, mcast):
@@ -108,7 +108,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
108 108
         # NOTE(akscram): reprovision=True means that the node will be
109 109
         #                re-provisioned during the reassigning. This is
110 110
         #                a default behavior.
111
-        data = {'node_id': node.id,
111
+        data = {'nodes_ids': [node.id],
112 112
                 'reprovision': True,
113 113
                 'roles': ['compute']}
114 114
         resp = self.app.post(
@@ -130,7 +130,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
130 130
         node = cluster.nodes[0]
131 131
         seed_cluster = self.env.create_cluster(api=False)
132 132
 
133
-        data = {'node_id': node.id,
133
+        data = {'nodes_ids': [node.id],
134 134
                 'reprovision': False,
135 135
                 'roles': ['compute']}
136 136
         resp = self.app.post(
@@ -148,7 +148,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
148 148
         resp = self.app.post(
149 149
             reverse('NodeReassignHandler',
150 150
                     kwargs={'cluster_id': cluster['id']}),
151
-            jsonutils.dumps({'node_id': 42}),
151
+            jsonutils.dumps({'nodes_ids': [42]}),
152 152
             headers=self.default_headers,
153 153
             expect_errors=True)
154 154
         self.assertEqual(404, resp.status_code)
@@ -163,7 +163,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
163 163
         resp = self.app.post(
164 164
             reverse('NodeReassignHandler',
165 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 167
             headers=self.default_headers,
168 168
             expect_errors=True)
169 169
         self.assertEqual(400, resp.status_code)
@@ -179,7 +179,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
179 179
         resp = self.app.post(
180 180
             reverse('NodeReassignHandler',
181 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 183
             headers=self.default_headers,
184 184
             expect_errors=True)
185 185
         self.assertEqual(400, resp.status_code)
@@ -196,7 +196,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
196 196
         resp = self.app.post(
197 197
             reverse('NodeReassignHandler',
198 198
                     kwargs={'cluster_id': cluster_id}),
199
-            jsonutils.dumps({'node_id': node_id}),
199
+            jsonutils.dumps({'nodes_ids': [node_id]}),
200 200
             headers=self.default_headers,
201 201
             expect_errors=True)
202 202
         self.assertEqual(400, resp.status_code)

+ 63
- 5
cluster_upgrade/tests/test_validators.py View File

@@ -137,7 +137,7 @@ class TestNodeReassignValidator(base.BaseTestCase):
137 137
         node = self.env.create_node(cluster_id=cluster.id,
138 138
                                     roles=["compute"],
139 139
                                     status="ready")
140
-        msg = "^'node_id' is a required property"
140
+        msg = "^'nodes_ids' is a required property"
141 141
         with self.assertRaisesRegexp(errors.InvalidData, msg):
142 142
             self.validator.validate("{}", node)
143 143
 
@@ -164,7 +164,7 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
164 164
                                          roles=["compute"], status="ready")
165 165
 
166 166
     def test_validate_defaults(self):
167
-        request = {"node_id": self.node.id}
167
+        request = {"nodes_ids": [self.node.id]}
168 168
         data = jsonutils.dumps(request)
169 169
         parsed = self.validator.validate(data, self.dst_cluster)
170 170
         self.assertEqual(parsed, request)
@@ -172,7 +172,7 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
172 172
 
173 173
     def test_validate_with_roles(self):
174 174
         request = {
175
-            "node_id": self.node.id,
175
+            "nodes_ids": [self.node.id],
176 176
             "reprovision": True,
177 177
             "roles": ['controller'],
178 178
         }
@@ -182,7 +182,7 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
182 182
 
183 183
     def test_validate_not_unique_roles(self):
184 184
         data = jsonutils.dumps({
185
-            "node_id": self.node.id,
185
+            "nodes_ids": [self.node.id],
186 186
             "roles": ['compute', 'compute'],
187 187
         })
188 188
         msg = "has non-unique elements"
@@ -191,7 +191,7 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
191 191
 
192 192
     def test_validate_no_reprovision_with_conflicts(self):
193 193
         data = jsonutils.dumps({
194
-            "node_id": self.node.id,
194
+            "nodes_ids": [self.node.id],
195 195
             "reprovision": False,
196 196
             "roles": ['controller', 'compute'],
197 197
         })
@@ -203,6 +203,64 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
203 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
 
207 265
 class TestCopyVIPsValidator(base.BaseTestCase):
208 266
     validator = validators.CopyVIPsValidator

+ 25
- 5
cluster_upgrade/validators.py View File

@@ -98,13 +98,18 @@ class NodeReassignValidator(assignment.NodeAssignmentValidator):
98 98
         "description": "Serialized parameters to assign node",
99 99
         "type": "object",
100 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 107
             "reprovision": {"type": "boolean", "default": True},
103 108
             "roles": {"type": "array",
104 109
                       "items": {"type": "string"},
105 110
                       "uniqueItems": True},
106 111
         },
107
-        "required": ["node_id"],
112
+        "required": ["nodes_ids"],
108 113
     }
109 114
 
110 115
     @classmethod
@@ -112,14 +117,19 @@ class NodeReassignValidator(assignment.NodeAssignmentValidator):
112 117
         parsed = super(NodeReassignValidator, cls).validate(data)
113 118
         cls.validate_schema(parsed, cls.schema)
114 119
 
115
-        node = cls.validate_node(parsed['node_id'])
116
-        cls.validate_node_cluster(node, cluster)
120
+        nodes = []
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 126
         roles = parsed.get('roles', [])
119 127
         if roles:
128
+            cls.validate_nodes_roles(nodes)
120 129
             cls.validate_roles(cluster, roles)
121 130
         else:
122
-            cls.validate_roles(cluster, node.roles)
131
+            for node in nodes:
132
+                cls.validate_roles(cluster, node.roles)
123 133
         return parsed
124 134
 
125 135
     @classmethod
@@ -147,6 +157,16 @@ class NodeReassignValidator(assignment.NodeAssignmentValidator):
147 157
                                      log_message=True)
148 158
         return node
149 159
 
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
+
150 170
     @classmethod
151 171
     def validate_node_cluster(cls, node, cluster):
152 172
         if node.cluster_id == cluster.id:

Loading…
Cancel
Save