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