Generate notifications when policies change
[policy/pap.git] / main / src / main / java / org / onap / policy / pap / main / rest / depundep / PdpGroupDeployProvider.java
1 /*
2  * ============LICENSE_START=======================================================
3  * ONAP PAP
4  * ================================================================================
5  * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.onap.policy.pap.main.rest.depundep;
22
23 import com.att.aft.dme2.internal.apache.commons.lang.ObjectUtils;
24 import java.util.ArrayList;
25 import java.util.Collections;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Set;
31 import java.util.function.Consumer;
32 import java.util.stream.Collectors;
33 import javax.ws.rs.core.Response.Status;
34 import org.onap.policy.common.parameters.BeanValidationResult;
35 import org.onap.policy.common.parameters.ObjectValidationResult;
36 import org.onap.policy.common.parameters.ValidationResult;
37 import org.onap.policy.common.parameters.ValidationStatus;
38 import org.onap.policy.common.utils.services.Registry;
39 import org.onap.policy.models.base.PfModelException;
40 import org.onap.policy.models.base.PfModelRuntimeException;
41 import org.onap.policy.models.pap.concepts.PdpDeployPolicies;
42 import org.onap.policy.models.pdp.concepts.Pdp;
43 import org.onap.policy.models.pdp.concepts.PdpGroup;
44 import org.onap.policy.models.pdp.concepts.PdpGroups;
45 import org.onap.policy.models.pdp.concepts.PdpStateChange;
46 import org.onap.policy.models.pdp.concepts.PdpSubGroup;
47 import org.onap.policy.models.pdp.concepts.PdpUpdate;
48 import org.onap.policy.models.pdp.enums.PdpState;
49 import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
50 import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicyIdentifier;
51 import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicyIdentifierOptVersion;
52 import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicyTypeIdentifier;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 /**
57  * Provider for PAP component to deploy PDP groups. The following items must be in the
58  * {@link Registry}:
59  * <ul>
60  * <li>PDP Modification Lock</li>
61  * <li>PDP Modify Request Map</li>
62  * <li>PAP DAO Factory</li>
63  * </ul>
64  */
65 public class PdpGroupDeployProvider extends ProviderBase {
66     private static final Logger logger = LoggerFactory.getLogger(PdpGroupDeployProvider.class);
67
68     private static final String POLICY_RESULT_NAME = "policy";
69
70
71     /**
72      * Constructs the object.
73      */
74     public PdpGroupDeployProvider() {
75         super();
76     }
77
78     /**
79      * Creates or updates PDP groups.
80      *
81      * @param groups PDP group configurations to be created or updated
82      * @throws PfModelException if an error occurred
83      */
84     public void createOrUpdateGroups(PdpGroups groups) throws PfModelException {
85         ValidationResult result = groups.validatePapRest();
86
87         if (!result.isValid()) {
88             String msg = result.getResult().trim();
89             logger.warn(msg);
90             throw new PfModelException(Status.BAD_REQUEST, msg);
91         }
92
93         process(groups, this::createOrUpdate);
94     }
95
96     /**
97      * Creates or updates PDP groups. This is the method that does the actual work.
98      *
99      * @param data session data
100      * @param groups PDP group configurations
101      * @throws PfModelException if an error occurred
102      */
103     private void createOrUpdate(SessionData data, PdpGroups groups) throws PfModelException {
104         BeanValidationResult result = new BeanValidationResult("groups", groups);
105
106         for (PdpGroup group : groups.getGroups()) {
107             PdpGroup dbgroup = data.getGroup(group.getName());
108
109             if (dbgroup == null) {
110                 result.addResult(addGroup(data, group));
111
112             } else {
113                 result.addResult(updateGroup(data, dbgroup, group));
114             }
115         }
116
117         if (!result.isValid()) {
118             throw new PfModelException(Status.BAD_REQUEST, result.getResult().trim());
119         }
120     }
121
122     /**
123      * Adds a new group.
124      *
125      * @param data session data
126      * @param group the group to be added
127      * @return the validation result
128      * @throws PfModelException if an error occurred
129      */
130     private ValidationResult addGroup(SessionData data, PdpGroup group) throws PfModelException {
131         BeanValidationResult result = new BeanValidationResult(group.getName(), group);
132
133         validateGroupOnly(group, result);
134         if (!result.isValid()) {
135             return result;
136         }
137
138         // default to active
139         if (group.getPdpGroupState() == null) {
140             group.setPdpGroupState(PdpState.ACTIVE);
141         }
142
143         for (PdpSubGroup subgrp : group.getPdpSubgroups()) {
144             result.addResult(addSubGroup(data, subgrp));
145         }
146
147         if (result.isValid()) {
148             data.create(group);
149         }
150
151         return result;
152     }
153
154     /**
155      * Performs additional validations of a group, but does not examine the subgroups.
156      *
157      * @param group the group to be validated
158      * @param result the validation result
159      */
160     private void validateGroupOnly(PdpGroup group, BeanValidationResult result) {
161         if (group.getPdpGroupState() == null) {
162             return;
163         }
164
165         switch (group.getPdpGroupState()) {
166             case ACTIVE:
167             case PASSIVE:
168                 break;
169
170             default:
171                 result.addResult(new ObjectValidationResult("pdpGroupState", group.getPdpGroupState(),
172                                 ValidationStatus.INVALID, "must be null, ACTIVE, or PASSIVE"));
173                 break;
174         }
175     }
176
177     /**
178      * Updates an existing group.
179      *
180      * @param data session data
181      * @param dbgroup the group, as it appears within the DB
182      * @param group the group to be added
183      * @return the validation result
184      * @throws PfModelException if an error occurred
185      */
186     private ValidationResult updateGroup(SessionData data, PdpGroup dbgroup, PdpGroup group) throws PfModelException {
187         BeanValidationResult result = new BeanValidationResult(group.getName(), group);
188
189         if (!ObjectUtils.equals(dbgroup.getProperties(), group.getProperties())) {
190             result.addResult(new ObjectValidationResult("properties", "", ValidationStatus.INVALID,
191                             "cannot change properties"));
192         }
193
194         boolean updated = updateField(dbgroup.getDescription(), group.getDescription(), dbgroup::setDescription);
195         updated = notifyPdpsDelSubGroups(data, dbgroup, group) || updated;
196         updated = addOrUpdateSubGroups(data, dbgroup, group, result) || updated;
197
198         if (result.isValid() && updated) {
199             data.update(group);
200         }
201
202         return result;
203     }
204
205     /**
206      * Updates a field, if the new value is different than the old value.
207      *
208      * @param oldValue old value
209      * @param newValue new value
210      * @param setter function to set the field to the new value
211      * @return {@code true} if the field was updated, {@code false} if it already matched
212      *         the new value
213      */
214     private <T> boolean updateField(T oldValue, T newValue, Consumer<T> setter) {
215         if (oldValue == newValue) {
216             return false;
217         }
218
219         if (oldValue != null && oldValue.equals(newValue)) {
220             return false;
221         }
222
223         setter.accept(newValue);
224         return true;
225     }
226
227     /**
228      * Adds or updates subgroups within the group.
229      *
230      * @param data session data
231      * @param dbgroup the group, as it appears within the DB
232      * @param group the group to be added
233      * @param result the validation result
234      * @return {@code true} if the DB group was modified, {@code false} otherwise
235      * @throws PfModelException if an error occurred
236      */
237     private boolean addOrUpdateSubGroups(SessionData data, PdpGroup dbgroup, PdpGroup group,
238                     BeanValidationResult result) throws PfModelException {
239
240         // create a map of existing subgroups
241         Map<String, PdpSubGroup> type2sub = new HashMap<>();
242         dbgroup.getPdpSubgroups().forEach(subgrp -> type2sub.put(subgrp.getPdpType(), subgrp));
243
244         boolean updated = false;
245
246         for (PdpSubGroup subgrp : group.getPdpSubgroups()) {
247             PdpSubGroup dbsub = type2sub.get(subgrp.getPdpType());
248             BeanValidationResult subResult = new BeanValidationResult(subgrp.getPdpType(), subgrp);
249
250             if (dbsub == null) {
251                 updated = true;
252                 subResult.addResult(addSubGroup(data, subgrp));
253                 dbgroup.getPdpSubgroups().add(subgrp);
254
255             } else {
256                 updated = updateSubGroup(data, group, dbsub, subgrp, subResult) || updated;
257             }
258
259             result.addResult(subResult);
260         }
261
262         return updated;
263     }
264
265     /**
266      * Notifies any PDPs whose subgroups are being removed.
267      *
268      * @param data session data
269      * @param dbgroup the group, as it appears within the DB
270      * @param group the group being updated
271      * @return {@code true} if a subgroup was removed, {@code false} otherwise
272      * @throws PfModelException if an error occurred
273      */
274     private boolean notifyPdpsDelSubGroups(SessionData data, PdpGroup dbgroup, PdpGroup group) throws PfModelException {
275         boolean updated = false;
276
277         // subgroups, as they appear within the updated group
278         Set<String> subgroups = new HashSet<>();
279         group.getPdpSubgroups().forEach(subgrp -> subgroups.add(subgrp.getPdpType()));
280
281         // loop through subgroups as they appear within the DB
282         for (PdpSubGroup subgrp : dbgroup.getPdpSubgroups()) {
283
284             if (!subgroups.contains(subgrp.getPdpType())) {
285                 // this subgroup no longer appears - notify its PDPs
286                 updated = true;
287                 notifyPdpsDelSubGroup(data, subgrp);
288                 trackPdpsDelSubGroup(data, subgrp);
289             }
290         }
291
292         return updated;
293     }
294
295     /**
296      * Notifies the PDPs that their subgroup is being removed.
297      *
298      * @param data session data
299      * @param subgrp subgroup that is being removed
300      */
301     private void notifyPdpsDelSubGroup(SessionData data, PdpSubGroup subgrp) {
302         for (Pdp pdp : subgrp.getPdpInstances()) {
303             String name = pdp.getInstanceId();
304
305             // make it passive
306             PdpStateChange change = new PdpStateChange();
307             change.setName(name);
308             change.setState(PdpState.PASSIVE);
309
310             // remove it from subgroup and undeploy all policies
311             PdpUpdate update = new PdpUpdate();
312             update.setName(name);
313
314             data.addRequests(update, change);
315         }
316     }
317
318     /**
319      * Tracks PDP responses when their subgroup is removed.
320      *
321      * @param data session data
322      * @param subgrp subgroup that is being removed
323      * @throws PfModelException if an error occurred
324      */
325     private void trackPdpsDelSubGroup(SessionData data, PdpSubGroup subgrp) throws PfModelException {
326         Set<String> pdps = subgrp.getPdpInstances().stream().map(Pdp::getInstanceId).collect(Collectors.toSet());
327
328         for (ToscaPolicyIdentifier policyId : subgrp.getPolicies()) {
329             data.trackUndeploy(policyId, pdps);
330         }
331     }
332
333     /**
334      * Adds a new subgroup.
335      *
336      * @param data session data
337      * @param subgrp the subgroup to be added, updated to fully qualified versions upon
338      *        return
339      * @return the validation result
340      * @throws PfModelException if an error occurred
341      */
342     private ValidationResult addSubGroup(SessionData data, PdpSubGroup subgrp) throws PfModelException {
343         subgrp.setCurrentInstanceCount(0);
344         subgrp.setPdpInstances(Collections.emptyList());
345
346         BeanValidationResult result = new BeanValidationResult(subgrp.getPdpType(), subgrp);
347
348         result.addResult(validateSupportedTypes(data, subgrp));
349         result.addResult(validatePolicies(data, null, subgrp));
350
351         return result;
352     }
353
354     /**
355      * Updates an existing subgroup.
356      *
357      * @param data session data
358      * @param dbgroup the group, from the DB, containing the subgroup
359      * @param dbsub the subgroup, from the DB
360      * @param subgrp the subgroup to be updated, updated to fully qualified versions upon
361      *        return
362      * @param container container for additional validation results
363      * @return {@code true} if the subgroup content was changed, {@code false} if there
364      *         were no changes
365      * @throws PfModelException if an error occurred
366      */
367     private boolean updateSubGroup(SessionData data, PdpGroup dbgroup, PdpSubGroup dbsub, PdpSubGroup subgrp,
368                     BeanValidationResult container) throws PfModelException {
369
370         // perform additional validations first
371         if (!validateSubGroup(data, dbsub, subgrp, container)) {
372             return false;
373         }
374
375         /*
376          * first, apply the changes about which the PDPs care
377          */
378         boolean updated = updatePolicies(data, dbsub, subgrp);
379
380         // publish any changes to the PDPs
381         if (updated) {
382             makeUpdates(data, dbgroup, dbsub);
383         }
384
385         /*
386          * now make any remaining changes
387          */
388         updated = updateList(dbsub.getSupportedPolicyTypes(), subgrp.getSupportedPolicyTypes(),
389                         dbsub::setSupportedPolicyTypes) || updated;
390
391         return updateField(dbsub.getDesiredInstanceCount(), subgrp.getDesiredInstanceCount(),
392                         dbsub::setDesiredInstanceCount) || updated;
393     }
394
395     private boolean updatePolicies(SessionData data, PdpSubGroup dbsub, PdpSubGroup subgrp) throws PfModelException {
396         Set<ToscaPolicyIdentifier> undeployed = new HashSet<>(dbsub.getPolicies());
397         undeployed.removeAll(subgrp.getPolicies());
398
399         Set<ToscaPolicyIdentifier> deployed = new HashSet<>(subgrp.getPolicies());
400         deployed.removeAll(dbsub.getPolicies());
401
402         if (deployed.isEmpty() && undeployed.isEmpty()) {
403             // lists are identical
404             return false;
405         }
406
407
408         Set<String> pdps = subgrp.getPdpInstances().stream().map(Pdp::getInstanceId).collect(Collectors.toSet());
409
410         for (ToscaPolicyIdentifier policyId : deployed) {
411             data.trackDeploy(policyId, pdps);
412         }
413
414         for (ToscaPolicyIdentifier policyId : undeployed) {
415             data.trackUndeploy(policyId, pdps);
416         }
417
418         dbsub.setPolicies(new ArrayList<>(subgrp.getPolicies()));
419         return true;
420     }
421
422     /**
423      * Performs additional validations of a subgroup.
424      *
425      * @param data session data
426      * @param dbsub the subgroup, from the DB
427      * @param subgrp the subgroup to be validated, updated to fully qualified versions
428      *        upon return
429      * @param container container for additional validation results
430      * @return {@code true} if the subgroup is valid, {@code false} otherwise
431      * @throws PfModelException if an error occurred
432      */
433     private boolean validateSubGroup(SessionData data, PdpSubGroup dbsub, PdpSubGroup subgrp,
434                     BeanValidationResult container) throws PfModelException {
435
436         BeanValidationResult result = new BeanValidationResult(subgrp.getPdpType(), subgrp);
437
438         if (!ObjectUtils.equals(dbsub.getProperties(), subgrp.getProperties())) {
439             result.addResult(new ObjectValidationResult("properties", "", ValidationStatus.INVALID,
440                             "cannot change properties"));
441         }
442
443         result.addResult(validatePolicies(data, dbsub, subgrp));
444         container.addResult(result);
445
446         return result.isValid();
447     }
448
449     /**
450      * Updates a DB list with items from a new list.
451      *
452      * @param dblist the list from the DB
453      * @param newList the new list
454      * @param setter function to set the new list
455      * @return {@code true} if the list changed, {@code false} if the lists were the same
456      */
457     private <T> boolean updateList(List<T> dblist, List<T> newList, Consumer<List<T>> setter) {
458
459         Set<T> dbTypes = new HashSet<>(dblist);
460         Set<T> newTypes = new HashSet<>(newList);
461
462         if (dbTypes.equals(newTypes)) {
463             return false;
464         }
465
466         setter.accept(new ArrayList<>(newTypes));
467
468         return true;
469     }
470
471     /**
472      * Performs additional validations of the supported policy types within a subgroup.
473      *
474      * @param data session data
475      * @param subgrp the subgroup to be validated
476      * @param result the validation result
477      * @throws PfModelException if an error occurred
478      */
479     private ValidationResult validateSupportedTypes(SessionData data, PdpSubGroup subgrp) throws PfModelException {
480         BeanValidationResult result = new BeanValidationResult(subgrp.getPdpType(), subgrp);
481
482         for (ToscaPolicyTypeIdentifier type : subgrp.getSupportedPolicyTypes()) {
483             if (data.getPolicyType(type) == null) {
484                 result.addResult(new ObjectValidationResult("policy type", type, ValidationStatus.INVALID,
485                                 "unknown policy type"));
486             }
487         }
488
489         return result;
490     }
491
492     /**
493      * Performs additional validations of the policies within a subgroup.
494      *
495      * @param data session data
496      * @param dbsub subgroup from the DB, or {@code null} if this is a new subgroup
497      * @param subgrp the subgroup whose policies are to be validated, updated to fully
498      *        qualified versions upon return
499      * @param result the validation result
500      * @throws PfModelException if an error occurred
501      */
502     private ValidationResult validatePolicies(SessionData data, PdpSubGroup dbsub, PdpSubGroup subgrp)
503                     throws PfModelException {
504
505         // build a map of the DB data, from policy name to (fully qualified) policy
506         // version
507         Map<String, String> dbname2vers = new HashMap<>();
508         if (dbsub != null) {
509             dbsub.getPolicies().forEach(ident -> dbname2vers.put(ident.getName(), ident.getVersion()));
510         }
511
512         BeanValidationResult result = new BeanValidationResult(subgrp.getPdpType(), subgrp);
513
514         for (ToscaPolicyIdentifier ident : subgrp.getPolicies()) {
515             // note: "ident" may not have a fully qualified version
516
517             String expectedVersion = dbname2vers.get(ident.getName());
518             if (expectedVersion != null) {
519                 // policy exists in the DB list - compare the versions
520                 validateVersion(expectedVersion, ident, result);
521                 ident.setVersion(expectedVersion);
522                 continue;
523             }
524
525             // policy doesn't appear in the DB's policy list - look it up
526
527             ToscaPolicy policy = data.getPolicy(new ToscaPolicyIdentifierOptVersion(ident));
528             if (policy == null) {
529                 result.addResult(new ObjectValidationResult(POLICY_RESULT_NAME, ident, ValidationStatus.INVALID,
530                                 "unknown policy"));
531
532             } else if (!subgrp.getSupportedPolicyTypes().contains(policy.getTypeIdentifier())) {
533                 result.addResult(new ObjectValidationResult(POLICY_RESULT_NAME, ident, ValidationStatus.INVALID,
534                                 "not a supported policy for the subgroup"));
535
536             } else {
537                 // replace version with the fully qualified version from the policy
538                 ident.setVersion(policy.getVersion());
539             }
540         }
541
542         return result;
543     }
544
545     /**
546      * Determines if the new version matches the version in the DB.
547      *
548      * @param dbvers fully qualified version from the DB
549      * @param ident identifier whose version is to be validated; the version need not be
550      *        fully qualified
551      * @param result the validation result
552      */
553     private void validateVersion(String dbvers, ToscaPolicyIdentifier ident, BeanValidationResult result) {
554         String idvers = ident.getVersion();
555         if (dbvers.equals(idvers)) {
556             return;
557         }
558
559         // did not match - see if it's a prefix
560
561         if (SessionData.isVersionPrefix(idvers) && dbvers.startsWith(idvers + ".")) {
562             // ident has a prefix of this version
563             return;
564         }
565
566         result.addResult(new ObjectValidationResult(POLICY_RESULT_NAME, ident, ValidationStatus.INVALID,
567                         "different version already deployed: " + dbvers));
568     }
569
570     /**
571      * Deploys or updates PDP policies using the simple API.
572      *
573      * @param policies PDP policies
574      * @throws PfModelException if an error occurred
575      */
576     public void deployPolicies(PdpDeployPolicies policies) throws PfModelException {
577         process(policies, this::deploySimplePolicies);
578     }
579
580     /**
581      * Deploys or updates PDP policies using the simple API. This is the method that does
582      * the actual work.
583      *
584      * @param data session data
585      * @param extPolicies external PDP policies
586      * @return a list of requests that should be sent to configure the PDPs
587      * @throws PfModelException if an error occurred
588      */
589     private void deploySimplePolicies(SessionData data, PdpDeployPolicies policies) throws PfModelException {
590
591         for (ToscaPolicyIdentifierOptVersion desiredPolicy : policies.getPolicies()) {
592
593             try {
594                 processPolicy(data, desiredPolicy);
595
596             } catch (PfModelException | RuntimeException e) {
597                 // no need to log the error here, as it will be logged by the invoker
598                 logger.warn("failed to deploy policy: {}", desiredPolicy);
599                 throw e;
600             }
601         }
602     }
603
604     /**
605      * Adds a policy to a subgroup, if it isn't there already.
606      */
607     @Override
608     protected Updater makeUpdater(SessionData data, ToscaPolicy policy,
609                     ToscaPolicyIdentifierOptVersion requestedIdent) {
610
611         ToscaPolicyIdentifier desiredIdent = policy.getIdentifier();
612         ToscaPolicyTypeIdentifier desiredType = policy.getTypeIdentifier();
613
614         return (group, subgroup) -> {
615
616             if (!subgroup.getSupportedPolicyTypes().contains(desiredType)) {
617                 // doesn't support the desired policy type
618                 return false;
619             }
620
621             if (containsPolicy(group, subgroup, desiredIdent)) {
622                 return false;
623             }
624
625             if (subgroup.getPdpInstances().isEmpty()) {
626                 throw new PfModelRuntimeException(Status.BAD_REQUEST, "group " + group.getName() + " subgroup "
627                                 + subgroup.getPdpType() + " has no active PDPs");
628             }
629
630
631             // add the policy to the subgroup
632             subgroup.getPolicies().add(desiredIdent);
633
634             logger.info("add policy {} {} to subgroup {} {} count={}", desiredIdent.getName(),
635                             desiredIdent.getVersion(), group.getName(), subgroup.getPdpType(),
636                             subgroup.getPolicies().size());
637
638             Set<String> pdps = subgroup.getPdpInstances().stream().map(Pdp::getInstanceId).collect(Collectors.toSet());
639             data.trackDeploy(desiredIdent, pdps);
640
641             return true;
642         };
643     }
644
645     /**
646      * Determines if a subgroup already contains the desired policy.
647      *
648      * @param group group that contains the subgroup
649      * @param subgroup subgroup of interest
650      * @param desiredIdent identifier of the desired policy
651      * @return {@code true} if the subgroup contains the desired policy, {@code false}
652      *         otherwise
653      * @throws PfModelRuntimeException if the subgroup contains a different version of the
654      *         desired policy
655      */
656     private boolean containsPolicy(PdpGroup group, PdpSubGroup subgroup, ToscaPolicyIdentifier desiredIdent) {
657         String desnm = desiredIdent.getName();
658         String desvers = desiredIdent.getVersion();
659
660         for (ToscaPolicyIdentifier actualIdent : subgroup.getPolicies()) {
661             if (!actualIdent.getName().equals(desnm)) {
662                 continue;
663             }
664
665             // found the policy - ensure the version matches
666             if (!actualIdent.getVersion().equals(desvers)) {
667                 throw new PfModelRuntimeException(Status.BAD_REQUEST,
668                                 "group " + group.getName() + " subgroup " + subgroup.getPdpType() + " policy " + desnm
669                                                 + " " + desvers + " different version already deployed: "
670                                                 + actualIdent.getVersion());
671             }
672
673             // already has the desired policy & version
674             logger.info("subgroup {} {} already contains policy {} {}", group.getName(), subgroup.getPdpType(), desnm,
675                             desvers);
676             return true;
677         }
678
679         return false;
680     }
681 }