Merge "Clean up Dependencies"
authorToine Siebelink <toine.siebelink@est.tech>
Mon, 16 Oct 2023 16:14:49 +0000 (16:14 +0000)
committerGerrit Code Review <gerrit@onap.org>
Mon, 16 Oct 2023 16:14:49 +0000 (16:14 +0000)
35 files changed:
.gitignore
cps-ncmp-events/src/main/resources/schemas/trustlevel/device-trust-level-event-schema-1.0.0.json [new file with mode: 0644]
cps-ncmp-rest-stub/cps-ncmp-rest-stub-service/pom.xml
cps-ncmp-rest-stub/cps-ncmp-rest-stub-service/src/main/java/org/onap/cps/ncmp/rest/stub/controller/NetworkCmProxyStubController.java
cps-ncmp-rest-stub/cps-ncmp-rest-stub-service/src/test/groovy/org/onap/cps/ncmp/rest/stub/SampleCpsNcmpClientSpec.groovy [new file with mode: 0644]
cps-ncmp-rest-stub/cps-ncmp-rest-stub-service/src/test/groovy/org/onap/cps/ncmp/rest/stub/TestApplication.groovy [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandleQueryServiceImpl.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/embeddedcache/TrustLevelCacheConfig.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/mapper/CloudEventMapper.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/inventory/CmHandleQueries.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/inventory/CmHandleQueriesImpl.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/trustlevel/DeviceHeartbeatConsumer.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/trustlevel/TrustLevel.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/trustlevel/TrustLevelFilter.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/trustlevel/dmiavailability/DMiPluginWatchDog.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/CmHandleQueryConditions.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandleQueryServiceSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/embeddedcache/TrustLevelCacheConfigSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/mapper/CloudEventMapperSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/trustlevel/DeviceHeartbeatConsumerSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/trustlevel/TrustLevelFilterSpec.groovy [moved from cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/trustlevel/DeviceTrustLevel.java with 55% similarity]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/trustlevel/dmiavailability/DMiPluginWatchDogSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/CmHandleQueryConditionsSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CmHandleQueriesImplSpec.groovy
docs/cps-path.rst
docs/cps-stubs.rst [new file with mode: 0644]
docs/deployment.rst
docs/design.rst
docs/index.rst
docs/release-notes.rst
docs/spelling_wordlist.txt [new file with mode: 0644]
docs/tox.ini

index b4abc48..c59fc47 100755 (executable)
@@ -27,10 +27,10 @@ tmp/
 .checkstyle
 
 /.tox
-/_build/*
 /__pycache__/*
 
 /docs/docs/
 /docs/.vscode/
+/docs/_build/*
 
 /test-tools/metrics-reports/
diff --git a/cps-ncmp-events/src/main/resources/schemas/trustlevel/device-trust-level-event-schema-1.0.0.json b/cps-ncmp-events/src/main/resources/schemas/trustlevel/device-trust-level-event-schema-1.0.0.json
new file mode 100644 (file)
index 0000000..e1796fb
--- /dev/null
@@ -0,0 +1,30 @@
+{
+  "$schema": "https://json-schema.org/draft/2019-09/schema",
+  "$id": "urn:cps:org.onap.cps.ncmp.events:device-trust-level-event-schema:1.0.0",
+  "$ref": "#/definitions/DeviceTrustLevel",
+  "definitions": {
+    "DeviceTrustLevel" : {
+      "description": "The payload for device trust level event.",
+      "type": "object",
+      "javaType": "org.onap.cps.ncmp.events.trustlevel.DeviceTrustLevel",
+      "properties": {
+        "data": {
+          "type": "object",
+          "properties": {
+            "trustLevel": {
+              "type": "string"
+            }
+          },
+          "required": [
+            "trustLevel"
+          ],
+          "additionalProperties": false
+        }
+      },
+      "additionalProperties": false,
+      "required": [
+        "data"
+      ]
+    }
+  }
+}
\ No newline at end of file
index 83699b9..d2cce87 100644 (file)
             <groupId>org.onap.cps</groupId>
             <artifactId>cps-ncmp-rest</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-server</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+        </dependency>
         <!-- T E S T - D E P E N D E N C I E S -->
         <dependency>
             <groupId>org.spockframework</groupId>
             <artifactId>spock-core</artifactId>
             <scope>test</scope>
         </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.junit.vintage</groupId>
+                    <artifactId>junit-vintage-engine</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.spockframework</groupId>
+            <artifactId>spock-spring</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.spockframework</groupId>
+            <artifactId>spock-core</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
+    
 </project>
\ No newline at end of file
index 198b14f..e33af45 100644 (file)
@@ -141,13 +141,13 @@ public class NetworkCmProxyStubController implements NetworkCmProxyApi {
     }
 
     @Override
-    public ResponseEntity<Void> createResourceDataRunningForCmHandle(@NotNull @Valid final String resourceIdentifier,
-                                                                     final String datastoreName, final String cmHandle,
-                                                                     @Valid final Object body,
+    public ResponseEntity<Void> createResourceDataRunningForCmHandle(final String datastoreName, final String cmHandle,
+                                                                     @NotNull @Valid final String resourceIdentifier, 
+                                                                     @Valid final Object body, 
                                                                      final String contentType) {
         return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
     }
-
+    
     @Override
     public ResponseEntity<Void> deleteResourceDataRunningForCmHandle(final String datastoreName, final String cmHandle,
                                                                      @NotNull @Valid final String resourceIdentifier,
@@ -183,13 +183,13 @@ public class NetworkCmProxyStubController implements NetworkCmProxyApi {
     }
 
     @Override
-    public ResponseEntity<Object> patchResourceDataRunningForCmHandle(@NotNull @Valid final String resourceIdentifier,
-                                                                      final String datastoreName, final String cmHandle,
-                                                                      @Valid final Object body,
+    public ResponseEntity<Object> patchResourceDataRunningForCmHandle(final String datastoreName, final String cmHandle,
+                                                                      @NotNull @Valid final String resourceIdentifier, 
+                                                                      @Valid final Object body, 
                                                                       final String contentType) {
         return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
     }
-
+    
     @Override
     public ResponseEntity<Object> queryResourceDataForCmHandle(final String datastoreName, final String cmHandle,
                                                                @Valid final String cpsPath, @Valid final String options,
@@ -218,9 +218,10 @@ public class NetworkCmProxyStubController implements NetworkCmProxyApi {
     }
 
     @Override
-    public ResponseEntity<Object> updateResourceDataRunningForCmHandle(@NotNull @Valid final String resourceIdentifier,
-                                                                       final String datastoreName,
-                                                                       final String cmHandle, @Valid final Object body,
+    public ResponseEntity<Object> updateResourceDataRunningForCmHandle(final String datastoreName, 
+                                                                       final String cmHandle, 
+                                                                       @NotNull @Valid final String resourceIdentifier, 
+                                                                       @Valid final Object body,
                                                                        final String contentType) {
         return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
 
diff --git a/cps-ncmp-rest-stub/cps-ncmp-rest-stub-service/src/test/groovy/org/onap/cps/ncmp/rest/stub/SampleCpsNcmpClientSpec.groovy b/cps-ncmp-rest-stub/cps-ncmp-rest-stub-service/src/test/groovy/org/onap/cps/ncmp/rest/stub/SampleCpsNcmpClientSpec.groovy
new file mode 100644 (file)
index 0000000..b36e25a
--- /dev/null
@@ -0,0 +1,82 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2023 Nordix Foundation.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+package org.onap.cps.ncmp.rest.stub
+
+import org.onap.cps.ncmp.api.impl.operations.DatastoreType
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration
+import org.springframework.boot.test.context.SpringBootContextLoader
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.boot.test.web.client.TestRestTemplate
+import org.springframework.boot.test.web.server.LocalServerPort
+import org.springframework.http.HttpStatus
+import org.springframework.test.context.ActiveProfiles
+import org.springframework.test.context.ContextConfiguration
+
+import com.fasterxml.jackson.core.JsonProcessingException
+import com.fasterxml.jackson.core.type.TypeReference
+import com.fasterxml.jackson.databind.JsonMappingException
+import com.fasterxml.jackson.databind.ObjectMapper
+
+import spock.lang.Shared
+import spock.lang.Specification
+
+@SpringBootTest(classes = TestApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@ActiveProfiles("test")
+class SampleCpsNcmpClientSpec extends Specification {
+
+    static final String CM_HANDLE = "anything"
+
+    static final String DATA_STORE_NAME = DatastoreType.PASSTHROUGH_OPERATIONAL.getDatastoreName()
+
+    @LocalServerPort
+    def port
+
+    final def testRestTemplate = new TestRestTemplate()
+
+    @Value('${rest.api.ncmp-stub-base-path}')
+    def stubBasePath
+
+    @Autowired
+    ObjectMapper objectMapper
+
+    def 'Test the invocation of the stub API' () throws JsonMappingException, JsonProcessingException {
+
+        when: 'Get resource data for cm handle URL is invoked'
+            def url = "${getBaseUrl()}/v1/ch/${CM_HANDLE}/data/ds/${DATA_STORE_NAME}?resourceIdentifier=parent&options=(a=1,b=2)"
+            def response = testRestTemplate.getForEntity(url, String.class)
+        then: 'Response is OK'
+            response.getStatusCode() == HttpStatus.OK
+
+        and: 'Response body contains customized stub response that contains correct bookstore category code'
+            def typeRef = new TypeReference<HashMap<String, Object>>() {}
+            def map = objectMapper.readValue(response.getBody(), typeRef)
+            def obj1 = (Map<String, Object>) map.get("stores:bookstore")
+            def obj2 = (List<Object>) obj1.get("categories")
+            def obj3 = (Map<String, Object>) obj2.iterator().next()
+
+            assert obj3.get("code") == "02"
+    }
+
+    def String getBaseUrl() {
+        return "http://localhost:" + port + stubBasePath
+    }
+}
diff --git a/cps-ncmp-rest-stub/cps-ncmp-rest-stub-service/src/test/groovy/org/onap/cps/ncmp/rest/stub/TestApplication.groovy b/cps-ncmp-rest-stub/cps-ncmp-rest-stub-service/src/test/groovy/org/onap/cps/ncmp/rest/stub/TestApplication.groovy
new file mode 100644 (file)
index 0000000..7938423
--- /dev/null
@@ -0,0 +1,28 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2023 Nordix Foundation.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+package org.onap.cps.ncmp.rest.stub
+
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.context.annotation.ComponentScan
+
+@SpringBootApplication
+@ComponentScan(basePackages = ["org.onap.cps.ncmp.rest.stub", "org.onap.cps.ncmp.rest.stub.controller", "org.onap.cps.ncmp.rest.api"])
+class TestApplication {
+}
index 7475cdd..1f6c948 100644 (file)
@@ -24,6 +24,7 @@ import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DM
 import static org.onap.cps.ncmp.api.impl.utils.CmHandleQueryConditions.HAS_ALL_MODULES;
 import static org.onap.cps.ncmp.api.impl.utils.CmHandleQueryConditions.HAS_ALL_PROPERTIES;
 import static org.onap.cps.ncmp.api.impl.utils.CmHandleQueryConditions.WITH_CPS_PATH;
+import static org.onap.cps.ncmp.api.impl.utils.CmHandleQueryConditions.WITH_TRUST_LEVEL;
 import static org.onap.cps.ncmp.api.impl.utils.RestQueryParametersValidator.validateCpsPathConditionProperties;
 import static org.onap.cps.ncmp.api.impl.utils.RestQueryParametersValidator.validateModuleNameConditionProperties;
 import static org.onap.cps.ncmp.api.impl.utils.YangDataConverter.convertYangModelCmHandleToNcmpServiceCmHandle;
@@ -70,7 +71,8 @@ public class NetworkCmProxyCmHandleQueryServiceImpl implements NetworkCmProxyCmH
         return executeQueries(cmHandleQueryServiceParameters,
             this::executeCpsPathQuery,
             this::queryCmHandlesByPublicProperties,
-            this::executeModuleNameQuery);
+            this::executeModuleNameQuery,
+                this::queryCmHandlesByTrustLevel);
     }
 
     @Override
@@ -117,9 +119,10 @@ public class NetworkCmProxyCmHandleQueryServiceImpl implements NetworkCmProxyCmH
                 getPropertyPairs(cmHandleQueryServiceParameters.getCmHandleQueryParameters(),
                         InventoryQueryConditions.HAS_ALL_ADDITIONAL_PROPERTIES.getName());
 
-        return privatePropertyQueryPairs.isEmpty()
-                ? NO_QUERY_TO_EXECUTE
-                : cmHandleQueries.queryCmHandleAdditionalProperties(privatePropertyQueryPairs);
+        if (privatePropertyQueryPairs.isEmpty()) {
+            return NO_QUERY_TO_EXECUTE;
+        }
+        return cmHandleQueries.queryCmHandleAdditionalProperties(privatePropertyQueryPairs);
     }
 
     private Collection<String> queryCmHandlesByPublicProperties(
@@ -129,9 +132,23 @@ public class NetworkCmProxyCmHandleQueryServiceImpl implements NetworkCmProxyCmH
                 getPropertyPairs(cmHandleQueryServiceParameters.getCmHandleQueryParameters(),
                         HAS_ALL_PROPERTIES.getConditionName());
 
-        return publicPropertyQueryPairs.isEmpty()
-                ? NO_QUERY_TO_EXECUTE
-                : cmHandleQueries.queryCmHandlePublicProperties(publicPropertyQueryPairs);
+        if (publicPropertyQueryPairs.isEmpty()) {
+            return NO_QUERY_TO_EXECUTE;
+        }
+        return cmHandleQueries.queryCmHandlePublicProperties(publicPropertyQueryPairs);
+    }
+
+    private Collection<String> queryCmHandlesByTrustLevel(final CmHandleQueryServiceParameters
+                                                                  cmHandleQueryServiceParameters) {
+
+        final Map<String, String> trustLevelPropertyQueryPairs =
+                getPropertyPairs(cmHandleQueryServiceParameters.getCmHandleQueryParameters(),
+                        WITH_TRUST_LEVEL.getConditionName());
+
+        if (trustLevelPropertyQueryPairs.isEmpty()) {
+            return NO_QUERY_TO_EXECUTE;
+        }
+        return cmHandleQueries.queryCmHandlesByTrustLevel(trustLevelPropertyQueryPairs);
     }
 
     private Collection<String> executeModuleNameQuery(
index 692a9f2..a37b271 100755 (executable)
@@ -99,7 +99,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
     private final LcmEventsCmHandleStateHandler lcmEventsCmHandleStateHandler;
     private final CpsDataService cpsDataService;
     private final IMap<String, Object> moduleSyncStartedOnCmHandles;
-    private final IMap<String, TrustLevel> trustLevelPerDmiPlugin;
+    private final Map<String, TrustLevel> trustLevelPerDmiPlugin;
 
     @Override
     public DmiPluginRegistrationResponse updateDmiRegistrationAndSyncModule(
index 6a8310c..e8ce050 100644 (file)
@@ -75,10 +75,8 @@ public class DmiRestClient {
             final HttpEntity<Object> httpHeaders = new HttpEntity<>(configureHttpHeaders(new HttpHeaders()));
             final JsonNode dmiPluginHealthStatus = restTemplate.getForObject(dmiPluginBaseUrl + "/manage/health",
                     JsonNode.class, httpHeaders);
-            if (dmiPluginHealthStatus != null) {
-                if (dmiPluginHealthStatus.get("status").asText().equals("UP")) {
-                    return DmiPluginStatus.UP;
-                }
+            if (dmiPluginHealthStatus != null && dmiPluginHealthStatus.get("status").asText().equals("UP")) {
+                return DmiPluginStatus.UP;
             }
         } catch (final Exception exception) {
             log.warn("Could not send request for health check since {}", exception.getMessage());
index ebe9905..171db52 100644 (file)
 
 package org.onap.cps.ncmp.api.impl.config.embeddedcache;
 
-import com.hazelcast.collection.ISet;
 import com.hazelcast.config.MapConfig;
-import com.hazelcast.config.SetConfig;
-import com.hazelcast.map.IMap;
+import java.util.Map;
 import org.onap.cps.cache.HazelcastCacheConfig;
 import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevel;
 import org.springframework.context.annotation.Bean;
@@ -32,21 +30,21 @@ import org.springframework.context.annotation.Configuration;
 @Configuration
 public class TrustLevelCacheConfig extends HazelcastCacheConfig {
 
-    private static final SetConfig untrustworthyCmHandlesSetConfig =
-            createSetConfig("untrustworthyCmHandlesSetConfig");
+    private static final MapConfig trustLevelPerCmHandleCacheConfig =
+            createMapConfig("trustLevelPerCmHandleCacheConfig");
 
     private static final MapConfig trustLevelPerDmiPluginCacheConfig =
             createMapConfig("trustLevelPerDmiPluginCacheConfig");
 
     /**
-     * Distributed collection of untrustworthy cm handles.
+     * Distributed instance of trust level cache containing the trust level per cm handle.
      *
-     * @return instance of distributed set of untrustworthy cm handles.
+     * @return configured map of cm handle name as keys to trust-level for values.
      */
     @Bean
-    public ISet<String> untrustworthyCmHandlesSet() {
-        return createHazelcastInstance("untrustworthyCmHandlesSet",
-                untrustworthyCmHandlesSetConfig).getSet("untrustworthyCmHandlesSet");
+    public Map<String, TrustLevel> trustLevelPerCmHandle() {
+        return createHazelcastInstance("hazelcastInstanceTrustLevelPerCmHandleMap",
+                trustLevelPerCmHandleCacheConfig).getMap("trustLevelPerCmHandle");
     }
 
     /**
@@ -55,7 +53,7 @@ public class TrustLevelCacheConfig extends HazelcastCacheConfig {
      * @return configured map of dmi-plugin name as keys to trust-level for values.
      */
     @Bean
-    public IMap<String, TrustLevel> trustLevelPerDmiPlugin() {
+    public Map<String, TrustLevel> trustLevelPerDmiPlugin() {
         return createHazelcastInstance("hazelcastInstanceTrustLevelPerDmiPluginMap",
                 trustLevelPerDmiPluginCacheConfig).getMap("trustLevelPerDmiPlugin");
     }
index 98ba953..4120970 100644 (file)
@@ -25,7 +25,6 @@ import io.cloudevents.CloudEvent;
 import io.cloudevents.core.CloudEventUtils;
 import io.cloudevents.core.data.PojoCloudEventData;
 import io.cloudevents.jackson.PojoCloudEventDataMapper;
-import io.cloudevents.rw.CloudEventRWException;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -51,9 +50,9 @@ public class CloudEventMapper {
             mappedCloudEvent =
                     CloudEventUtils.mapData(cloudEvent, PojoCloudEventDataMapper.from(objectMapper, targetEventClass));
 
-        } catch (final CloudEventRWException cloudEventRwException) {
+        } catch (final RuntimeException runtimeException) {
             log.error("Unable to map cloud event to target event class type : {} with cause : {}", targetEventClass,
-                    cloudEventRwException.getMessage());
+                    runtimeException.getMessage());
         }
 
         return mappedCloudEvent == null ? null : mappedCloudEvent.getValue();
index a5892af..81467db 100644 (file)
@@ -44,6 +44,14 @@ public interface CmHandleQueries {
      */
     Collection<String> queryCmHandlePublicProperties(Map<String, String> publicPropertyQueryPairs);
 
+    /**
+     * Query CmHandles based on Trust Level.
+     *
+     * @param trustLevelPropertyQueryPairs trust level properties for query
+     * @return CmHandles which have desired trust level
+     */
+    Collection<String> queryCmHandlesByTrustLevel(Map<String, String> trustLevelPropertyQueryPairs);
+
     /**
      * Method which returns cm handles by the cm handles state.
      *
index c4e3fd0..e5cf8ed 100644 (file)
@@ -35,6 +35,8 @@ import java.util.Map;
 import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import org.onap.cps.ncmp.api.impl.inventory.enums.PropertyType;
+import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevel;
+import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevelFilter;
 import org.onap.cps.spi.CpsDataPersistenceService;
 import org.onap.cps.spi.FetchDescendantsOption;
 import org.onap.cps.spi.model.DataNode;
@@ -45,9 +47,9 @@ import org.springframework.stereotype.Component;
 public class CmHandleQueriesImpl implements CmHandleQueries {
 
     private static final String DESCENDANT_PATH = "//";
-
-    private final CpsDataPersistenceService cpsDataPersistenceService;
     private static final String ANCESTOR_CM_HANDLES = "/ancestor::cm-handles";
+    private final CpsDataPersistenceService cpsDataPersistenceService;
+    private final Map<String, TrustLevel> trustLevelPerCmHandle;
 
     @Override
     public Collection<String> queryCmHandleAdditionalProperties(final Map<String, String> privatePropertyQueryPairs) {
@@ -59,6 +61,15 @@ public class CmHandleQueriesImpl implements CmHandleQueries {
         return queryCmHandleAnyProperties(publicPropertyQueryPairs, PropertyType.PUBLIC);
     }
 
+    @Override
+    public Collection<String> queryCmHandlesByTrustLevel(final Map<String, String> trustLevelPropertyQueryPairs) {
+        final String trustLevelProperty = trustLevelPropertyQueryPairs.values().iterator().next();
+        final TrustLevel targetTrustLevel = TrustLevel.valueOf(trustLevelProperty);
+
+        final TrustLevelFilter trustLevelFilter = new TrustLevelFilter(targetTrustLevel, trustLevelPerCmHandle);
+        return trustLevelFilter.getAllCmHandleIdsByTargetTrustLevel();
+    }
+
     @Override
     public List<DataNode> queryCmHandlesByState(final CmHandleState cmHandleState) {
         return queryCmHandleAncestorsByCpsPath("//state[@cm-handle-state=\"" + cmHandleState + "\"]",
index 458c1b8..b6d74d9 100644 (file)
 
 package org.onap.cps.ncmp.api.impl.trustlevel;
 
-import static org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper.toTargetEvent;
-
-import com.hazelcast.collection.ISet;
 import io.cloudevents.CloudEvent;
 import io.cloudevents.kafka.impl.KafkaHeaders;
+import java.util.Map;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper;
+import org.onap.cps.ncmp.events.trustlevel.DeviceTrustLevel;
 import org.springframework.kafka.annotation.KafkaListener;
 import org.springframework.stereotype.Component;
 
@@ -36,7 +36,8 @@ import org.springframework.stereotype.Component;
 @RequiredArgsConstructor
 public class DeviceHeartbeatConsumer {
 
-    private final ISet<String> untrustworthyCmHandlesSet;
+    private static final String CLOUD_EVENT_ID_HEADER_NAME = "ce_id";
+    private final Map<String, TrustLevel> trustLevelPerCmHandle;
 
     /**
      * Listening the device heartbeats.
@@ -47,23 +48,16 @@ public class DeviceHeartbeatConsumer {
             containerFactory = "cloudEventConcurrentKafkaListenerContainerFactory")
     public void heartbeatListener(final ConsumerRecord<String, CloudEvent> deviceHeartbeatConsumerRecord) {
 
-        final String cmHandleId = KafkaHeaders.getParsedKafkaHeader(deviceHeartbeatConsumerRecord.headers(), "ce_id");
+        final String cmHandleId = KafkaHeaders.getParsedKafkaHeader(deviceHeartbeatConsumerRecord.headers(),
+                CLOUD_EVENT_ID_HEADER_NAME);
 
         final DeviceTrustLevel deviceTrustLevel =
-                toTargetEvent(deviceHeartbeatConsumerRecord.value(), DeviceTrustLevel.class);
-
-        if (deviceTrustLevel == null || deviceTrustLevel.getTrustLevel() == null) {
-            log.warn("No or Invalid trust level defined");
-            return;
-        }
+                CloudEventMapper.toTargetEvent(deviceHeartbeatConsumerRecord.value(), DeviceTrustLevel.class);
 
-        if (deviceTrustLevel.getTrustLevel().equals(TrustLevel.NONE)) {
-            untrustworthyCmHandlesSet.add(cmHandleId);
-            log.debug("Added cmHandleId to untrustworthy set : {}", cmHandleId);
-        } else if (deviceTrustLevel.getTrustLevel().equals(TrustLevel.COMPLETE) && untrustworthyCmHandlesSet.contains(
-                cmHandleId)) {
-            untrustworthyCmHandlesSet.remove(cmHandleId);
-            log.debug("Removed cmHandleId from untrustworthy set : {}", cmHandleId);
+        if (cmHandleId != null && deviceTrustLevel != null) {
+            final String trustLevel = deviceTrustLevel.getData().getTrustLevel();
+            trustLevelPerCmHandle.put(cmHandleId, TrustLevel.valueOf(trustLevel));
+            log.debug("Added cmHandleId to trustLevelPerCmHandle map as {}:{}", cmHandleId, trustLevel);
         }
     }
 
index f4254bb..8d1f8e9 100644 (file)
 
 package org.onap.cps.ncmp.api.impl.trustlevel;
 
+import lombok.Getter;
+
+@Getter
 public enum TrustLevel {
-    NONE, COMPLETE;
+    NONE(0), COMPLETE(99);
+
+    private final int value;
+
+    TrustLevel(final int value) {
+        this.value = value;
+    }
+
 }
\ No newline at end of file
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/trustlevel/TrustLevelFilter.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/trustlevel/TrustLevelFilter.java
new file mode 100644 (file)
index 0000000..3b704ae
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2023 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.trustlevel;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import lombok.EqualsAndHashCode;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+public class TrustLevelFilter implements Comparable<TrustLevel> {
+
+    @EqualsAndHashCode.Include
+    private final TrustLevel targetTrustLevel;
+    private final Map<String, TrustLevel> trustLevelPerCmHandle;
+
+    @Override
+    public int compareTo(@NonNull final TrustLevel other) {
+        return Integer.compare(this.targetTrustLevel.getValue(), other.getValue());
+    }
+
+    /**
+     * This method return cm handles that matches with given trust level.
+     *
+     * @return cm handle ids.
+     */
+    public Collection<String> getAllCmHandleIdsByTargetTrustLevel() {
+        final Collection<String> resultCmHandleIds = new HashSet<>();
+        trustLevelPerCmHandle.entrySet().forEach(cmHandleTrustLevelEntrySet -> {
+            if (compareTo(cmHandleTrustLevelEntrySet.getValue()) == 0) {
+                resultCmHandleIds.add(cmHandleTrustLevelEntrySet.getKey());
+            }
+        });
+        return resultCmHandleIds;
+    }
+
+}
index d3b95ea..39f8802 100644 (file)
@@ -20,7 +20,7 @@
 
 package org.onap.cps.ncmp.api.impl.trustlevel.dmiavailability;
 
-import com.hazelcast.map.IMap;
+import java.util.Map;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.ncmp.api.impl.client.DmiRestClient;
@@ -33,7 +33,7 @@ import org.springframework.stereotype.Service;
 @Service
 public class DMiPluginWatchDog {
 
-    private final IMap<String, TrustLevel> trustLevelPerDmiPlugin;
+    private final Map<String, TrustLevel> trustLevelPerDmiPlugin;
 
     private final DmiRestClient dmiRestClient;
 
index b1bb7f7..a597760 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2022 Nordix Foundation
+ *  Copyright (C) 2022-2023 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -29,7 +29,8 @@ import lombok.Getter;
 public enum CmHandleQueryConditions {
     HAS_ALL_PROPERTIES("hasAllProperties"),
     HAS_ALL_MODULES("hasAllModules"),
-    WITH_CPS_PATH("cmHandleWithCpsPath");
+    WITH_CPS_PATH("cmHandleWithCpsPath"),
+    WITH_TRUST_LEVEL("cmHandleWithTrustLevel");
 
     public static final Collection<String> ALL_CONDITION_NAMES = Arrays.stream(CmHandleQueryConditions.values())
         .map(CmHandleQueryConditions::getConditionName).collect(Collectors.toList());
index ce6d856..7c410cc 100644 (file)
@@ -39,7 +39,7 @@ import spock.lang.Specification
 class NetworkCmProxyCmHandleQueryServiceSpec extends Specification {
 
     def cmHandleQueries = Mock(CmHandleQueries)
-    def partiallyMockedCmHandleQueries = Spy(CmHandleQueriesImpl)
+    def partiallyMockedCmHandleQueries = Spy(CmHandleQueries)
     def mockInventoryPersistence = Mock(InventoryPersistence)
 
     def dmiRegistry = new DataNode(xpath: NCMP_DMI_REGISTRY_PARENT, childDataNodes: createDataNodeList(['PNFDemo1', 'PNFDemo2', 'PNFDemo3', 'PNFDemo4']))
@@ -106,6 +106,17 @@ class NetworkCmProxyCmHandleQueryServiceSpec extends Specification {
             'No anchors are returned' | []
     }
 
+    def 'Query cm handles with some trust level query parameters'() {
+        given: 'a trust level condition property'
+            def trustLevelQueryParameters = new CmHandleQueryServiceParameters()
+            def trustLevelConditionProperties = createConditionProperties('cmHandleWithTrustLevel', [['trustLevel': 'COMPLETE'] as Map])
+            trustLevelQueryParameters.setCmHandleQueryParameters([trustLevelConditionProperties])
+        when: 'the query is being executed'
+            objectUnderTest.queryCmHandleIds(trustLevelQueryParameters)
+        then: 'the query is being delegated to the cm handle query service with correct parameter'
+            1 * cmHandleQueries.queryCmHandlesByTrustLevel(['trustLevel': 'COMPLETE'] as Map)
+    }
+
     def 'Query cm handle details with module names when #scenario from query.'() {
         given: 'a modules condition property'
             def cmHandleQueryParameters = new CmHandleQueryServiceParameters()
index 0d9aa61..af65cfc 100644 (file)
@@ -76,7 +76,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
     def mockCpsCmHandlerQueryService = Mock(NetworkCmProxyCmHandleQueryService)
     def mockLcmEventsCmHandleStateHandler = Mock(LcmEventsCmHandleStateHandler)
     def stubModuleSyncStartedOnCmHandles = Stub(IMap<String, Object>)
-    def stubTrustLevelPerDmiPlugin = Stub(IMap<String, TrustLevel>)
+    def stubTrustLevelPerDmiPlugin = Stub(Map<String, TrustLevel>)
 
     def NO_TOPIC = null
     def NO_REQUEST_ID = null
index 6184a97..3eff96d 100644 (file)
@@ -22,7 +22,6 @@ package org.onap.cps.ncmp.api.impl.config.embeddedcache
 
 import com.hazelcast.config.Config
 import com.hazelcast.core.Hazelcast
-import com.hazelcast.map.IMap
 import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevel
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.boot.test.context.SpringBootTest
@@ -32,7 +31,10 @@ import spock.lang.Specification
 class TrustLevelCacheConfigSpec extends Specification {
 
     @Autowired
-    private IMap<String, TrustLevel> trustLevelPerDmiPlugin
+    private Map<String, TrustLevel> trustLevelPerDmiPlugin
+
+    @Autowired
+    private Map<String, TrustLevel> trustLevelPerCmHandle
 
     def 'Hazelcast cache for trust level per dmi plugin'() {
         expect: 'system is able to create an instance of the trust level per dmi plugin cache'
@@ -43,23 +45,29 @@ class TrustLevelCacheConfigSpec extends Specification {
             assert Hazelcast.allHazelcastInstances.name.contains('hazelcastInstanceTrustLevelPerDmiPluginMap')
     }
 
-    def 'Verify Trust Level Per Dmi Plugin Cache for basic hazelcast map operations'() {
-        when: 'the key inserted into Trust Level Per Dmi Plugin Cache'
-            trustLevelPerDmiPlugin.put('dmi1', TrustLevel.COMPLETE)
-            trustLevelPerDmiPlugin.put('dmi2', TrustLevel.NONE)
-        then: 'the value for each dmi can be retrieved'
-            assert trustLevelPerDmiPlugin.get('dmi1') == TrustLevel.COMPLETE
-            assert trustLevelPerDmiPlugin.get('dmi2') == TrustLevel.NONE
+    def 'Hazelcast cache for trust level per cm handle'() {
+        expect: 'system is able to create an instance of the trust level per cm handle cache'
+            assert null != trustLevelPerCmHandle
+        and: 'there is at least 1 instance'
+            assert Hazelcast.allHazelcastInstances.size() > 0
+        and: 'Hazelcast cache instance for trust level is present'
+            assert Hazelcast.allHazelcastInstances.name.contains('hazelcastInstanceTrustLevelPerCmHandleMap')
     }
 
-    def 'Verify configs for Distributed Caches'(){
-        given: 'the Trust Level Per Dmi Plugin Cache config'
-            def trustLevelDmiPerPluginCacheConfig =  Hazelcast.getHazelcastInstanceByName('hazelcastInstanceTrustLevelPerDmiPluginMap').config
-            def trustLevelDmiPerPluginCacheMapConfig =  trustLevelDmiPerPluginCacheConfig.mapConfigs.get('trustLevelPerDmiPluginCacheConfig')
-        expect: 'system created instance with correct config'
-            assert trustLevelDmiPerPluginCacheConfig.clusterName == 'cps-and-ncmp-test-caches'
-            assert trustLevelDmiPerPluginCacheMapConfig.backupCount == 3
-            assert trustLevelDmiPerPluginCacheMapConfig.asyncBackupCount == 3
+    def 'Trust level cache configurations: #scenario'() {
+        when: 'retrieving the cache config for trustLevel'
+            def cacheConfig = Hazelcast.getHazelcastInstanceByName(hazelcastInstanceName).config
+        then: 'the cache config has the right cluster'
+            assert cacheConfig.clusterName == 'cps-and-ncmp-test-caches'
+        when: 'retrieving the map config for trustLevel'
+            def mapConfig = cacheConfig.mapConfigs.get(hazelcastMapConfigName)
+        then: 'the map config has the correct backup counts'
+            assert mapConfig.backupCount == 3
+            assert mapConfig.asyncBackupCount == 3
+        where: 'the following caches are used'
+            scenario         | hazelcastInstanceName                        | hazelcastMapConfigName
+            'cmhandle map'   | 'hazelcastInstanceTrustLevelPerCmHandleMap'  | 'trustLevelPerCmHandleCacheConfig'
+            'dmi plugin map' | 'hazelcastInstanceTrustLevelPerDmiPluginMap' | 'trustLevelPerDmiPluginCacheConfig'
     }
 
     def 'Verify deployment network configs for Distributed Caches'() {
@@ -70,6 +78,14 @@ class TrustLevelCacheConfigSpec extends Specification {
             assert !trustLevelDmiPerPluginCacheConfig.join.kubernetesConfig.enabled
     }
 
+    def 'Verify deployment network configs for Cm Handle Distributed Caches'() {
+        given: 'the Trust Level Per Cm Handle Cache config'
+            def trustLevelPerCmHandlePluginCacheConfig = Hazelcast.getHazelcastInstanceByName('hazelcastInstanceTrustLevelPerCmHandleMap').config.networkConfig
+        expect: 'system created instance with correct config'
+            assert trustLevelPerCmHandlePluginCacheConfig.join.autoDetectionConfig.enabled
+            assert !trustLevelPerCmHandlePluginCacheConfig.join.kubernetesConfig.enabled
+    }
+
     def 'Verify network config'() {
         given: 'Synchronization config object and test configuration'
             def objectUnderTest = new TrustLevelCacheConfig()
index 380aea4..e6d3831 100644 (file)
@@ -34,17 +34,30 @@ class CloudEventMapperSpec extends Specification {
     @Autowired
     JsonObjectMapper jsonObjectMapper
 
-    def 'Cloud event to Target event type when it is #scenario'() {
-        expect: 'Events mapped correctly'
-            assert mappedCloudEvent == (CloudEventMapper.toTargetEvent(testCloudEvent(), targetClass) != null)
+    def 'Cloud event to target event type'() {
+        given: 'a cloud event with valid payload'
+            def cloudEvent = testCloudEvent(new CmSubscriptionNcmpInEvent())
+        when: 'the cloud event mapped to target event'
+            def result = CloudEventMapper.toTargetEvent((cloudEvent), CmSubscriptionNcmpInEvent.class)
+        then: 'the cloud event is mapped'
+            assert result instanceof CmSubscriptionNcmpInEvent
+    }
+
+    def 'Cloud event to target event type when it is #scenario'() {
+        given: 'a cloud event with invalid payload'
+            def cloudEvent = testCloudEvent(payload)
+        when: 'the cloud event mapped to target event'
+            def result = CloudEventMapper.toTargetEvent(cloudEvent, CmSubscriptionNcmpInEvent.class)
+        then: 'result is null'
+            assert result == null
         where: 'below are the scenarios'
-            scenario                | targetClass                     || mappedCloudEvent
-            'valid concrete type'   | CmSubscriptionNcmpInEvent.class || true
-            'invalid concrete type' | ArrayList.class                 || false
+            scenario               | payload
+            'invalid payload type' | ArrayList.class
+            'without payload'      | null
     }
 
-    def testCloudEvent() {
-        return CloudEventBuilder.v1().withData(jsonObjectMapper.asJsonBytes(new CmSubscriptionNcmpInEvent()))
+    def testCloudEvent(payload) {
+        return CloudEventBuilder.v1().withData(jsonObjectMapper.asJsonBytes(payload))
             .withId("cmhandle1")
             .withSource(URI.create('test-source'))
             .withDataSchema(URI.create('test'))
index 48de23d..80778b9 100644 (file)
 package org.onap.cps.ncmp.api.impl.trustlevel
 
 import com.fasterxml.jackson.databind.ObjectMapper
-import com.hazelcast.collection.ISet
+import com.hazelcast.map.IMap
 import io.cloudevents.CloudEvent
 import io.cloudevents.core.builder.CloudEventBuilder
 import org.apache.kafka.clients.consumer.ConsumerRecord
+import org.onap.cps.ncmp.events.trustlevel.DeviceTrustLevel
 import org.onap.cps.utils.JsonObjectMapper
+import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.boot.test.context.SpringBootTest
 import spock.lang.Specification
 
 @SpringBootTest(classes = [ObjectMapper, JsonObjectMapper])
 class DeviceHeartbeatConsumerSpec extends Specification {
 
-    def mockUntrustworthyCmHandlesSet = Mock(ISet<String>)
+    def mockTrustLevelPerCmHandle = Mock(Map<String, TrustLevel>)
+
+    def objectUnderTest = new DeviceHeartbeatConsumer(mockTrustLevelPerCmHandle)
     def objectMapper = new ObjectMapper()
 
-    def objectUnderTest = new DeviceHeartbeatConsumer(mockUntrustworthyCmHandlesSet)
+    @Autowired
+    JsonObjectMapper jsonObjectMapper
+
+    def static trustLevelString = '{"data":{"trustLevel": "COMPLETE"}}'
 
-    def 'Operations to be done in an empty untrustworthy set for #scenario'() {
-        given: 'an event with trustlevel as #trustLevel'
-            def incomingEvent = testCloudEvent(trustLevel)
-        and: 'transformed as a kafka record'
-            def consumerRecord = new ConsumerRecord<String, CloudEvent>('test-device-heartbeat', 0, 0, 'cmhandle1', incomingEvent)
+    def 'Consume a trustlevel event'() {
+        given: 'an event from dmi with trust level complete'
+            def payload = jsonObjectMapper.convertJsonString(trustLevelString, DeviceTrustLevel.class)
+            def eventFromDmi = createTrustLevelEvent(payload)
+        and: 'transformed to a consumer record with a cloud event id (ce_id)'
+            def consumerRecord = new ConsumerRecord<String, CloudEvent>('test-device-heartbeat', 0, 0, 'sample-message-key', eventFromDmi)
             consumerRecord.headers().add('ce_id', objectMapper.writeValueAsBytes('cmhandle1'))
         when: 'the event is consumed'
             objectUnderTest.heartbeatListener(consumerRecord)
-        then: 'untrustworthy cmhandles are stored'
-            untrustworthyCmHandlesSetInvocationForAdd * mockUntrustworthyCmHandlesSet.add(_)
-        and: 'trustworthy cmHandles will be removed from untrustworthy set'
-            untrustworthyCmHandlesSetInvocationForContains * mockUntrustworthyCmHandlesSet.contains(_)
-
-        where: 'below scenarios are applicable'
-            scenario         | trustLevel          || untrustworthyCmHandlesSetInvocationForAdd | untrustworthyCmHandlesSetInvocationForContains
-            'None trust'     | TrustLevel.NONE     || 1                                         | 0
-            'Complete trust' | TrustLevel.COMPLETE || 0                                         | 1
+        then: 'cm handles are stored with correct trust level'
+            1 * mockTrustLevelPerCmHandle.put('"cmhandle1"', TrustLevel.COMPLETE)
     }
 
-    def 'Invalid trust'() {
-        when: 'we provide an invalid trust in the event'
-            def consumerRecord = new ConsumerRecord<String, CloudEvent>('test-device-heartbeat', 0, 0, 'cmhandle1', testCloudEvent(null))
-            consumerRecord.headers().add('ce_id', objectMapper.writeValueAsBytes('cmhandle1'))
+    def 'Consume trustlevel event without cloud event id'() {
+        given: 'an event from dmi'
+            def payload = jsonObjectMapper.convertJsonString(trustLevelString, DeviceTrustLevel.class)
+            def eventFromDmi = createTrustLevelEvent(payload)
+        and: 'transformed to a consumer record WITHOUT Cloud event ID (ce_id)'
+            def consumerRecord = new ConsumerRecord<String, CloudEvent>('test-device-heartbeat', 0, 0, 'sample-message-key', eventFromDmi)
+        when: 'the event is consumed'
             objectUnderTest.heartbeatListener(consumerRecord)
-        then: 'no interaction with the untrustworthy cmhandles set'
-            0 * mockUntrustworthyCmHandlesSet.add(_)
-            0 * mockUntrustworthyCmHandlesSet.contains(_)
-            0 * mockUntrustworthyCmHandlesSet.remove(_)
-        and: 'control flow returns without any exception'
-            noExceptionThrown()
-
+        then: 'no cm handle has been stored in the map'
+            0 * mockTrustLevelPerCmHandle.put(*_)
     }
 
-    def 'Remove trustworthy cmhandles from untrustworthy cmhandles set'() {
-        given: 'an event with COMPLETE trustlevel'
-            def incomingEvent = testCloudEvent(TrustLevel.COMPLETE)
-        and: 'transformed as a kafka record'
-            def consumerRecord = new ConsumerRecord<String, CloudEvent>('test-device-heartbeat', 0, 0, 'cmhandle1', incomingEvent)
-            consumerRecord.headers().add('ce_id', objectMapper.writeValueAsBytes('cmhandle1'))
-        and: 'untrustworthy cmhandles set contains cmhandle1'
-            1 * mockUntrustworthyCmHandlesSet.contains(_) >> true
+    def 'Consume a trust level event without payload'() {
+        given: 'a consumer record with ce_id header but without payload'
+            def consumerRecord = new ConsumerRecord<String, CloudEvent>('test-device-heartbeat', 0, 0, 'cmhandle1', createTrustLevelEvent(null))
+            consumerRecord.headers().add('some_other_header_value', objectMapper.writeValueAsBytes('cmhandle1'))
         when: 'the event is consumed'
             objectUnderTest.heartbeatListener(consumerRecord)
-        then: 'cmhandle removed from untrustworthy cmhandles set'
-            1 * mockUntrustworthyCmHandlesSet.remove(_) >> {
-                args ->
-                    {
-                        args[0].equals('cmhandle1')
-                    }
-            }
-
+        then: 'no cm handle has been stored in the map'
+            0 * mockTrustLevelPerCmHandle.put(*_)
     }
 
-    def testCloudEvent(trustLevel) {
-        return CloudEventBuilder.v1().withData(objectMapper.writeValueAsBytes(new DeviceTrustLevel(trustLevel)))
+    def createTrustLevelEvent(eventPayload) {
+        return CloudEventBuilder.v1().withData(objectMapper.writeValueAsBytes(eventPayload))
             .withId("cmhandle1")
             .withSource(URI.create('DMI'))
             .withDataSchema(URI.create('test'))
-            .withType('org.onap.cm.events.trustlevel-notification')
+            .withType('org.onap.cps.ncmp.events.trustlevel.DeviceTrustLevel')
             .build()
     }
 
  *  ============LICENSE_END=========================================================
  */
 
-package org.onap.cps.ncmp.api.impl.trustlevel;
+package org.onap.cps.ncmp.api.impl.trustlevel
 
-import java.io.Serializable;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
 
-@AllArgsConstructor
-@Data
-@NoArgsConstructor
-class DeviceTrustLevel implements Serializable {
+import spock.lang.Specification
 
-    private static final long serialVersionUID = -1705715024067165212L;
+class TrustLevelFilterSpec extends Specification {
 
-    private TrustLevel trustLevel;
+    def targetTrustLevel = TrustLevel.COMPLETE
 
+    def trustLevelPerCmHandle = [ 'my completed cm handle': TrustLevel.COMPLETE, 'my untrusted cm handle': TrustLevel.NONE ]
+
+    def objectUnderTest = new TrustLevelFilter(targetTrustLevel, trustLevelPerCmHandle)
+
+    def 'Obtain cm handle ids by a given trust level value'() {
+        when: 'cm handles are retrieved'
+            def result = objectUnderTest.getAllCmHandleIdsByTargetTrustLevel()
+        then: 'the result only contains the completed cm handle'
+            assert result.size() == 1
+            assert result[0] == 'my completed cm handle'
+    }
 }
index af546b7..b6259bd 100644 (file)
@@ -20,7 +20,6 @@
 
 package org.onap.cps.ncmp.api.impl.trustlevel.dmiavailability
 
-import com.hazelcast.map.IMap
 import org.onap.cps.ncmp.api.impl.client.DmiRestClient
 import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevel
 import spock.lang.Specification
@@ -28,7 +27,7 @@ import spock.lang.Specification
 class DMiPluginWatchDogSpec extends Specification {
 
 
-    def mockTrustLevelPerDmiPlugin = Mock(IMap<String, TrustLevel>)
+    def mockTrustLevelPerDmiPlugin = Mock(Map<String, TrustLevel>)
     def mockDmiRestClient = Mock(DmiRestClient)
     def objectUnderTest = new DMiPluginWatchDog(mockTrustLevelPerDmiPlugin, mockDmiRestClient)
 
index f0e2d9f..100705f 100644 (file)
@@ -26,10 +26,11 @@ class CmHandleQueryConditionsSpec extends Specification {
 
     def 'CmHandle query condition names.'() {
         expect: '3 conditions with the correct names'
-            assert CmHandleQueryConditions.ALL_CONDITION_NAMES.size() == 3
+            assert CmHandleQueryConditions.ALL_CONDITION_NAMES.size() == 4
             assert CmHandleQueryConditions.ALL_CONDITION_NAMES.containsAll('hasAllProperties',
                                                                            'hasAllModules',
-                                                                           'cmHandleWithCpsPath')
+                                                                           'cmHandleWithCpsPath',
+                                                                            'cmHandleWithTrustLevel')
     }
 
 }
index e7c337c..a3a5efc 100644 (file)
 
 package org.onap.cps.ncmp.api.inventory
 
+import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevel
+
 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT
 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
 
+import com.hazelcast.map.IMap
 import org.onap.cps.ncmp.api.impl.inventory.CmHandleQueriesImpl
 import org.onap.cps.ncmp.api.impl.inventory.CmHandleState
 import org.onap.cps.ncmp.api.impl.inventory.DataStoreSyncState
@@ -37,8 +40,9 @@ import spock.lang.Specification
 
 class CmHandleQueriesImplSpec extends Specification {
     def cpsDataPersistenceService = Mock(CpsDataPersistenceService)
+    def trustLevelPerCmHandle = [ 'my completed cm handle': TrustLevel.COMPLETE, 'my untrusted cm handle': TrustLevel.NONE ]
 
-    def objectUnderTest = new CmHandleQueriesImpl(cpsDataPersistenceService)
+    def objectUnderTest = new CmHandleQueriesImpl(cpsDataPersistenceService, trustLevelPerCmHandle)
 
     @Shared
     def static sampleDataNodes = [new DataNode()]
@@ -60,11 +64,21 @@ class CmHandleQueriesImplSpec extends Specification {
             result.containsAll(expectedCmHandleIds)
             result.size() == expectedCmHandleIds.size()
         where: 'the following data is used'
-            scenario                         | publicPropertyPairs                                                                           || expectedCmHandleIds
-            'single property matches'        | ['Contact' : 'newemailforstore@bookstore.com']                                                || ['PNFDemo', 'PNFDemo2', 'PNFDemo4']
-            'public property does not match' | ['wont_match' : 'wont_match']                                                                 || []
-            '2 properties, only one match'   | ['Contact' : 'newemailforstore@bookstore.com', 'Contact2': 'newemailforstore2@bookstore.com'] || ['PNFDemo4']
-            '2 properties, no matches'       | ['Contact' : 'newemailforstore@bookstore.com', 'Contact2': '']                                || []
+            scenario                         | publicPropertyPairs                                                                      || expectedCmHandleIds
+            'single property matches'        | [Contact: 'newemailforstore@bookstore.com']                                              || ['PNFDemo', 'PNFDemo2', 'PNFDemo4']
+            'public property does not match' | [wont_match: 'wont_match']                                                               || []
+            '2 properties, only one match'   | [Contact: 'newemailforstore@bookstore.com', Contact2: 'newemailforstore2@bookstore.com'] || ['PNFDemo4']
+            '2 properties, no matches'       | [Contact: 'newemailforstore@bookstore.com', Contact2: '']                                || []
+    }
+
+    def 'Query cm handles on trust level'() {
+        given: 'query properties for trustlevel COMPLETE'
+            def trustLevelPropertyQueryPairs = ['trustLevel' : TrustLevel.COMPLETE.toString()]
+        when: 'the query is executed'
+            def result = objectUnderTest.queryCmHandlesByTrustLevel(trustLevelPropertyQueryPairs)
+        then: 'the result only contains the completed cm handle'
+            assert result.size() == 1
+            assert result[0] == 'my completed cm handle'
     }
 
     def 'Query CmHandles using empty public properties query pair.'() {
index 6611789..eb203d8 100644 (file)
@@ -247,7 +247,7 @@ leaf-conditions
   - Using comparative operators with string values will lead to an error at runtime. This error can't be validated earlier as the datatype is unknown until the execution phase.
   - The key should be supplied with correct data type for it to be queried from DB. In the last example above the attribute code is of type
     Integer so the cps query will not work if the value is passed as string.
-    eg: ``//categories[@code="1"]`` or ``//categories[@code='1']`` will not work because the key attribute code is treated a string.
+    e.g.: ``//categories[@code="1"]`` or ``//categories[@code='1']`` will not work because the key attribute code is treated a string.
 
 **Notes**
   - For performance reasons it does not make sense to query using key leaf as attribute. If the key value is known it is better to execute a get request with the complete xpath.
@@ -260,7 +260,7 @@ The text()-condition  can be added to any CPS path query.
 **Syntax**: ``<cps-path> ( '/' <leaf-name> '[text()=' <string-value> ']' )?``
   - ``cps-path``: Any CPS path query.
   - ``leaf-name``: The name of the leaf or leaf-list which value needs to be compared.
-  - ``string-value``: The required value of the leaf or leaf-list element as a string wrapped in quotation marks (U+0022) or apostrophes (U+0027). This wil still match integer values.
+  - ``string-value``: The required value of the leaf or leaf-list element as a string wrapped in quotation marks (U+0022) or apostrophes (U+0027). This will still match integer values.
 
 **Examples**
   - ``//book/label[text()="classic"]``
diff --git a/docs/cps-stubs.rst b/docs/cps-stubs.rst
new file mode 100644 (file)
index 0000000..00577eb
--- /dev/null
@@ -0,0 +1,120 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. http://creativecommons.org/licenses/by/4.0
+.. Copyright (C) 2023 Nordix Foundation
+
+.. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING
+
+.. _cpsStubs:
+
+
+CPS Stubs
+#########
+
+.. toctree::
+   :maxdepth: 1
+
+NCMP Stubs
+==========
+
+The CPS NCMP stub module provides the capability to create dynamic and customizable stubs, offering control over the responses generated for each endpoint. This capability ensures that client interactions adhere to a specified NCMP interface, enabling comprehensive testing and validation of your applications.
+
+The NCMP stub RestController is an extended implementation of the actual NCMP interface. It can be deployed as part of the application JAR or within a SpringBootTest JUnit environment, allowing you to define dynamic responses for each endpoint and  allowing testing against real stub interfaces.
+
+Prerequisites
+=============
+
+Ensure you meet the following prerequisites:
+
+1. **Required Java Installation:**
+   
+   Ensure that you have the required Java installed on your system. 
+
+2. **Access to Gerrit and Maven Installation (for building CPS project locally):**
+
+   - Ensure you have access to the ONAP Gerrit repository.
+   
+   - If you plan to build the CPS project locally, make sure you have Maven installed. 
+
+Method 1: Running Stubs as an Application
+=========================================
+
+Follow these steps to run the CPS-NCMP stub application:
+
+1. **Download Application Jar:**
+
+   You can obtain the CPS-NCMP stub application jar in one of the following ways:
+
+   - **Option 1: Download from Nexus Repository:**
+
+     Download the application jar from the Nexus repository at `https://nexus.onap.org/content/repositories/releases/org/onap/cps/cps-ncmp-rest-stub-app/`_.
+
+   - **Option 2: Build Locally:**
+
+     To build the CPS project locally, navigate to the project's root directory. Once there, you can build the project using :code:`mvn clean install`, and the application CPS-NCMP stub application jar can be found in the following location:
+
+     ::
+
+       cps/cps-ncmp-rest-stub/cps-ncmp-rest-stub-app/target/
+
+2. **Run the Application:**
+
+   After obtaining the application jar, use the following command to run it:
+
+   .. code-block:: bash
+
+      java -jar ./cps-ncmp-rest-stub-app-<VERSION>.jar
+
+   Replace ``<VERSION>`` with the actual version number of the application jar.
+
+This will start the CPS-NCMP stub application, and you can interact with it as needed.
+
+.. _`https://nexus.onap.org/content/repositories/releases/org/onap/cps/cps-ncmp-rest-stub-app/`: https://nexus.onap.org/content/repositories/releases/org/onap/cps/cps-ncmp-rest-stub-app/
+
+Method 2: Using Stubs in Unit Tests
+===================================
+1. **Add Dependency to pom.xml:**
+
+   To include the required module in your project, add the following dependency to your `pom.xml` file:
+
+   .. code-block:: xml
+
+      <dependency>
+        <groupId>org.onap.cps</groupId>
+        <artifactId>cps-ncmp-rest-stub-service</artifactId>
+        <version>VERSION</version>
+     </dependency>
+
+   Replace ``VERSION`` with the actual version number.
+
+2. **Using Custom Response Objects:**
+
+   If you prefer to use custom response objects instead of the built-in ones, follow these steps:
+
+   Modify the `application.yaml` file located in your project's test resources directory (`src/test/resources`).
+
+   Add the following property to the `application.yaml` file, specifying the directory that contains your custom response objects:
+   
+   .. code-block:: yaml
+
+      stub:
+          path: "/my_custom_stubs/"
+
+   **Note:** Custom response objects can be placed in the `src/test/resources` directory of your project under the directory defined in above property. Refer to the `examples <https://github.com/onap/cps/tree/master/cps-ncmp-rest-stub/cps-ncmp-rest-stub-service/src/main/resources/stubs>`_ included in the CPS source repository for reference.
+
+3. **Simple Test Code:**
+
+   You may refer to the sample test code 'SampleCpsNcmpClientSpec.groovy' in the local CPS project under the following directory:
+
+   ::
+
+     /cps/cps-ncmp-rest-stub/cps-ncmp-rest-stub-service/src/test/groovy/org/onap/cps/ncmp/rest/stub/
+
+   Alternatively, you can refer to the `example <https://github.com/onap/cps/tree/master/cps-ncmp-rest-stub/cps-ncmp-rest-stub-service/src/test/groovy/org/onap/cps/ncmp/rest/stub>`_ included in the CPS source repository.
+
+**Custom Responses for Supported Endpoints**
+
+  Only the following endpoints are supported for the first draft. To use your custom response objects for these endpoints, create the corresponding JSON files:
+
+  - For RequestMethod.GET /v1/ch/{cm-handle}/data/ds/{datastore-name}, create "passthrough-operational-example.json".
+
+  - For RequestMethod.POST /v1/ch/searches, create "cmHandlesSearch.json".
index 0642e6a..0fee559 100644 (file)
@@ -197,8 +197,8 @@ Any spring supported property can be configured by providing in ``config.additio
 | logging.level                         | Logging level set in cps-core                                                                           | info                          |
 |                                       |                                                                                                         |                               |
 +---------------------------------------+---------------------------------------------------------------------------------------------------------+-------------------------------+
-| config.useStrimziKafka                | If targeting a custom kafka cluster, ie useStrimziKafka: false, the config.eventPublisher.spring.kafka  | true                          |
-|                                       | values below must be set.                                                                               |                               |
+| config.useStrimziKafka                | If targeting a custom kafka cluster, i.e. useStrimziKafka: false, the                                   | true                          |
+|                                       | config.eventPublisher.spring.kafka values below must be set.                                            |                               |
 +---------------------------------------+---------------------------------------------------------------------------------------------------------+-------------------------------+
 | config.eventPublisher.                | Kafka hostname and port                                                                                 | ``<kafka-bootstrap>:9092``    |
 | spring.kafka.bootstrap-servers        |                                                                                                         |                               |
index 5ad86a3..80eb5f1 100755 (executable)
@@ -1,6 +1,6 @@
 .. This work is licensed under a Creative Commons Attribution 4.0 International License.
 .. http://creativecommons.org/licenses/by/4.0
-.. Copyright (C) 2021-2022 Nordix Foundation
+.. Copyright (C) 2021-2023 Nordix Foundation
 
 .. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING
 .. _design:
@@ -93,3 +93,24 @@ NCMP uses common responses codes in REST responses and events. Also the DMI plug
    :maxdepth: 1
 
    cps-ncmp-message-status-codes.rst
+
+Contract Testing (stubs)
+========================
+
+The CPS team is committed to supporting consumers of our APIs through contract testing.
+Obviously we test our own contracts on a continuous basis as part of the build and delivery process.
+CPS uses a contract-first approach. That means we write our OpenAPi contracts first and then generate the interface code from that.
+This means our interface implementation simply cannot deviate from the OpenApi contracts we deliver.
+
+Another advantage is that we can also generate 'stubs'. Stubs are a basic implementation of the same interface for testing purposes.
+These stubs can be used by clients for unit testing but also for more higher level integration-like testing where the real service is replaced by a stub.
+This can be useful for faster feedback loops where deployment of a full stack is difficult and not strictly needed for the purpose of the tests.
+
+Stubs for contract testing typically always return the same response which is sufficient for the strict definition of a contract test.
+However it is often useful to allow more variation in the responses so different clients or the same client can test different scenarios without having to mock the service.
+CPS has implemented what we call 'extended stubs' that allow clients to provide alternate responses.implementation
+
+The available stubs and how to use them are described in :doc:'cps-stubs'.
+
+
+
index c1427ad..573bd54 100755 (executable)
@@ -25,6 +25,7 @@ CPS
    cps-events.rst
    deployment.rst
    release-notes.rst
+   cps-stubs.rst
 
 DMI-Plugin
 ==========
index d891162..e6b81fc 100755 (executable)
@@ -515,7 +515,7 @@ Bug Fixes
    - `CPS-1289 <https://jira.onap.org/browse/CPS-1289>`_  Getting wrong error code for create node api
    - `CPS-1326 <https://jira.onap.org/browse/CPS-1326>`_  Creation of DataNodeBuilder with module name prefix is very slow
    - `CPS-1344 <https://jira.onap.org/browse/CPS-1344>`_  Top level container (prefix) is not always the first module
-   - `CPS-1350 <https://jira.onap.org/browse/CPS-1350>`_  Add Basic Auth to CPS/NCMP OpenAPI Definitions.
+   - `CPS-1350 <https://jira.onap.org/browse/CPS-1350>`_  Add Basic Authentication to CPS/NCMP OpenAPI Definitions.
    - `CPS-1352 <https://jira.onap.org/browse/CPS-1352>`_  Handle YangChoiceNode in right format.
    - `CPS-1409 <https://jira.onap.org/browse/CPS-1409>`_  Fix Delete uses case with '/' in path.
    - `CPS-1433 <https://jira.onap.org/browse/CPS-1433>`_  Fix to allow posting data with '/' key fields.
@@ -701,7 +701,7 @@ Bug Fixes
    - `CPS-957 <https://jira.onap.org/browse/CPS-957>`_  NCMP: fix getResourceDataForPassthroughOperational endpoint
    - `CPS-1020 <https://jira.onap.org/browse/CPS-1020>`_  DuplicatedYangResourceException error at parallel cmHandle registration
    - `CPS-1056 <https://jira.onap.org/browse/CPS-1056>`_  Wrong error response format in case of Dmi plugin error
-   - `CPS-1067 <https://jira.onap.org/browse/CPS-1067>`_  NCMP returns 500 error on searches endpoint when No DMi Handles registered
+   - `CPS-1067 <https://jira.onap.org/browse/CPS-1067>`_  NCMP returns 500 error on searches endpoint when No DMI Handles registered
    - `CPS-1085 <https://jira.onap.org/browse/CPS-1085>`_  Performance degradation on ncmp/v1/ch/searches endpoint
    - `CPS-1088 <https://jira.onap.org/browse/CPS-1088>`_  Kafka consumer can not be turned off
    - `CPS-1097 <https://jira.onap.org/browse/CPS-1097>`_  Unable to change state from LOCKED to ADVISED
@@ -813,7 +813,7 @@ Bug Fixes
    - `CPS-886 <https://jira.onap.org/browse/CPS-886>`_ Fragment handling decreasing performance for large number of cmHandles
    - `CPS-887 <https://jira.onap.org/browse/CPS-887>`_ Increase performance of cmHandle registration for large number of schema sets in DB
    - `CPS-892 <https://jira.onap.org/browse/CPS-892>`_ Fixed the response code during CM-Handle Registration from 201 CREATED to 204 NO_CONTENT
-   - `CPS-893 <https://jira.onap.org/browse/CPS-893>`_ NCMP Java API depends on NCM-Rest-API (cyclic) through json properties on Java API
+   - `CPS-893 <https://jira.onap.org/browse/CPS-893>`_ NCMP Java API depends on NCMP-Rest-API (cyclic) through json properties on Java API
 
 Known Limitations, Issues and Workarounds
 -----------------------------------------
@@ -1268,7 +1268,7 @@ Security Notes
 
 *Known Security Issues*
 
-    * Weak Crytography using md5
+    * Weak Cryptography using md5
     * Risk seen in Zip file expansion
 
 *Known Vulnerabilities in Used Modules*
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
new file mode 100644 (file)
index 0000000..3bfa9a0
--- /dev/null
@@ -0,0 +1,67 @@
+api
+async
+authorization
+boolean
+behavior
+camelCasing
+cmHandle
+cmHandles
+config
+color
+cps
+cpsPath
+cpsPaths
+csit
+customizations
+dataschema
+dataspace
+dataspaces
+datasource
+datastore
+datastores
+deliverables
+dmi
+dmiPlugin
+dmiPlugins
+rnv
+hazelcast
+hostname
+jira
+json
+kafka
+kibana
+kubernetes
+kohn
+lifecycle
+liquibase
+logback
+modeled
+modeling
+normalized
+ncmp
+onap
+openapi
+optimized
+passthrough
+performant
+postgres
+queryable
+repo
+runtime
+schemas
+sql
+ssl
+standardized
+synchronize
+synchronization
+undeploying
+utilizes
+xml
+xNF
+xNFs
+xpath
+xpaths
+xPath
+XPath
+XPaths
+yaml
\ No newline at end of file
index 8684276..7020752 100644 (file)
@@ -21,23 +21,23 @@ minversion = 1.6
 envlist = docs,docs-linkcheck,docs-spellcheck
 skipsdist = true
 [testenv:docs]
-basepython = python3.8
+basepython = python3.10
 deps =
     -r{toxinidir}/requirements-docs.txt
     -chttps://raw.githubusercontent.com/openstack/requirements/stable/yoga/upper-constraints.txt
     -chttps://git.onap.org/doc/plain/etc/upper-constraints.onap.txt?h=master
 commands =
-    sphinx-build -W -q -b html -n -d {envtmpdir}/doctrees {toxinidir} {toxinidir}/_build/html
+    sphinx-build -q -b html -n -d {envtmpdir}/doctrees {toxinidir} {toxinidir}/_build/html
 [testenv:docs-linkcheck]
-basepython = python3.8
+basepython = python3.10
 deps =
     -r{toxinidir}/requirements-docs.txt
     -chttps://raw.githubusercontent.com/openstack/requirements/stable/yoga/upper-constraints.txt
     -chttps://git.onap.org/doc/plain/etc/upper-constraints.onap.txt?h=master
 commands =
-    sphinx-build -W -q -b linkcheck -d {envtmpdir}/doctrees {toxinidir} {toxinidir}/_build/linkcheck
+    sphinx-build -q -b linkcheck -d {envtmpdir}/doctrees {toxinidir} {toxinidir}/_build/linkcheck
 [testenv:docs-spellcheck]
-basepython = python3.8
+basepython = python3.10
 deps =
     -r{toxinidir}/requirements-docs.txt
     -chttps://raw.githubusercontent.com/openstack/requirements/stable/yoga/upper-constraints.txt