/*
* ============LICENSE_START=======================================================
* ONAP PAP
* ================================================================================
* Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved.
* Modifications Copyright (C) 2020-2021 Nordix Foundation.
* Modifications Copyright (C) 2021 Bell Canada. All rights reserved.
* ================================================================================
* 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.
* ============LICENSE_END=========================================================
*/
package org.onap.policy.pap.main.rest;
import com.google.gson.annotations.SerializedName;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.ws.rs.core.Response.Status;
import lombok.Getter;
import org.onap.policy.common.parameters.BeanValidationResult;
import org.onap.policy.common.parameters.BeanValidator;
import org.onap.policy.common.parameters.ValidationResult;
import org.onap.policy.common.parameters.ValidationStatus;
import org.onap.policy.common.parameters.annotations.NotNull;
import org.onap.policy.common.parameters.annotations.Pattern;
import org.onap.policy.common.parameters.annotations.Valid;
import org.onap.policy.common.utils.coder.CoderException;
import org.onap.policy.common.utils.coder.StandardCoder;
import org.onap.policy.common.utils.services.Registry;
import org.onap.policy.models.base.PfKey;
import org.onap.policy.models.base.PfModelException;
import org.onap.policy.models.base.PfModelRuntimeException;
import org.onap.policy.models.pap.concepts.PdpDeployPolicies;
import org.onap.policy.models.pdp.concepts.DeploymentGroup;
import org.onap.policy.models.pdp.concepts.DeploymentGroups;
import org.onap.policy.models.pdp.concepts.DeploymentSubGroup;
import org.onap.policy.models.pdp.concepts.Pdp;
import org.onap.policy.models.pdp.concepts.PdpGroup;
import org.onap.policy.models.pdp.concepts.PdpSubGroup;
import org.onap.policy.models.tosca.authorative.concepts.ToscaConceptIdentifier;
import org.onap.policy.models.tosca.authorative.concepts.ToscaConceptIdentifierOptVersion;
import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
/**
* Provider for PAP component to deploy PDP groups. The following items must be in the
* {@link Registry}:
*
* - PDP Modification Lock
* - PDP Modify Request Map
* - PAP DAO Factory
*
*/
@Service
public class PdpGroupDeployProvider extends ProviderBase {
private static final Logger logger = LoggerFactory.getLogger(PdpGroupDeployProvider.class);
private static final StandardCoder coder = new StandardCoder();
private static final String POLICY_RESULT_NAME = "policy";
/**
* Updates policies in specific PDP groups.
*
* @param groups PDP group deployments to be updated
* @param user user triggering deployment
* @throws PfModelException if an error occurred
*/
public void updateGroupPolicies(DeploymentGroups groups, String user) throws PfModelException {
ValidationResult result = groups.validatePapRest();
if (!result.isValid()) {
String msg = result.getResult().trim();
throw new PfModelException(Status.BAD_REQUEST, msg);
}
process(user, groups, this::updateGroups);
}
/**
* Updates policies in specific PDP groups. This is the method that does the actual work.
*
* @param data session data
* @param groups PDP group deployments
* @throws PfModelException if an error occurred
*/
private void updateGroups(SessionData data, DeploymentGroups groups) throws PfModelException {
var result = new BeanValidationResult("groups", groups);
for (DeploymentGroup group : groups.getGroups()) {
PdpGroup dbgroup = data.getGroup(group.getName());
if (dbgroup == null) {
result.addResult(group.getName(), group, ValidationStatus.INVALID, "unknown group");
} else {
result.addResult(updateGroup(data, dbgroup, group));
}
}
if (!result.isValid()) {
throw new PfModelException(Status.BAD_REQUEST, result.getResult().trim());
}
}
/**
* Updates an existing group.
*
* @param data session data
* @param dbgroup the group, as it appears within the DB
* @param group the group to be added
* @return the validation result
* @throws PfModelException if an error occurred
*/
private ValidationResult updateGroup(SessionData data, PdpGroup dbgroup, DeploymentGroup group)
throws PfModelException {
var result = new BeanValidationResult(group.getName(), group);
boolean updated = updateSubGroups(data, dbgroup, group, result);
if (result.isValid() && updated) {
data.update(dbgroup);
}
return result;
}
/**
* Adds or updates subgroups within the group.
*
* @param data session data
* @param dbgroup the group, as it appears within the DB
* @param group the group to be added
* @param result the validation result
* @return {@code true} if the DB group was modified, {@code false} otherwise
* @throws PfModelException if an error occurred
*/
private boolean updateSubGroups(SessionData data, PdpGroup dbgroup, DeploymentGroup group,
BeanValidationResult result) throws PfModelException {
// create a map of existing subgroups
Map type2sub = new HashMap<>();
dbgroup.getPdpSubgroups().forEach(subgrp -> type2sub.put(subgrp.getPdpType(), subgrp));
var updated = false;
for (DeploymentSubGroup subgrp : group.getDeploymentSubgroups()) {
PdpSubGroup dbsub = type2sub.get(subgrp.getPdpType());
var subResult = new BeanValidationResult(subgrp.getPdpType(), subgrp);
if (dbsub == null) {
subResult.addResult(subgrp.getPdpType(), subgrp, ValidationStatus.INVALID, "unknown subgroup");
} else {
updated = updateSubGroup(data, dbgroup, dbsub, subgrp, subResult) || updated;
}
result.addResult(subResult);
}
return updated;
}
/**
* Updates an existing subgroup.
*
* @param data session data
* @param dbgroup the group, from the DB, containing the subgroup
* @param dbsub the subgroup, from the DB
* @param subgrp the subgroup to be updated, updated to fully qualified versions upon
* return
* @param container container for additional validation results
* @return {@code true} if the subgroup content was changed, {@code false} if there
* were no changes
* @throws PfModelException if an error occurred
*/
private boolean updateSubGroup(SessionData data, PdpGroup dbgroup, PdpSubGroup dbsub, DeploymentSubGroup subgrp,
BeanValidationResult container) throws PfModelException {
// perform additional validations first
if (!validateSubGroup(data, dbsub, subgrp, container)) {
return false;
}
var updated = false;
switch (subgrp.getAction()) {
case POST:
updated = addPolicies(data, dbgroup.getName(), dbsub, subgrp);
break;
case DELETE:
updated = deletePolicies(data, dbgroup.getName(), dbsub, subgrp);
break;
case PATCH:
default:
updated = updatePolicies(data, dbgroup.getName(), dbsub, subgrp);
break;
}
if (updated) {
// publish any changes to the PDPs
makeUpdates(data, dbgroup, dbsub);
return true;
}
return false;
}
private boolean addPolicies(SessionData data, String pdpGroup, PdpSubGroup dbsub, DeploymentSubGroup subgrp)
throws PfModelException {
Set policies = new LinkedHashSet<>(dbsub.getPolicies());
policies.addAll(subgrp.getPolicies());
var subgrp2 = new DeploymentSubGroup(subgrp);
subgrp2.getPolicies().clear();
subgrp2.getPolicies().addAll(policies);
return updatePolicies(data, pdpGroup, dbsub, subgrp2);
}
private boolean deletePolicies(SessionData data, String pdpGroup, PdpSubGroup dbsub, DeploymentSubGroup subgrp)
throws PfModelException {
Set policies = new LinkedHashSet<>(dbsub.getPolicies());
policies.removeAll(subgrp.getPolicies());
var subgrp2 = new DeploymentSubGroup(subgrp);
subgrp2.getPolicies().clear();
subgrp2.getPolicies().addAll(policies);
return updatePolicies(data, pdpGroup, dbsub, subgrp2);
}
private boolean updatePolicies(SessionData data, String pdpGroup, PdpSubGroup dbsub, DeploymentSubGroup subgrp)
throws PfModelException {
Set undeployed = new HashSet<>(dbsub.getPolicies());
undeployed.removeAll(subgrp.getPolicies());
Set deployed = new HashSet<>(subgrp.getPolicies());
deployed.removeAll(dbsub.getPolicies());
if (deployed.isEmpty() && undeployed.isEmpty()) {
// lists are identical
return false;
}
Set pdps = dbsub.getPdpInstances().stream().map(Pdp::getInstanceId).collect(Collectors.toSet());
for (ToscaConceptIdentifier policyId : deployed) {
ToscaPolicy policyToBeDeployed = data.getPolicy(new ToscaConceptIdentifierOptVersion(policyId));
data.trackDeploy(policyToBeDeployed, pdps, pdpGroup, dbsub.getPdpType());
}
for (ToscaConceptIdentifier policyId : undeployed) {
data.trackUndeploy(policyId, pdps, pdpGroup, dbsub.getPdpType());
}
dbsub.setPolicies(new ArrayList<>(subgrp.getPolicies()));
return true;
}
/**
* Performs additional validations of a subgroup.
*
* @param data session data
* @param dbsub the subgroup, from the DB
* @param subgrp the subgroup to be validated, updated to fully qualified versions
* upon return
* @param container container for additional validation results
* @return {@code true} if the subgroup is valid, {@code false} otherwise
* @throws PfModelException if an error occurred
*/
private boolean validateSubGroup(SessionData data, PdpSubGroup dbsub, DeploymentSubGroup subgrp,
BeanValidationResult container) throws PfModelException {
var result = new BeanValidationResult(subgrp.getPdpType(), subgrp);
result.addResult(validatePolicies(data, dbsub, subgrp));
container.addResult(result);
return result.isValid();
}
/**
* Performs additional validations of the policies within a subgroup.
*
* @param data session data
* @param dbsub subgroup from the DB, or {@code null} if this is a new subgroup
* @param subgrp the subgroup whose policies are to be validated, updated to fully
* qualified versions upon return
* @return the validation result
* @throws PfModelException if an error occurred
*/
private ValidationResult validatePolicies(SessionData data, PdpSubGroup dbsub, DeploymentSubGroup subgrp)
throws PfModelException {
// build a map of the DB data, from policy name to (fully qualified) policy
// version
Map dbname2vers = new HashMap<>();
dbsub.getPolicies().forEach(ident -> dbname2vers.put(ident.getName(), ident.getVersion()));
var result = new BeanValidationResult(subgrp.getPdpType(), subgrp);
for (ToscaConceptIdentifier ident : subgrp.getPolicies()) {
// note: "ident" may not have a fully qualified version
String expectedVersion = dbname2vers.get(ident.getName());
if (expectedVersion != null) {
// policy exists in the DB list - compare the versions
validateVersion(expectedVersion, ident, result);
ident.setVersion(expectedVersion);
continue;
}
// policy doesn't appear in the DB's policy list - look it up
ToscaPolicy policy = data.getPolicy(new ToscaConceptIdentifierOptVersion(ident));
if (policy == null) {
result.addResult(POLICY_RESULT_NAME, ident, ValidationStatus.INVALID, "unknown policy");
} else if (!isPolicySupported(dbsub.getSupportedPolicyTypes(), policy.getTypeIdentifier())) {
result.addResult(POLICY_RESULT_NAME, ident, ValidationStatus.INVALID,
"not a supported policy for the subgroup");
} else {
// replace version with the fully qualified version from the policy
ident.setVersion(policy.getVersion());
}
}
return result;
}
/**
* Determines if the new version matches the version in the DB.
*
* @param dbvers fully qualified version from the DB
* @param ident identifier whose version is to be validated; the version need not be
* fully qualified
* @param result the validation result
*/
private void validateVersion(String dbvers, ToscaConceptIdentifier ident, BeanValidationResult result) {
String idvers = ident.getVersion();
if (dbvers.equals(idvers)) {
return;
}
// did not match - see if it's a prefix
if (SessionData.isVersionPrefix(idvers) && dbvers.startsWith(idvers + ".")) {
// ident has a prefix of this version
return;
}
result.addResult(POLICY_RESULT_NAME, ident, ValidationStatus.INVALID,
"different version already deployed: " + dbvers);
}
/**
* Deploys or updates PDP policies using the simple API.
*
* @param policies PDP policies
* @param user user triggering deployment
* @throws PfModelException if an error occurred
*/
public void deployPolicies(PdpDeployPolicies policies, String user) throws PfModelException {
try {
MyPdpDeployPolicies checked = coder.convert(policies, MyPdpDeployPolicies.class);
ValidationResult result = new BeanValidator().validateTop(PdpDeployPolicies.class.getSimpleName(), checked);
if (!result.isValid()) {
String msg = result.getResult().trim();
throw new PfModelException(Status.BAD_REQUEST, msg);
}
} catch (CoderException e) {
throw new PfModelException(Status.INTERNAL_SERVER_ERROR, "cannot decode request", e);
}
process(user, policies, this::deploySimplePolicies);
}
/**
* Deploys or updates PDP policies using the simple API. This is the method that does
* the actual work.
*
* @param data session data
* @param policies external PDP policies
* @throws PfModelException if an error occurred
*/
private void deploySimplePolicies(SessionData data, PdpDeployPolicies policies) throws PfModelException {
for (ToscaConceptIdentifierOptVersion desiredPolicy : policies.getPolicies()) {
try {
processPolicy(data, desiredPolicy);
} catch (PfModelException | RuntimeException e) {
// no need to log the error here, as it will be logged by the invoker
logger.warn("failed to deploy policy: {}", desiredPolicy);
throw e;
}
}
}
/**
* Adds a policy to a subgroup, if it isn't there already.
*/
@Override
protected Updater makeUpdater(SessionData data, ToscaPolicy policy,
ToscaConceptIdentifierOptVersion requestedIdent) {
ToscaConceptIdentifier desiredIdent = policy.getIdentifier();
ToscaConceptIdentifier desiredType = policy.getTypeIdentifier();
return (group, subgroup) -> {
if (!isPolicySupported(subgroup.getSupportedPolicyTypes(), desiredType)) {
// doesn't support the desired policy type
return false;
}
if (containsPolicy(group, subgroup, desiredIdent)) {
return false;
}
if (subgroup.getPdpInstances().isEmpty()) {
throw new PfModelRuntimeException(Status.BAD_REQUEST, "group " + group.getName() + " subgroup "
+ subgroup.getPdpType() + " has no active PDPs");
}
// add the policy to the subgroup
subgroup.getPolicies().add(desiredIdent);
logger.info("add policy {} to subgroup {} {} count={}", desiredIdent, group.getName(),
subgroup.getPdpType(), subgroup.getPolicies().size());
Set pdps = subgroup.getPdpInstances().stream().map(Pdp::getInstanceId).collect(Collectors.toSet());
ToscaPolicy policyToBeDeployed = data.getPolicy(new ToscaConceptIdentifierOptVersion(desiredIdent));
data.trackDeploy(policyToBeDeployed, pdps, group.getName(), subgroup.getPdpType());
return true;
};
}
/**
* Determines if a policy type is supported.
*
* @param supportedTypes supported policy types, any of which may end with ".*"
* @param desiredType policy type of interest
* @return {@code true} if the policy type is supported, {@code false} otherwise
*/
private boolean isPolicySupported(List supportedTypes,
ToscaConceptIdentifier desiredType) {
if (supportedTypes.contains(desiredType)) {
return true;
}
String desiredTypeName = desiredType.getName();
for (ToscaConceptIdentifier type : supportedTypes) {
String supType = type.getName();
if (supType.endsWith(".*") && desiredTypeName.startsWith(supType.substring(0, supType.length() - 1))) {
// matches everything up to, AND INCLUDING, the "."
return true;
}
}
return false;
}
/**
* Determines if a subgroup already contains the desired policy.
*
* @param group group that contains the subgroup
* @param subgroup subgroup of interest
* @param desiredIdent identifier of the desired policy
* @return {@code true} if the subgroup contains the desired policy, {@code false}
* otherwise
* @throws PfModelRuntimeException if the subgroup contains a different version of the
* desired policy
*/
private boolean containsPolicy(PdpGroup group, PdpSubGroup subgroup, ToscaConceptIdentifier desiredIdent) {
String desnm = desiredIdent.getName();
String desvers = desiredIdent.getVersion();
for (ToscaConceptIdentifier actualIdent : subgroup.getPolicies()) {
if (!actualIdent.getName().equals(desnm)) {
continue;
}
// found the policy - ensure the version matches
if (!actualIdent.getVersion().equals(desvers)) {
throw new PfModelRuntimeException(Status.BAD_REQUEST,
"group " + group.getName() + " subgroup " + subgroup.getPdpType() + " policy " + desnm
+ " " + desvers + " different version already deployed: "
+ actualIdent.getVersion());
}
// already has the desired policy & version
logger.info("subgroup {} {} already contains policy {}", group.getName(), subgroup.getPdpType(),
desiredIdent);
return true;
}
return false;
}
/*
* These are only used to validate the incoming request.
*/
@Getter
public static class MyPdpDeployPolicies {
@NotNull
private List<@NotNull @Valid PolicyIdent> policies;
}
@Getter
public static class PolicyIdent {
@SerializedName("policy-id")
@NotNull
@Pattern(regexp = PfKey.NAME_REGEXP)
private String name;
@SerializedName("policy-version")
@Pattern(regexp = "\\d+([.]\\d+[.]\\d+)?")
private String version;
}
}