2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2022-2024 Nordix Foundation
4 * Modifications Copyright (C) 2022 Bell Canada
5 * ================================================================================
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
18 * SPDX-License-Identifier: Apache-2.0
19 * ============LICENSE_END=========================================================
22 package org.onap.cps.ncmp.api.impl.inventory.sync
24 import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.LOCKED_MISBEHAVING
25 import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.MODULE_UPGRADE
26 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL
27 import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.MODULE_SYNC_FAILED
28 import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.MODULE_UPGRADE_FAILED
30 import ch.qos.logback.classic.Level
31 import ch.qos.logback.classic.Logger
32 import ch.qos.logback.core.read.ListAppender
33 import org.slf4j.LoggerFactory
34 import org.springframework.context.annotation.AnnotationConfigApplicationContext
35 import com.fasterxml.jackson.databind.JsonNode
36 import com.fasterxml.jackson.databind.ObjectMapper
37 import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations
38 import org.onap.cps.ncmp.api.impl.inventory.CmHandleQueries
39 import org.onap.cps.ncmp.api.impl.inventory.CmHandleState
40 import org.onap.cps.ncmp.api.impl.inventory.CompositeState
41 import org.onap.cps.ncmp.api.impl.inventory.CompositeStateBuilder
42 import org.onap.cps.ncmp.api.impl.inventory.DataStoreSyncState
43 import org.onap.cps.spi.FetchDescendantsOption
44 import org.onap.cps.spi.model.DataNode
45 import org.onap.cps.utils.JsonObjectMapper
46 import org.springframework.http.HttpStatus
47 import org.springframework.http.ResponseEntity
48 import spock.lang.Specification
49 import java.time.OffsetDateTime
50 import java.time.format.DateTimeFormatter
51 import java.util.stream.Collectors
53 class ModuleOperationsUtilsSpec extends Specification{
55 def mockCmHandleQueries = Mock(CmHandleQueries)
57 def mockDmiDataOperations = Mock(DmiDataOperations)
59 def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
61 def objectUnderTest = new ModuleOperationsUtils(mockCmHandleQueries, mockDmiDataOperations, jsonObjectMapper)
63 def static neverUpdatedBefore = '1900-01-01T00:00:00.000+0100'
65 def static now = OffsetDateTime.now()
67 def static nowAsString = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(now)
69 def static dataNode = new DataNode(leaves: ['id': 'cm-handle-123'])
71 def applicationContext = new AnnotationConfigApplicationContext()
73 def logger = (Logger) LoggerFactory.getLogger(ModuleOperationsUtils)
74 def loggingListAppender
77 logger.setLevel(Level.DEBUG)
78 loggingListAppender = new ListAppender()
79 logger.addAppender(loggingListAppender)
80 loggingListAppender.start()
81 applicationContext.refresh()
85 ((Logger) LoggerFactory.getLogger(ModuleOperationsUtils.class)).detachAndStopAllAppenders()
86 applicationContext.close()
89 def 'Get an advised Cm-Handle where ADVISED cm handle #scenario'() {
90 given: 'the inventory persistence service returns a collection of data nodes'
91 mockCmHandleQueries.queryCmHandlesByState(CmHandleState.ADVISED) >> dataNodeCollection
92 when: 'get advised cm handles are fetched'
93 def yangModelCmHandles = objectUnderTest.getAdvisedCmHandles()
94 then: 'the returned data node collection is the correct size'
95 yangModelCmHandles.size() == expectedDataNodeSize
96 where: 'the following scenarios are used'
97 scenario | dataNodeCollection || expectedCallsToGetYangModelCmHandle | expectedDataNodeSize
98 'exists' | [dataNode] || 1 | 1
99 'does not exist' | [] || 0 | 0
102 def 'Update Lock Reason, Details and Attempts where lock reason #scenario'() {
103 given: 'A locked state'
104 def compositeState = new CompositeState(lockReason: lockReason)
105 when: 'update cm handle details and attempts is called'
106 objectUnderTest.updateLockReasonDetailsAndAttempts(compositeState, MODULE_SYNC_FAILED, 'new error message')
107 then: 'the composite state lock reason and details are updated'
108 assert compositeState.lockReason.lockReasonCategory == MODULE_SYNC_FAILED
109 assert compositeState.lockReason.details.contains(expectedDetails)
111 scenario | lockReason || expectedDetails
112 'does not exist' | null || 'Attempt #1 failed: new error message'
113 'exists' | CompositeState.LockReason.builder().details("Attempt #2 failed: some error message").build() || 'Attempt #3 failed: new error message'
116 def 'Update lock reason details that contains #scenario'() {
117 given: 'A locked state'
118 def compositeState = new CompositeStateBuilder().withCmHandleState(CmHandleState.LOCKED)
119 .withLockReason(MODULE_UPGRADE, "Upgrade to ModuleSetTag: " + moduleSetTag).build()
120 when: 'update cm handle details'
121 objectUnderTest.updateLockReasonDetailsAndAttempts(compositeState, MODULE_UPGRADE_FAILED, 'new error message')
122 then: 'the composite state lock reason and details are updated'
123 assert compositeState.lockReason.lockReasonCategory == MODULE_UPGRADE_FAILED
124 assert compositeState.lockReason.details.contains("Upgrade to ModuleSetTag: " + expectedDetails)
126 scenario | moduleSetTag || expectedDetails
127 'a module set tag' | 'someModuleSetTag' || 'someModuleSetTag'
128 'empty module set tag' | '' || 'not-specified'
131 def 'Get all locked cm-Handles where lock reasons are model sync failed or upgrade'() {
132 given: 'the cps (persistence service) returns a collection of data nodes'
133 mockCmHandleQueries.queryCmHandleAncestorsByCpsPath(ModuleOperationsUtils.CPS_PATH_CM_HANDLES_MODEL_SYNC_FAILED_OR_UPGRADE,
134 FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode]
135 when: 'get locked Misbehaving cm handle is called'
136 def result = objectUnderTest.getCmHandlesThatFailedModelSyncOrUpgrade()
137 then: 'the returned cm handle collection is the correct size'
139 and: 'the correct cm handle is returned'
140 result[0].id == 'cm-handle-123'
143 def 'Retry Locked Cm-Handle where the last update time is #scenario'() {
144 given: 'Last update was #lastUpdateMinutesAgo minutes ago (-1 means never)'
145 def lastUpdatedTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(now.minusMinutes(lastUpdateMinutesAgo))
146 if (lastUpdateMinutesAgo < 0 ) {
147 lastUpdatedTime = neverUpdatedBefore
149 when: 'checking to see if cm handle is ready for retry'
150 def result = objectUnderTest.needsModuleSyncRetryOrUpgrade(new CompositeStateBuilder()
151 .withLockReason(MODULE_SYNC_FAILED, lockDetails)
152 .withLastUpdatedTime(lastUpdatedTime).build())
153 then: 'retry is only attempted when expected'
154 assert result == retryExpected
155 and: 'logs contain related information'
156 def logs = loggingListAppender.list.toString()
157 assert logs.contains(logReason)
158 where: 'the following parameters are used'
159 scenario | lastUpdateMinutesAgo | lockDetails | logReason || retryExpected
160 'never attempted before' | -1 | 'Fist attempt:' | 'First Attempt:' || true
161 '1st attempt, last attempt > 2 minute ago' | 3 | 'Attempt #1 failed: some error' | 'Retry due now' || true
162 '2nd attempt, last attempt < 4 minutes ago' | 1 | 'Attempt #2 failed: some error' | 'Time until next attempt is 3 minutes:' || false
163 '2nd attempt, last attempt > 4 minutes ago' | 5 | 'Attempt #2 failed: some error' | 'Retry due now' || true
166 def 'Retry Locked Cm-Handle with lock reasons (category) #lockReasonCategory'() {
167 when: 'checking to see if cm handle is ready for retry'
168 def result = objectUnderTest.needsModuleSyncRetryOrUpgrade(new CompositeStateBuilder()
169 .withLockReason(lockReasonCategory, 'some details')
170 .withLastUpdatedTime(nowAsString).build())
171 then: 'verify retry attempts'
173 and: 'logs contain related information'
174 def logs = loggingListAppender.list.toString()
175 assert logs.contains(logReason)
176 where: 'the following lock reasons occurred'
177 scenario | lockReasonCategory || logReason
178 'module upgrade' | MODULE_UPGRADE_FAILED || 'First Attempt:'
179 'module sync failed' | MODULE_SYNC_FAILED || 'First Attempt:'
180 'lock misbehaving' | LOCKED_MISBEHAVING || 'Locked for other reason'
183 def 'Get a Cm-Handle where #scenario'() {
184 given: 'the inventory persistence service returns a collection of data nodes'
185 mockCmHandleQueries.queryCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED) >> unSynchronizedDataNodes
186 mockCmHandleQueries.cmHandleHasState('cm-handle-123', CmHandleState.READY) >> cmHandleHasState
187 when: 'get advised cm handles are fetched'
188 def yangModelCollection = objectUnderTest.getUnsynchronizedReadyCmHandles()
189 then: 'the returned data node collection is the correct size'
190 yangModelCollection.size() == expectedDataNodeSize
191 and: 'the result contains the correct data'
192 yangModelCollection.stream().map(yangModel -> yangModel.id).collect(Collectors.toSet()) == expectedYangModelCollectionIds
193 where: 'the following scenarios are used'
194 scenario | unSynchronizedDataNodes | cmHandleHasState || expectedDataNodeSize | expectedYangModelCollectionIds
195 'a Cm-Handle unsynchronized and ready' | [dataNode] | true || 1 | ['cm-handle-123'] as Set
196 'a Cm-Handle unsynchronized but not ready' | [dataNode] | false || 0 | [] as Set
197 'all Cm-Handle synchronized' | [] | false || 0 | [] as Set
200 def 'Get resource data through DMI Operations #scenario'() {
201 given: 'the inventory persistence service returns a collection of data nodes'
202 def jsonString = '{"stores:bookstore":{"categories":[{"code":"01"}]}}'
203 JsonNode jsonNode = jsonObjectMapper.convertToJsonNode(jsonString);
204 def responseEntity = new ResponseEntity<>(jsonNode, HttpStatus.OK)
205 mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_OPERATIONAL.datastoreName, 'cm-handle-123', _) >> responseEntity
206 when: 'get resource data is called'
207 def result = objectUnderTest.getResourceData('cm-handle-123')
208 then: 'the returned data is correct'
212 def 'Extract module set tag and number of attempt when lock reason contains #scenario'() {
213 expect: 'lock reason details are extracted correctly'
214 def result = objectUnderTest.getLockedCompositeStateDetails(new CompositeStateBuilder().withLockReason(MODULE_UPGRADE, lockReasonDetails).build().lockReason)
215 and: 'the result contains the correct moduleSetTag'
216 assert result['moduleSetTag'] == expectedModuleSetTag
217 and: 'the result contains the correct number of attempts'
218 assert result['attempt'] == expectedNumberOfAttempts
219 where: 'the following scenarios are used'
220 scenario | lockReasonDetails || expectedModuleSetTag | expectedNumberOfAttempts
221 'module set tag only' | 'Upgrade to ModuleSetTag: targetModuleSetTag' || 'targetModuleSetTag' | null
222 'number of attempts only' | 'Attempt #1 failed: some error' || null | '1'
223 'number of attempts and module set tag both' | 'Upgrade to ModuleSetTag: targetModuleSetTag Attempt #1 failed: some error' || 'targetModuleSetTag' | '1'