2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2021-2024 Nordix Foundation
4 * Modifications Copyright (C) 2021 Pantheon.tech
5 * Modifications Copyright (C) 2021-2022 Bell Canada
6 * Modifications Copyright (C) 2023 TechMahindra Ltd.
7 * ================================================================================
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
20 * SPDX-License-Identifier: Apache-2.0
21 * ============LICENSE_END=========================================================
24 package org.onap.cps.ncmp.api.impl
25 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
26 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
27 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
28 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.OPERATIONAL
29 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL
30 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING
31 import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE
32 import static org.onap.cps.ncmp.api.impl.operations.OperationType.UPDATE
34 import com.hazelcast.map.IMap
35 import org.onap.cps.ncmp.api.NetworkCmProxyCmHandleQueryService
36 import org.onap.cps.ncmp.api.impl.events.lcm.LcmEventsCmHandleStateHandler
37 import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevel
38 import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevelManager
39 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
40 import org.onap.cps.ncmp.api.impl.inventory.CmHandleQueries
41 import org.onap.cps.ncmp.api.impl.inventory.CmHandleState
42 import org.onap.cps.ncmp.api.impl.inventory.CompositeState
43 import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence
44 import org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory
45 import org.onap.cps.ncmp.api.impl.inventory.DataStoreSyncState
46 import org.onap.cps.ncmp.api.models.DataOperationDefinition
47 import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters
48 import org.onap.cps.ncmp.api.models.CmHandleQueryServiceParameters
49 import org.onap.cps.ncmp.api.models.ConditionApiProperties
50 import org.onap.cps.ncmp.api.models.DmiPluginRegistration
51 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
52 import org.onap.cps.ncmp.api.models.DataOperationRequest
53 import org.onap.cps.spi.exceptions.CpsException
54 import org.onap.cps.spi.model.ConditionProperties
55 import spock.lang.Shared
56 import java.util.stream.Collectors
57 import org.onap.cps.utils.JsonObjectMapper
58 import com.fasterxml.jackson.databind.ObjectMapper
59 import org.onap.cps.api.CpsDataService
60 import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations
61 import org.onap.cps.spi.FetchDescendantsOption
62 import org.onap.cps.spi.model.DataNode
63 import org.springframework.http.HttpStatus
64 import org.springframework.http.ResponseEntity
65 import spock.lang.Specification
67 class NetworkCmProxyDataServiceImplSpec extends Specification {
69 def mockCpsDataService = Mock(CpsDataService)
70 def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
71 def mockDmiDataOperations = Mock(DmiDataOperations)
72 def nullNetworkCmProxyDataServicePropertyHandler = null
73 def mockInventoryPersistence = Mock(InventoryPersistence)
74 def mockCmHandleQueries = Mock(CmHandleQueries)
75 def mockDmiPluginRegistration = Mock(DmiPluginRegistration)
76 def mockCpsCmHandlerQueryService = Mock(NetworkCmProxyCmHandleQueryService)
77 def mockLcmEventsCmHandleStateHandler = Mock(LcmEventsCmHandleStateHandler)
78 def stubModuleSyncStartedOnCmHandles = Stub(IMap<String, Object>)
79 def stubTrustLevelPerDmiPlugin = Stub(Map<String, TrustLevel>)
80 def mockTrustLevelManager = Mock(TrustLevelManager)
83 def NO_REQUEST_ID = null
85 def OPTIONS_PARAM = '(a=1,b=2)'
87 def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: 'test-cm-handle-id')
89 def objectUnderTest = new NetworkCmProxyDataServiceImpl(
90 spiedJsonObjectMapper,
91 mockDmiDataOperations,
92 nullNetworkCmProxyDataServicePropertyHandler,
93 mockInventoryPersistence,
95 mockCpsCmHandlerQueryService,
96 mockLcmEventsCmHandleStateHandler,
98 stubModuleSyncStartedOnCmHandles,
99 stubTrustLevelPerDmiPlugin,
100 mockTrustLevelManager)
102 def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']"
104 def dataNode = [new DataNode(leaves: ['id': 'some-cm-handle', 'dmi-service-name': 'testDmiService'])]
106 def 'Write resource data for pass-through running from DMI using POST.'() {
107 given: 'cpsDataService returns valid datanode'
109 when: 'write resource data is called'
110 objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
111 'testResourceId', CREATE,
112 '{some-json}', 'application/json')
113 then: 'DMI called with correct data'
114 1 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi('testCmHandle', 'testResourceId',
115 CREATE, '{some-json}', 'application/json')
116 >> { new ResponseEntity<>(HttpStatus.CREATED) }
119 def 'Get resource data for pass-through operational from DMI.'() {
120 given: 'cpsDataService returns valid data node'
122 and: 'get resource data from DMI is called'
123 mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_OPERATIONAL.datastoreName,'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID) >>
124 new ResponseEntity<>('dmi-response', HttpStatus.OK)
125 when: 'get resource data operational for cm-handle is called'
126 def response = objectUnderTest.getResourceDataForCmHandle(PASSTHROUGH_OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID)
127 then: 'DMI returns a json response'
128 assert response == 'dmi-response'
131 def 'Get resource data for pass-through running from DMI.'() {
132 given: 'cpsDataService returns valid data node'
134 and: 'DMI returns valid response and data'
135 mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID) >>
136 new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
137 when: 'get resource data is called'
138 def response = objectUnderTest.getResourceDataForCmHandle(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID)
139 then: 'get resource data returns expected response'
140 assert response == '{dmi-response}'
143 def 'Get resource data for operational (cached) data.'() {
144 given: 'CPS Data service returns some object(s)'
145 mockCpsDataService.getDataNodes(OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId', FetchDescendantsOption.OMIT_DESCENDANTS) >> ['First Object', 'other Object']
146 when: 'get resource data is called'
147 def response = objectUnderTest.getResourceDataForCmHandle(OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId', FetchDescendantsOption.OMIT_DESCENDANTS)
148 then: 'get resource data returns teh first object from the data service'
149 assert response == 'First Object'
152 def 'Execute (async) data operation for #datastoreName from DMI.'() {
153 given: 'cpsDataService returns valid data node'
154 def dataOperationRequest = getDataOperationRequest(datastoreName)
155 when: 'request resource data for data operation is called'
156 objectUnderTest.executeDataOperationForCmHandles('some topic', dataOperationRequest, 'requestId')
157 then: 'request resource data for data operation returns expected response'
158 1 * mockDmiDataOperations.requestResourceDataFromDmi('some topic', dataOperationRequest, 'requestId')
159 where: 'the following data stores are used'
160 datastoreName << [PASSTHROUGH_RUNNING.datastoreName, PASSTHROUGH_OPERATIONAL.datastoreName]
163 def 'Getting Yang Resources.'() {
164 when: 'yang resources is called'
165 objectUnderTest.getYangResourcesModuleReferences('some-cm-handle')
166 then: 'CPS module services is invoked for the correct dataspace and cm handle'
167 1 * mockInventoryPersistence.getYangResourcesModuleReferences('some-cm-handle')
170 def 'Get a cm handle.'() {
171 given: 'the system returns a yang modelled cm handle'
172 def dmiServiceName = 'some service name'
173 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED,
174 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.MODULE_SYNC_FAILED).details("lock details").build(),
175 lastUpdateTime: 'some-timestamp',
176 dataSyncEnabled: false,
177 dataStores: dataStores())
178 def dmiProperties = [new YangModelCmHandle.Property('Book', 'Romance Novel')]
179 def publicProperties = [new YangModelCmHandle.Property('Public Book', 'Public Romance Novel')]
180 def yangModelCmHandle = new YangModelCmHandle(id: 'some-cm-handle', dmiServiceName: dmiServiceName,
181 dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
182 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
183 when: 'getting cm handle details for a given cm handle id from ncmp service'
184 def result = objectUnderTest.getNcmpServiceCmHandle('some-cm-handle')
185 then: 'the result is a ncmpServiceCmHandle'
186 result.class == NcmpServiceCmHandle.class
187 and: 'the cm handle contains the cm handle id'
188 result.cmHandleId == 'some-cm-handle'
189 and: 'the cm handle contains the DMI Properties'
190 result.dmiProperties ==[ Book:'Romance Novel' ]
191 and: 'the cm handle contains the public Properties'
192 result.publicProperties == [ "Public Book":'Public Romance Novel' ]
193 and: 'the cm handle contains the cm handle composite state'
194 result.compositeState == compositeState
198 def 'Get cm handle public properties'() {
199 given: 'a yang modelled cm handle'
200 def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')]
201 def publicProperties = [new YangModelCmHandle.Property('public prop', 'some public prop')]
202 def yangModelCmHandle = new YangModelCmHandle(id:'some-cm-handle', dmiServiceName: 'some service name', dmiProperties: dmiProperties, publicProperties: publicProperties)
203 and: 'the system returns this yang modelled cm handle'
204 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
205 when: 'getting cm handle public properties for a given cm handle id from ncmp service'
206 def result = objectUnderTest.getCmHandlePublicProperties('some-cm-handle')
207 then: 'the result returns the correct data'
208 result == [ 'public prop' : 'some public prop' ]
211 def 'Execute cm handle id search for inventory'() {
212 given: 'a ConditionApiProperties object'
213 def conditionProperties = new ConditionProperties()
214 conditionProperties.conditionName = 'hasAllProperties'
215 conditionProperties.conditionParameters = [ [ 'some-key' : 'some-value' ] ]
216 def conditionServiceProps = new CmHandleQueryServiceParameters()
217 conditionServiceProps.cmHandleQueryParameters = [conditionProperties] as List<ConditionProperties>
218 and: 'the system returns an set of cmHandle ids'
219 mockCpsCmHandlerQueryService.queryCmHandleIdsForInventory(*_) >> [ 'cmHandle1', 'cmHandle2' ]
220 when: 'getting cm handle id set for a given dmi property'
221 def result = objectUnderTest.executeCmHandleIdSearchForInventory(conditionServiceProps)
222 then: 'the result returns the correct 2 elements'
223 assert result.size() == 2
224 assert result.contains('cmHandle1')
225 assert result.contains('cmHandle2')
228 def 'Get cm handle composite state'() {
229 given: 'a yang modelled cm handle'
230 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED,
231 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.MODULE_SYNC_FAILED).details("lock details").build(),
232 lastUpdateTime: 'some-timestamp',
233 dataSyncEnabled: false,
234 dataStores: dataStores())
235 def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')]
236 def publicProperties = [new YangModelCmHandle.Property('public prop', 'some public prop')]
237 def yangModelCmHandle = new YangModelCmHandle(id:'some-cm-handle', dmiServiceName: 'some service name', dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
238 and: 'the system returns this yang modelled cm handle'
239 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
240 when: 'getting cm handle composite state for a given cm handle id from ncmp service'
241 def result = objectUnderTest.getCmHandleCompositeState('some-cm-handle')
242 then: 'the result returns the correct data'
243 result == compositeState
246 def 'Update resource data for pass-through running from dmi using POST #scenario DMI properties.'() {
247 given: 'cpsDataService returns valid datanode'
248 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
249 when: 'get resource data is called'
250 objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
251 'testResourceId', UPDATE,
252 '{some-json}', 'application/json')
253 then: 'DMI called with correct data'
254 1 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi('testCmHandle', 'testResourceId',
255 UPDATE, '{some-json}', 'application/json')
256 >> { new ResponseEntity<>(HttpStatus.OK) }
259 def 'Verify modules and create anchor params.'() {
260 given: 'dmi plugin registration return created cm handles'
261 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'service1', dmiModelPlugin: 'service1',
262 dmiDataPlugin: 'service2')
263 dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
264 mockDmiPluginRegistration.getCreatedCmHandles() >> [ncmpServiceCmHandle]
265 when: 'parse and create cm handle in dmi registration then sync module'
266 objectUnderTest.parseAndProcessCreatedCmHandlesInRegistration(mockDmiPluginRegistration)
267 then: 'system persists the cm handle state'
268 1 * mockLcmEventsCmHandleStateHandler.initiateStateAdvised(_) >> {
270 def cmHandleStatePerCmHandle = (args[0] as Collection)
271 cmHandleStatePerCmHandle.each {
272 assert it.id == 'test-cm-handle-id'
278 def 'Execute cm handle id search'() {
279 given: 'valid CmHandleQueryApiParameters input'
280 def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
281 def conditionApiProperties = new ConditionApiProperties()
282 conditionApiProperties.conditionName = 'hasAllModules'
283 conditionApiProperties.conditionParameters = [[moduleName: 'module-name-1']]
284 cmHandleQueryApiParameters.cmHandleQueryParameters = [conditionApiProperties]
285 and: 'query cm handle method return with a data node list'
286 mockCpsCmHandlerQueryService.queryCmHandleIds(
287 spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
288 >> ['cm-handle-id-1']
289 when: 'execute cm handle search is called'
290 def result = objectUnderTest.executeCmHandleIdSearch(cmHandleQueryApiParameters)
291 then: 'result is the same collection as returned by the CPS Data Service'
292 assert result == ['cm-handle-id-1']
295 def 'Getting module definitions by module'() {
296 when: 'get module definitions is performed with module name'
297 objectUnderTest.getModuleDefinitionsByCmHandleAndModule('some-cm-handle', 'some-module', '2021-08-04')
298 then: 'ncmp inventory persistence service is invoked once with correct parameters'
299 1 * mockInventoryPersistence.getModuleDefinitionsByCmHandleAndModule('some-cm-handle', 'some-module', '2021-08-04')
302 def 'Getting module definitions by cm handle id'() {
303 when: 'get module definitions is performed with cm handle id'
304 objectUnderTest.getModuleDefinitionsByCmHandleId('some-cm-handle')
305 then: 'ncmp inventory persistence service is invoked once with correct parameter'
306 1 * mockInventoryPersistence.getModuleDefinitionsByCmHandleId('some-cm-handle')
309 def 'Execute cm handle search'() {
310 given: 'valid CmHandleQueryApiParameters input'
311 def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
312 def conditionApiProperties = new ConditionApiProperties()
313 conditionApiProperties.conditionName = 'hasAllModules'
314 conditionApiProperties.conditionParameters = [[moduleName: 'module-name-1']]
315 cmHandleQueryApiParameters.cmHandleQueryParameters = [conditionApiProperties]
316 and: 'query cm handle method return with a data node list'
317 mockCpsCmHandlerQueryService.queryCmHandles(
318 spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
319 >> [new NcmpServiceCmHandle(cmHandleId: 'cm-handle-id-1')]
320 when: 'execute cm handle search is called'
321 def result = objectUnderTest.executeCmHandleSearch(cmHandleQueryApiParameters)
322 then: 'result is the same collection as returned by the CPS Data Service'
323 assert result.stream().map(d -> d.cmHandleId).collect(Collectors.toSet()) == ['cm-handle-id-1'] as Set
326 def 'Set Cm Handle Data Sync Enabled Flag where data sync flag is #scenario'() {
327 given: 'an existing cm handle composite state'
328 def compositeState = new CompositeState(cmHandleState: CmHandleState.READY, dataSyncEnabled: initialDataSyncEnabledFlag,
329 dataStores: CompositeState.DataStores.builder()
330 .operationalDataStore(CompositeState.Operational.builder()
331 .dataStoreSyncState(initialDataSyncState)
333 and: 'get cm handle state returns the composite state for the given cm handle id'
334 mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
335 when: 'set data sync enabled is called with the data sync enabled flag set to #dataSyncEnabledFlag'
336 objectUnderTest.setDataSyncEnabled('some-cm-handle-id', dataSyncEnabledFlag)
337 then: 'the data sync enabled flag is set to #dataSyncEnabled'
338 compositeState.dataSyncEnabled == dataSyncEnabledFlag
339 and: 'the data store sync state is set to #expectedDataStoreSyncState'
340 compositeState.dataStores.operationalDataStore.dataStoreSyncState == expectedDataStoreSyncState
341 and: 'the cps data service to delete data nodes is invoked the expected number of times'
342 deleteDataNodeExpectedNumberOfInvocation * mockCpsDataService.deleteDataNode(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'some-cm-handle-id', '/netconf-state', _)
343 and: 'the inventory persistence service to update node leaves is called with the correct values'
344 saveCmHandleStateExpectedNumberOfInvocations * mockInventoryPersistence.saveCmHandleState('some-cm-handle-id', compositeState)
345 where: 'the following data sync enabled flag is used'
346 scenario | dataSyncEnabledFlag | initialDataSyncEnabledFlag | initialDataSyncState || expectedDataStoreSyncState | deleteDataNodeExpectedNumberOfInvocation | saveCmHandleStateExpectedNumberOfInvocations
347 'enabled' | true | false | DataStoreSyncState.NONE_REQUESTED || DataStoreSyncState.UNSYNCHRONIZED | 0 | 1
348 'disabled' | false | true | DataStoreSyncState.UNSYNCHRONIZED || DataStoreSyncState.NONE_REQUESTED | 0 | 1
349 'disabled where sync-state is currently SYNCHRONIZED' | false | true | DataStoreSyncState.SYNCHRONIZED || DataStoreSyncState.NONE_REQUESTED | 1 | 1
350 'is set to existing flag state' | true | true | DataStoreSyncState.UNSYNCHRONIZED || DataStoreSyncState.UNSYNCHRONIZED | 0 | 0
353 def 'Set cm Handle Data Sync Enabled flag with following cm handle not in ready state exception' () {
354 given: 'a cm handle composite state'
355 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED, dataSyncEnabled: false)
356 and: 'get cm handle state returns the composite state for the given cm handle id'
357 mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
358 when: 'set data sync enabled is called with the data sync enabled flag set to true'
359 objectUnderTest.setDataSyncEnabled('some-cm-handle-id', true)
360 then: 'the expected exception is thrown'
362 and: 'the inventory persistence service to update node leaves is not invoked'
363 0 * mockInventoryPersistence.saveCmHandleState(_, _)
366 def 'Get all cm handle IDs by DMI plugin identifier.' () {
367 given: 'cm handle queries service returns cm handles'
368 1 * mockCmHandleQueries.getCmHandleIdsByDmiPluginIdentifier('some-dmi-plugin-identifier') >> ['cm-handle-1','cm-handle-2']
369 when: 'cm handle Ids are requested with dmi plugin identifier'
370 def result = objectUnderTest.getAllCmHandleIdsByDmiPluginIdentifier('some-dmi-plugin-identifier')
371 then: 'the result size is correct'
372 assert result.size() == 2
373 and: 'the result returns the correct details'
374 assert result.containsAll('cm-handle-1','cm-handle-2')
378 CompositeState.DataStores.builder()
379 .operationalDataStore(CompositeState.Operational.builder()
380 .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED)
381 .lastSyncTime('some-timestamp').build()).build()
385 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
388 def getDataOperationRequest(datastore) {
389 def dataOperationRequest = new DataOperationRequest()
390 def dataOperationDefinitions = new ArrayList()
391 dataOperationDefinitions.add(getDataOperationDefinition(datastore))
392 dataOperationRequest.setDataOperationDefinitions(dataOperationDefinitions)
393 return dataOperationRequest
396 def getDataOperationDefinition(datastore) {
397 def dataOperationDefinition = new DataOperationDefinition()
398 dataOperationDefinition.setOperation("read")
399 dataOperationDefinition.setOperationId("operational-12")
400 dataOperationDefinition.setDatastore(datastore)
401 def targetIds = new ArrayList()
402 targetIds.add("some-cm-handle")
403 dataOperationDefinition.setCmHandleIds(targetIds)
404 return dataOperationDefinition