2963fe621be9c6db6d616fb97ca4bde589d0f6cd
[policy/pap.git] / main / src / main / java / org / onap / policy / pap / main / rest / PdpGroupDeployProvider.java
1 /*
2  * ============LICENSE_START=======================================================
3  * ONAP PAP
4  * ================================================================================
5  * Copyright (C) 2019, 2022 AT&T Intellectual Property. All rights reserved.
6  * Modifications Copyright (C) 2020-2021 Nordix Foundation.
7  * Modifications Copyright (C) 2021 Bell Canada. All rights reserved.
8  * ================================================================================
9  * Licensed under the Apache License, Version 2.0 (the "License");
10  * you may not use this file except in compliance with the License.
11  * You may obtain a copy of the License at
12  *
13  *      http://www.apache.org/licenses/LICENSE-2.0
14  *
15  * Unless required by applicable law or agreed to in writing, software
16  * distributed under the License is distributed on an "AS IS" BASIS,
17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18  * See the License for the specific language governing permissions and
19  * limitations under the License.
20  * ============LICENSE_END=========================================================
21  */
22
23 package org.onap.policy.pap.main.rest;
24
25 import com.google.gson.annotations.SerializedName;
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.HashSet;
29 import java.util.LinkedHashSet;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Set;
33 import java.util.stream.Collectors;
34 import javax.ws.rs.core.Response.Status;
35 import lombok.Getter;
36 import org.onap.policy.common.parameters.BeanValidationResult;
37 import org.onap.policy.common.parameters.BeanValidator;
38 import org.onap.policy.common.parameters.ValidationResult;
39 import org.onap.policy.common.parameters.ValidationStatus;
40 import org.onap.policy.common.parameters.annotations.NotNull;
41 import org.onap.policy.common.parameters.annotations.Pattern;
42 import org.onap.policy.common.parameters.annotations.Valid;
43 import org.onap.policy.common.utils.coder.CoderException;
44 import org.onap.policy.common.utils.coder.StandardCoder;
45 import org.onap.policy.common.utils.services.Registry;
46 import org.onap.policy.models.base.PfKey;
47 import org.onap.policy.models.base.PfModelException;
48 import org.onap.policy.models.base.PfModelRuntimeException;
49 import org.onap.policy.models.pap.concepts.PdpDeployPolicies;
50 import org.onap.policy.models.pdp.concepts.DeploymentGroup;
51 import org.onap.policy.models.pdp.concepts.DeploymentGroups;
52 import org.onap.policy.models.pdp.concepts.DeploymentSubGroup;
53 import org.onap.policy.models.pdp.concepts.Pdp;
54 import org.onap.policy.models.pdp.concepts.PdpGroup;
55 import org.onap.policy.models.pdp.concepts.PdpSubGroup;
56 import org.onap.policy.models.tosca.authorative.concepts.ToscaConceptIdentifier;
57 import org.onap.policy.models.tosca.authorative.concepts.ToscaConceptIdentifierOptVersion;
58 import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
59 import org.onap.policy.pap.main.PapConstants;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
62 import org.springframework.stereotype.Service;
63
64 /**
65  * Provider for PAP component to deploy PDP groups. The following items must be in the
66  * {@link Registry}:
67  * <ul>
68  * <li>PDP Modification Lock</li>
69  * <li>PDP Modify Request Map</li>
70  * <li>PAP DAO Factory</li>
71  * </ul>
72  */
73 @Service
74 public class PdpGroupDeployProvider extends ProviderBase {
75     private static final Logger logger = LoggerFactory.getLogger(PdpGroupDeployProvider.class);
76     private static final StandardCoder coder = new StandardCoder();
77
78     private static final String POLICY_RESULT_NAME = "policy";
79
80     /**
81      * Updates policies in specific PDP groups.
82      *
83      * @param groups PDP group deployments to be updated
84      * @param user user triggering deployment
85      * @throws PfModelException if an error occurred
86      */
87     public void updateGroupPolicies(DeploymentGroups groups, String user) throws PfModelException {
88         ValidationResult result = groups.validatePapRest();
89         if (!result.isValid()) {
90             String msg = result.getResult().trim();
91             throw new PfModelException(Status.BAD_REQUEST, msg);
92         }
93
94         process(user, groups, this::updateGroups);
95     }
96
97     /**
98      * Updates policies in specific PDP groups. This is the method that does the actual work.
99      *
100      * @param data session data
101      * @param groups PDP group deployments
102      * @throws PfModelException if an error occurred
103      */
104     private void updateGroups(SessionData data, DeploymentGroups groups) throws PfModelException {
105         var result = new BeanValidationResult("groups", groups);
106
107         for (DeploymentGroup group : groups.getGroups()) {
108             PdpGroup dbgroup = data.getGroup(group.getName());
109
110             if (dbgroup == null) {
111                 result.addResult(group.getName(), group, ValidationStatus.INVALID, "unknown group");
112
113             } else {
114                 result.addResult(updateGroup(data, dbgroup, group));
115             }
116         }
117
118         if (!result.isValid()) {
119             throw new PfModelException(Status.BAD_REQUEST, result.getResult().trim());
120         }
121     }
122
123     /**
124      * Updates an existing group.
125      *
126      * @param data session data
127      * @param dbgroup the group, as it appears within the DB
128      * @param group the group to be added
129      * @return the validation result
130      * @throws PfModelException if an error occurred
131      */
132     private ValidationResult updateGroup(SessionData data, PdpGroup dbgroup, DeploymentGroup group)
133                     throws PfModelException {
134
135         var result = new BeanValidationResult(group.getName(), group);
136
137         boolean updated = updateSubGroups(data, dbgroup, group, result);
138
139         if (result.isValid() && updated) {
140             data.update(dbgroup);
141         }
142
143         return result;
144     }
145
146     /**
147      * Adds or updates subgroups within the group.
148      *
149      * @param data session data
150      * @param dbgroup the group, as it appears within the DB
151      * @param group the group to be added
152      * @param result the validation result
153      * @return {@code true} if the DB group was modified, {@code false} otherwise
154      * @throws PfModelException if an error occurred
155      */
156     private boolean updateSubGroups(SessionData data, PdpGroup dbgroup, DeploymentGroup group,
157                     BeanValidationResult result) throws PfModelException {
158
159         // create a map of existing subgroups
160         Map<String, PdpSubGroup> type2sub = new HashMap<>();
161         dbgroup.getPdpSubgroups().forEach(subgrp -> type2sub.put(subgrp.getPdpType(), subgrp));
162
163         var updated = false;
164
165         for (DeploymentSubGroup subgrp : group.getDeploymentSubgroups()) {
166             PdpSubGroup dbsub = type2sub.get(subgrp.getPdpType());
167             var subResult = new BeanValidationResult(subgrp.getPdpType(), subgrp);
168
169             if (dbsub == null) {
170                 subResult.addResult(subgrp.getPdpType(), subgrp, ValidationStatus.INVALID, "unknown subgroup");
171
172             } else {
173                 updated = updateSubGroup(data, dbgroup, dbsub, subgrp, subResult) || updated;
174             }
175
176             result.addResult(subResult);
177         }
178
179         return updated;
180     }
181
182     /**
183      * Updates an existing subgroup.
184      *
185      * @param data session data
186      * @param dbgroup the group, from the DB, containing the subgroup
187      * @param dbsub the subgroup, from the DB
188      * @param subgrp the subgroup to be updated, updated to fully qualified versions upon
189      *        return
190      * @param container container for additional validation results
191      * @return {@code true} if the subgroup content was changed, {@code false} if there
192      *         were no changes
193      * @throws PfModelException if an error occurred
194      */
195     private boolean updateSubGroup(SessionData data, PdpGroup dbgroup, PdpSubGroup dbsub, DeploymentSubGroup subgrp,
196                     BeanValidationResult container) throws PfModelException {
197
198         // perform additional validations first
199         if (!validateSubGroup(data, dbsub, subgrp, container)) {
200             return false;
201         }
202
203         var updated = false;
204
205         switch (subgrp.getAction()) {
206             case POST:
207                 updated = addPolicies(data, dbgroup.getName(), dbsub, subgrp);
208                 break;
209             case DELETE:
210                 updated = deletePolicies(data, dbgroup.getName(), dbsub, subgrp);
211                 break;
212             case PATCH:
213             default:
214                 updated = updatePolicies(data, dbgroup.getName(), dbsub, subgrp);
215                 break;
216         }
217
218         if (updated) {
219             // publish any changes to the PDPs
220             makeUpdates(data, dbgroup, dbsub);
221             return true;
222         }
223
224         return false;
225     }
226
227     private boolean addPolicies(SessionData data, String pdpGroup, PdpSubGroup dbsub, DeploymentSubGroup subgrp)
228                     throws PfModelException {
229
230         Set<ToscaConceptIdentifier> policies = new LinkedHashSet<>(dbsub.getPolicies());
231         policies.addAll(subgrp.getPolicies());
232
233         var subgrp2 = new DeploymentSubGroup(subgrp);
234         subgrp2.getPolicies().clear();
235         subgrp2.getPolicies().addAll(policies);
236
237         return updatePolicies(data, pdpGroup, dbsub, subgrp2);
238     }
239
240     private boolean deletePolicies(SessionData data, String pdpGroup, PdpSubGroup dbsub, DeploymentSubGroup subgrp)
241                     throws PfModelException {
242
243         Set<ToscaConceptIdentifier> policies = new LinkedHashSet<>(dbsub.getPolicies());
244         policies.removeAll(subgrp.getPolicies());
245
246         var subgrp2 = new DeploymentSubGroup(subgrp);
247         subgrp2.getPolicies().clear();
248         subgrp2.getPolicies().addAll(policies);
249
250         return updatePolicies(data, pdpGroup, dbsub, subgrp2);
251     }
252
253     private boolean updatePolicies(SessionData data, String pdpGroup, PdpSubGroup dbsub, DeploymentSubGroup subgrp)
254                     throws PfModelException {
255
256         Set<ToscaConceptIdentifier> undeployed = new HashSet<>(dbsub.getPolicies());
257         undeployed.removeAll(subgrp.getPolicies());
258
259         Set<ToscaConceptIdentifier> deployed = new HashSet<>(subgrp.getPolicies());
260         deployed.removeAll(dbsub.getPolicies());
261
262         if (deployed.isEmpty() && undeployed.isEmpty()) {
263             // lists are identical
264             return false;
265         }
266
267         Set<String> pdps = dbsub.getPdpInstances().stream().map(Pdp::getInstanceId).collect(Collectors.toSet());
268
269         for (ToscaConceptIdentifier policyId : deployed) {
270             ToscaPolicy policyToBeDeployed = data.getPolicy(new ToscaConceptIdentifierOptVersion(policyId));
271             data.trackDeploy(policyToBeDeployed, pdps, pdpGroup, dbsub.getPdpType());
272         }
273
274         for (ToscaConceptIdentifier policyId : undeployed) {
275             data.trackUndeploy(policyId, pdps, pdpGroup, dbsub.getPdpType());
276         }
277
278         dbsub.setPolicies(new ArrayList<>(subgrp.getPolicies()));
279         return true;
280     }
281
282     /**
283      * Performs additional validations of a subgroup.
284      *
285      * @param data session data
286      * @param dbsub the subgroup, from the DB
287      * @param subgrp the subgroup to be validated, updated to fully qualified versions
288      *        upon return
289      * @param container container for additional validation results
290      * @return {@code true} if the subgroup is valid, {@code false} otherwise
291      * @throws PfModelException if an error occurred
292      */
293     private boolean validateSubGroup(SessionData data, PdpSubGroup dbsub, DeploymentSubGroup subgrp,
294                     BeanValidationResult container) throws PfModelException {
295
296         var result = new BeanValidationResult(subgrp.getPdpType(), subgrp);
297
298         result.addResult(validatePolicies(data, dbsub, subgrp));
299         container.addResult(result);
300
301         return result.isValid();
302     }
303
304     /**
305      * Performs additional validations of the policies within a subgroup.
306      *
307      * @param data session data
308      * @param dbsub subgroup from the DB, or {@code null} if this is a new subgroup
309      * @param subgrp the subgroup whose policies are to be validated, updated to fully
310      *        qualified versions upon return
311      * @return the validation result
312      * @throws PfModelException if an error occurred
313      */
314     private ValidationResult validatePolicies(SessionData data, PdpSubGroup dbsub, DeploymentSubGroup subgrp)
315                     throws PfModelException {
316
317         // build a map of the DB data, from policy name to (fully qualified) policy
318         // version
319         Map<String, String> dbname2vers = new HashMap<>();
320         dbsub.getPolicies().forEach(ident -> dbname2vers.put(ident.getName(), ident.getVersion()));
321
322         var result = new BeanValidationResult(subgrp.getPdpType(), subgrp);
323
324         for (ToscaConceptIdentifier ident : subgrp.getPolicies()) {
325             // note: "ident" may not have a fully qualified version
326
327             String expectedVersion = dbname2vers.get(ident.getName());
328             if (expectedVersion != null) {
329                 // policy exists in the DB list - compare the versions
330                 validateVersion(expectedVersion, ident, result);
331                 ident.setVersion(expectedVersion);
332                 continue;
333             }
334
335             // policy doesn't appear in the DB's policy list - look it up
336
337             ToscaPolicy policy = data.getPolicy(new ToscaConceptIdentifierOptVersion(ident));
338             if (policy == null) {
339                 result.addResult(POLICY_RESULT_NAME, ident, ValidationStatus.INVALID, "unknown policy");
340
341             } else if (!isPolicySupported(dbsub.getSupportedPolicyTypes(), policy.getTypeIdentifier())) {
342                 result.addResult(POLICY_RESULT_NAME, ident, ValidationStatus.INVALID,
343                                 "not a supported policy for the subgroup");
344
345             } else {
346                 // replace version with the fully qualified version from the policy
347                 ident.setVersion(policy.getVersion());
348             }
349         }
350
351         return result;
352     }
353
354     /**
355      * Determines if the new version matches the version in the DB.
356      *
357      * @param dbvers fully qualified version from the DB
358      * @param ident identifier whose version is to be validated; the version need not be
359      *        fully qualified
360      * @param result the validation result
361      */
362     private void validateVersion(String dbvers, ToscaConceptIdentifier ident, BeanValidationResult result) {
363         String idvers = ident.getVersion();
364         if (dbvers.equals(idvers)) {
365             return;
366         }
367
368         // did not match - see if it's a prefix
369
370         if (SessionData.isVersionPrefix(idvers) && dbvers.startsWith(idvers + ".")) {
371             // ident has a prefix of this version
372             return;
373         }
374
375         result.addResult(POLICY_RESULT_NAME, ident, ValidationStatus.INVALID,
376                         "different version already deployed: " + dbvers);
377     }
378
379     /**
380      * Deploys or updates PDP policies using the simple API.
381      *
382      * @param policies PDP policies
383      * @param user user triggering deployment
384      * @throws PfModelException if an error occurred
385      */
386     public void deployPolicies(PdpDeployPolicies policies, String user) throws PfModelException {
387         try {
388             MyPdpDeployPolicies checked = coder.convert(policies, MyPdpDeployPolicies.class);
389             ValidationResult result = new BeanValidator().validateTop(PdpDeployPolicies.class.getSimpleName(), checked);
390             if (!result.isValid()) {
391                 String msg = result.getResult().trim();
392                 throw new PfModelException(Status.BAD_REQUEST, msg);
393             }
394         } catch (CoderException e) {
395             throw new PfModelException(Status.INTERNAL_SERVER_ERROR, "cannot decode request", e);
396         }
397
398         process(user, policies, this::deploySimplePolicies);
399     }
400
401     /**
402      * Deploys or updates PDP policies using the simple API. This is the method that does
403      * the actual work.
404      *
405      * @param data session data
406      * @param policies external PDP policies
407      * @throws PfModelException if an error occurred
408      */
409     private void deploySimplePolicies(SessionData data, PdpDeployPolicies policies) throws PfModelException {
410
411         for (ToscaConceptIdentifierOptVersion desiredPolicy : policies.getPolicies()) {
412             try {
413                 processPolicy(data, desiredPolicy);
414
415             } catch (PfModelException | RuntimeException e) {
416                 // no need to log the error here, as it will be logged by the invoker
417                 logger.warn("failed to deploy policy: {}", desiredPolicy);
418                 throw e;
419             }
420         }
421     }
422
423     /**
424      * Adds a policy to a subgroup, if it isn't there already.
425      */
426     @Override
427     protected Updater makeUpdater(SessionData data, ToscaPolicy policy,
428                     ToscaConceptIdentifierOptVersion requestedIdent) {
429
430         ToscaConceptIdentifier desiredIdent = policy.getIdentifier();
431         ToscaConceptIdentifier desiredType = policy.getTypeIdentifier();
432         PapStatisticsManager mgr = Registry.get(PapConstants.REG_STATISTICS_MANAGER, PapStatisticsManager.class);
433         mgr.updateTotalPolicyDeployCount();
434
435         return (group, subgroup) -> {
436
437             if (!isPolicySupported(subgroup.getSupportedPolicyTypes(), desiredType)) {
438                 // doesn't support the desired policy type
439                 return false;
440             }
441
442             if (containsPolicy(group, subgroup, desiredIdent)) {
443                 return false;
444             }
445
446             if (subgroup.getPdpInstances().isEmpty()) {
447                 throw new PfModelRuntimeException(Status.BAD_REQUEST, "group " + group.getName() + " subgroup "
448                                 + subgroup.getPdpType() + " has no active PDPs");
449             }
450
451
452             // add the policy to the subgroup
453             subgroup.getPolicies().add(desiredIdent);
454
455             logger.info("add policy {} to subgroup {} {} count={}", desiredIdent, group.getName(),
456                             subgroup.getPdpType(), subgroup.getPolicies().size());
457
458             Set<String> pdps = subgroup.getPdpInstances().stream().map(Pdp::getInstanceId).collect(Collectors.toSet());
459             ToscaPolicy policyToBeDeployed = data.getPolicy(new ToscaConceptIdentifierOptVersion(desiredIdent));
460             data.trackDeploy(policyToBeDeployed, pdps, group.getName(), subgroup.getPdpType());
461
462             return true;
463         };
464     }
465
466     /**
467      * Determines if a policy type is supported.
468      *
469      * @param supportedTypes supported policy types, any of which may end with ".*"
470      * @param desiredType policy type of interest
471      * @return {@code true} if the policy type is supported, {@code false} otherwise
472      */
473     private boolean isPolicySupported(List<ToscaConceptIdentifier> supportedTypes,
474                     ToscaConceptIdentifier desiredType) {
475
476         if (supportedTypes.contains(desiredType)) {
477             return true;
478         }
479
480         String desiredTypeName = desiredType.getName();
481         for (ToscaConceptIdentifier type : supportedTypes) {
482             String supType = type.getName();
483             if (supType.endsWith(".*") && desiredTypeName.startsWith(supType.substring(0, supType.length() - 1))) {
484                 // matches everything up to, AND INCLUDING, the "."
485                 return true;
486             }
487         }
488
489         return false;
490     }
491
492     /**
493      * Determines if a subgroup already contains the desired policy.
494      *
495      * @param group group that contains the subgroup
496      * @param subgroup subgroup of interest
497      * @param desiredIdent identifier of the desired policy
498      * @return {@code true} if the subgroup contains the desired policy, {@code false}
499      *         otherwise
500      * @throws PfModelRuntimeException if the subgroup contains a different version of the
501      *         desired policy
502      */
503     private boolean containsPolicy(PdpGroup group, PdpSubGroup subgroup, ToscaConceptIdentifier desiredIdent) {
504         String desnm = desiredIdent.getName();
505         String desvers = desiredIdent.getVersion();
506
507         for (ToscaConceptIdentifier actualIdent : subgroup.getPolicies()) {
508             if (!actualIdent.getName().equals(desnm)) {
509                 continue;
510             }
511
512             // found the policy - ensure the version matches
513             if (!actualIdent.getVersion().equals(desvers)) {
514                 throw new PfModelRuntimeException(Status.BAD_REQUEST,
515                                 "group " + group.getName() + " subgroup " + subgroup.getPdpType() + " policy " + desnm
516                                                 + " " + desvers + " different version already deployed: "
517                                                 + actualIdent.getVersion());
518             }
519
520             // already has the desired policy & version
521             logger.info("subgroup {} {} already contains policy {}", group.getName(), subgroup.getPdpType(),
522                         desiredIdent);
523             return true;
524         }
525
526         return false;
527     }
528
529     /*
530      * These are only used to validate the incoming request.
531      */
532
533     @Getter
534     public static class MyPdpDeployPolicies {
535         @NotNull
536         private List<@NotNull @Valid PolicyIdent> policies;
537     }
538
539     @Getter
540     public static class PolicyIdent {
541         @SerializedName("policy-id")
542         @NotNull
543         @Pattern(regexp = PfKey.NAME_REGEXP)
544         private String name;
545
546         @SerializedName("policy-version")
547         @Pattern(regexp = "\\d+([.]\\d+[.]\\d+)?")
548         private String version;
549     }
550 }