Browse Source

Add support to reset checkpoint state

Now when doing checkpoint copy failed, checkpoint
will be wait_copying status forever, and so we can
not do the restore anymore. So we should add an API
to support checkpoint status reset if we deeply knows
that the checkpoint is ok. This patch added API
support for doing checkpoint state reset.
Implements: bp checkpoint-status-reset

Change-Id: Iabaa98c9900fba554be2ad0833d438901e01147a
jiaopengju 4 months ago
parent
commit
ecc6b64f4a

+ 19
- 0
karbor/api/schemas/checkpoints.py View File

@@ -35,3 +35,22 @@ create = {
35 35
     'required': ['checkpoint'],
36 36
     'additionalProperties': False,
37 37
 }
38
+
39
+update = {
40
+    'type': 'object',
41
+    'properties': {
42
+        'os-resetState': {
43
+            'type': 'object',
44
+            'properties': {
45
+                'state': {
46
+                    'type': 'string',
47
+                    'enum': ['available', 'error'],
48
+                },
49
+            },
50
+            'required': ['state'],
51
+            'additionalProperties': False,
52
+        },
53
+    },
54
+    'required': [],
55
+    'additionalProperties': False,
56
+}

+ 39
- 0
karbor/api/v1/providers.py View File

@@ -476,6 +476,45 @@ class ProvidersController(wsgi.Controller):
476 476
         LOG.info("Delete checkpoint request issued successfully.")
477 477
         return {}
478 478
 
479
+    def _checkpoint_reset_state(self, context, provider_id,
480
+                                checkpoint_id, state):
481
+        try:
482
+            self.protection_api.reset_state(context, provider_id,
483
+                                            checkpoint_id, state)
484
+        except exception.AccessCheckpointNotAllowed as error:
485
+            raise exc.HTTPForbidden(explanation=error.msg)
486
+        except exception.CheckpointNotFound as error:
487
+            raise exc.HTTPNotFound(explanation=error.msg)
488
+        except exception.CheckpointNotBeReset as error:
489
+            raise exc.HTTPBadRequest(explanation=error.msg)
490
+        LOG.info("Reset checkpoint state request issued successfully.")
491
+        return {}
492
+
493
+    @validation.schema(checkpoint_schema.update)
494
+    def checkpoints_update(self, req, provider_id, checkpoint_id, body):
495
+        """Reset a checkpoint's state"""
496
+        context = req.environ['karbor.context']
497
+
498
+        LOG.info("Reset checkpoint state with id: %s", checkpoint_id)
499
+        LOG.info("provider_id: %s.", provider_id)
500
+
501
+        if not uuidutils.is_uuid_like(provider_id):
502
+            msg = _("Invalid provider id provided.")
503
+            raise exc.HTTPBadRequest(explanation=msg)
504
+
505
+        if not uuidutils.is_uuid_like(checkpoint_id):
506
+            msg = _("Invalid checkpoint id provided.")
507
+            raise exc.HTTPBadRequest(explanation=msg)
508
+
509
+        context.can(provider_policy.CHECKPOINT_UPDATE_POLICY)
510
+        if body.get("os-resetState"):
511
+            state = body["os-resetState"]["state"]
512
+            return self._checkpoint_reset_state(
513
+                context, provider_id, checkpoint_id, state)
514
+        else:
515
+            msg = _("Invalid input.")
516
+            raise exc.HTTPBadRequest(explanation=msg)
517
+
479 518
 
480 519
 def create_resource():
481 520
     return wsgi.Resource(ProvidersController())

+ 6
- 0
karbor/api/v1/router.py View File

@@ -96,6 +96,12 @@ class APIRouter(base_wsgi.Router):
96 96
                        controller=providers_resources,
97 97
                        action='checkpoints_delete',
98 98
                        conditions={"method": ['DELETE']})
99
+        mapper.connect("provider",
100
+                       "/{project_id}/providers/{provider_id}/checkpoints/"
101
+                       "{checkpoint_id}",
102
+                       controller=providers_resources,
103
+                       action='checkpoints_update',
104
+                       conditions={'method': ['PUT']})
99 105
         mapper.resource("trigger", "triggers",
100 106
                         controller=trigger_resources,
101 107
                         collection={},

+ 4
- 0
karbor/exception.py View File

@@ -390,6 +390,10 @@ class CheckpointNotBeDeleted(KarborException):
390 390
     message = _("The checkpoint %(checkpoint_id)s can not be deleted.")
391 391
 
392 392
 
393
+class CheckpointNotBeReset(KarborException):
394
+    message = _("The checkpoint %(checkpoint_id)s can not be reset.")
395
+
396
+
393 397
 class GetProtectionNetworkSubResourceFailed(KarborException):
394 398
     message = _("Get protection network sub-resources of type %(type)s failed:"
395 399
                 " %(reason)s")

+ 12
- 0
karbor/policies/providers.py View File

@@ -24,6 +24,7 @@ CHECKPOINT_GET_POLICY = 'provider:checkpoint_get'
24 24
 CHECKPOINT_GET_ALL_POLICY = 'provider:checkpoint_get_all'
25 25
 CHECKPOINT_CREATE_POLICY = 'provider:checkpoint_create'
26 26
 CHECKPOINT_DELETE_POLICY = 'provider:checkpoint_delete'
27
+CHECKPOINT_UPDATE_POLICY = 'provider:checkpoint_update'
27 28
 
28 29
 
29 30
 providers_policies = [
@@ -87,6 +88,17 @@ providers_policies = [
87 88
                 'path': '/providers/{provider_id}/checkpoints/{checkpoint_id}'
88 89
             }
89 90
         ]),
91
+    policy.DocumentedRuleDefault(
92
+        name=CHECKPOINT_UPDATE_POLICY,
93
+        check_str=base.RULE_ADMIN_OR_OWNER,
94
+        description='Reset checkpoint state.',
95
+        operations=[
96
+            {
97
+                'method': 'PUT',
98
+                'path': '/providers/{provider_id}/checkpoints/{checkpoint_id}'
99
+            }
100
+        ]
101
+    )
90 102
 ]
91 103
 
92 104
 

+ 8
- 0
karbor/services/protection/api.py View File

@@ -44,6 +44,14 @@ class API(base.Base):
44 44
             checkpoint_id
45 45
         )
46 46
 
47
+    def reset_state(self, context, provider_id, checkpoint_id, state):
48
+        return self.protection_rpcapi.reset_state(
49
+            context,
50
+            provider_id,
51
+            checkpoint_id,
52
+            state
53
+        )
54
+
47 55
     def show_checkpoint(self, context, provider_id, checkpoint_id):
48 56
         return self.protection_rpcapi.show_checkpoint(
49 57
             context,

+ 24
- 0
karbor/services/protection/manager.py View File

@@ -363,6 +363,30 @@ class ProtectionManager(manager.Manager):
363 363
             ))
364 364
         self._spawn(self.worker.run_flow, flow)
365 365
 
366
+    @messaging.expected_exceptions(exception.AccessCheckpointNotAllowed,
367
+                                   exception.CheckpointNotBeReset)
368
+    def reset_state(self, context, provider_id, checkpoint_id, state):
369
+        provider = self.provider_registry.show_provider(provider_id)
370
+
371
+        checkpoint = provider.get_checkpoint(checkpoint_id, context=context)
372
+        checkpoint_dict = checkpoint.to_dict()
373
+        if not context.is_admin and (
374
+                context.project_id != checkpoint_dict['project_id']):
375
+            raise exception.AccessCheckpointNotAllowed(
376
+                checkpoint_id=checkpoint_id)
377
+
378
+        if checkpoint.status not in [
379
+            constants.CHECKPOINT_STATUS_AVAILABLE,
380
+            constants.CHECKPOINT_STATUS_ERROR,
381
+            constants.CHECKPOINT_STATUS_COPYING,
382
+            constants.CHECKPOINT_STATUS_WAIT_COPYING,
383
+            constants.CHECKPOINT_STATUS_COPY_FINISHED
384
+        ]:
385
+            raise exception.CheckpointNotBeReset(
386
+                checkpoint_id=checkpoint_id)
387
+        checkpoint.status = state
388
+        checkpoint.commit()
389
+
366 390
     def start(self, plan):
367 391
         # TODO(wangliuan)
368 392
         pass

+ 9
- 0
karbor/services/protection/rpcapi.py View File

@@ -82,6 +82,15 @@ class ProtectionAPI(object):
82 82
             provider_id=provider_id,
83 83
             checkpoint_id=checkpoint_id)
84 84
 
85
+    def reset_state(self, ctxt, provider_id, checkpoint_id, state):
86
+        cctxt = self.client.prepare(version='1.0')
87
+        return cctxt.call(
88
+            ctxt,
89
+            'reset_state',
90
+            provider_id=provider_id,
91
+            checkpoint_id=checkpoint_id,
92
+            state=state)
93
+
85 94
     def show_checkpoint(self, ctxt, provider_id, checkpoint_id):
86 95
         cctxt = self.client.prepare(version='1.0')
87 96
         return cctxt.call(

+ 96
- 0
karbor/tests/unit/api/v1/test_providers.py View File

@@ -18,6 +18,7 @@ from webob import exc
18 18
 
19 19
 from karbor.api.v1 import providers
20 20
 from karbor import context
21
+from karbor import exception
21 22
 from karbor.tests import base
22 23
 from karbor.tests.unit.api import fakes
23 24
 
@@ -138,3 +139,98 @@ class ProvidersApiTest(base.TestCase):
138 139
             body=body)
139 140
         self.assertTrue(mock_plan_create.called)
140 141
         self.assertTrue(mock_protect.called)
142
+
143
+    @mock.patch('karbor.services.protection.api.API.reset_state')
144
+    def test_checkpoints_update_reset_state(self, mock_reset_state):
145
+        req = fakes.HTTPRequest.blank('/v1/providers/{provider_id}/'
146
+                                      'checkpoints/{checkpoint_id}')
147
+        body = {
148
+            'os-resetState': {'state': 'error'}
149
+        }
150
+        self.controller.checkpoints_update(
151
+            req,
152
+            '2220f8b1-975d-4621-a872-fa9afb43cb6c',
153
+            '2220f8b1-975d-4621-a872-fa9afb43cb6c',
154
+            body=body)
155
+        self.assertTrue(mock_reset_state.called)
156
+
157
+    def test_checkpoints_update_reset_state_with_invalid_provider_id(self):
158
+        req = fakes.HTTPRequest.blank('/v1/providers/{provider_id}/'
159
+                                      'checkpoints/{checkpoint_id}')
160
+        body = {
161
+            'os-resetState': {'state': 'error'}
162
+        }
163
+        self.assertRaises(
164
+            exc.HTTPBadRequest,
165
+            self.controller.checkpoints_update,
166
+            req,
167
+            'invalid_provider_id',
168
+            '2220f8b1-975d-4621-a872-fa9afb43cb6c',
169
+            body=body)
170
+
171
+    def test_checkpoints_update_reset_state_with_invalid_checkpoint_id(self):
172
+        req = fakes.HTTPRequest.blank('/v1/providers/{provider_id}/'
173
+                                      'checkpoints/{checkpoint_id}')
174
+        body = {
175
+            'os-resetState': {'state': 'error'}
176
+        }
177
+        self.assertRaises(
178
+            exc.HTTPBadRequest,
179
+            self.controller.checkpoints_update,
180
+            req,
181
+            '2220f8b1-975d-4621-a872-fa9afb43cb6c',
182
+            'invalid_checkpoint_id',
183
+            body=body)
184
+
185
+    def test_checkpoints_update_reset_state_with_invalid_body(self):
186
+        req = fakes.HTTPRequest.blank('/v1/providers/{provider_id}/'
187
+                                      'checkpoints/{checkpoint_id}')
188
+        self.assertRaises(
189
+            exc.HTTPBadRequest,
190
+            self.controller.checkpoints_update,
191
+            req,
192
+            '2220f8b1-975d-4621-a872-fa9afb43cb6c',
193
+            '2220f8b1-975d-4621-a872-fa9afb43cb6c',
194
+            body={})
195
+        self.assertRaises(
196
+            exception.ValidationError,
197
+            self.controller.checkpoints_update,
198
+            req,
199
+            '2220f8b1-975d-4621-a872-fa9afb43cb6c',
200
+            '2220f8b1-975d-4621-a872-fa9afb43cb6c',
201
+            body={'os-resetState': {'state': 'invalid_state'}})
202
+
203
+    @mock.patch('karbor.services.protection.api.API.reset_state')
204
+    def test_checkpoints_update_reset_state_with_protection_api_exceptions(
205
+            self, mock_reset_state):
206
+        req = fakes.HTTPRequest.blank('/v1/providers/{provider_id}/'
207
+                                      'checkpoints/{checkpoint_id}')
208
+        body = {
209
+            'os-resetState': {'state': 'error'}
210
+        }
211
+        mock_reset_state.side_effect = exception.AccessCheckpointNotAllowed(
212
+            checkpoint_id='2220f8b1-975d-4621-a872-fa9afb43cb6c')
213
+        self.assertRaises(exc.HTTPForbidden,
214
+                          self.controller.checkpoints_update,
215
+                          req,
216
+                          '2220f8b1-975d-4621-a872-fa9afb43cb6c',
217
+                          '2220f8b1-975d-4621-a872-fa9afb43cb6c',
218
+                          body=body)
219
+
220
+        mock_reset_state.side_effect = exception.CheckpointNotFound(
221
+            checkpoint_id='2220f8b1-975d-4621-a872-fa9afb43cb6c')
222
+        self.assertRaises(exc.HTTPNotFound,
223
+                          self.controller.checkpoints_update,
224
+                          req,
225
+                          '2220f8b1-975d-4621-a872-fa9afb43cb6c',
226
+                          '2220f8b1-975d-4621-a872-fa9afb43cb6c',
227
+                          body=body)
228
+
229
+        mock_reset_state.side_effect = exception.CheckpointNotBeReset(
230
+            checkpoint_id='2220f8b1-975d-4621-a872-fa9afb43cb6c')
231
+        self.assertRaises(exc.HTTPBadRequest,
232
+                          self.controller.checkpoints_update,
233
+                          req,
234
+                          '2220f8b1-975d-4621-a872-fa9afb43cb6c',
235
+                          '2220f8b1-975d-4621-a872-fa9afb43cb6c',
236
+                          body=body)

+ 48
- 0
karbor/tests/unit/protection/test_manager.py View File

@@ -233,6 +233,54 @@ class ProtectionServiceTest(base.TestCase):
233 233
                           'provider1',
234 234
                           'non_existent_checkpoint')
235 235
 
236
+    @mock.patch.object(provider.ProviderRegistry, 'show_provider')
237
+    def test_checkpoint_state_reset(self, mock_provider):
238
+        fake_provider = fakes.FakeProvider()
239
+        fake_checkpoint = fakes.FakeCheckpoint()
240
+        fake_checkpoint.commit = mock.MagicMock()
241
+        fake_provider.get_checkpoint = mock.MagicMock(
242
+            return_value=fake_checkpoint)
243
+        mock_provider.return_value = fake_provider
244
+        context = mock.MagicMock(project_id='fake_project_id', is_admin=True)
245
+        self.pro_manager.reset_state(context, 'provider1', 'fake_checkpoint',
246
+                                     'error')
247
+        self.assertEqual(fake_checkpoint.status, 'error')
248
+        self.assertEqual(True, fake_checkpoint.commit.called)
249
+
250
+    @mock.patch.object(provider.ProviderRegistry, 'show_provider')
251
+    def test_checkpoint_state_reset_with_access_not_allowed(
252
+            self, mock_provider):
253
+        fake_provider = fakes.FakeProvider()
254
+        fake_checkpoint = fakes.FakeCheckpoint()
255
+        fake_provider.get_checkpoint = mock.MagicMock(
256
+            return_value=fake_checkpoint)
257
+        mock_provider.return_value = fake_provider
258
+        context = mock.MagicMock(project_id='fake_project_id_01',
259
+                                 is_admin=False)
260
+        self.assertRaises(oslo_messaging.ExpectedException,
261
+                          self.pro_manager.reset_state,
262
+                          context,
263
+                          'fake_project_id',
264
+                          'fake_checkpoint_id',
265
+                          'error')
266
+
267
+    @mock.patch.object(provider.ProviderRegistry, 'show_provider')
268
+    def test_checkpoint_state_reset_with_wrong_checkpoint_state(
269
+            self, mock_provider):
270
+        fake_provider = fakes.FakeProvider()
271
+        fake_checkpoint = fakes.FakeCheckpoint()
272
+        fake_checkpoint.status = 'deleting'
273
+        fake_provider.get_checkpoint = mock.MagicMock(
274
+            return_value=fake_checkpoint)
275
+        mock_provider.return_value = fake_provider
276
+        context = mock.MagicMock(project_id='fake_project_id', is_admin=True)
277
+        self.assertRaises(oslo_messaging.ExpectedException,
278
+                          self.pro_manager.reset_state,
279
+                          context,
280
+                          'fake_project_id',
281
+                          'fake_checkpoint_id',
282
+                          'error')
283
+
236 284
     def tearDown(self):
237 285
         flow_manager.Worker._load_engine = self.load_engine
238 286
         super(ProtectionServiceTest, self).tearDown()

+ 4
- 0
releasenotes/notes/checkpoint-status-reset-d714b4a04da2f44d.yaml View File

@@ -0,0 +1,4 @@
1
+---
2
+features:
3
+  - |
4
+    Added support for checkpoint state reset by admin and owner.

Loading…
Cancel
Save