blob: b1acf7f51f123f5edbf5a77915cef04ffec11a61 [file] [log] [blame]
jaegonkim6a7b5242018-09-12 23:09:42 +09001/*
2 * Copyright 2018-present Open Networking Foundation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package org.onosproject.workflow.impl;
17
mohamedrahilr63a921c2019-02-27 19:48:25 +053018
jaegonkim6a7b5242018-09-12 23:09:42 +090019import com.fasterxml.jackson.databind.JsonNode;
jaegonkim13b25cb2019-03-22 06:23:23 +090020import com.fasterxml.jackson.databind.node.ArrayNode;
jaegonkim6a7b5242018-09-12 23:09:42 +090021import com.fasterxml.jackson.databind.node.JsonNodeFactory;
mohamedrahilr63a921c2019-02-27 19:48:25 +053022import com.fasterxml.jackson.databind.node.JsonNodeType;
jaegonkim13b25cb2019-03-22 06:23:23 +090023import com.fasterxml.jackson.databind.node.MissingNode;
jaegonkim44628d62019-04-07 10:30:32 +090024import com.google.common.base.MoreObjects;
jaegonkim6a7b5242018-09-12 23:09:42 +090025import org.onosproject.net.config.NetworkConfigRegistry;
26import org.onosproject.net.config.NetworkConfigService;
jaegonkim44628d62019-04-07 10:30:32 +090027import org.onosproject.workflow.api.WorkflowDefinitionException;
jaegonkim6a7b5242018-09-12 23:09:42 +090028import org.onosproject.workflow.api.WorkflowService;
29import org.onosproject.workflow.api.WorkflowExecutionService;
jaegonkim6a7b5242018-09-12 23:09:42 +090030import org.onosproject.workflow.api.WorkplaceStore;
mohamedrahilr63a921c2019-02-27 19:48:25 +053031import org.onosproject.workflow.api.WorkflowStore;
32import org.onosproject.workflow.api.WorkplaceDescription;
33import org.onosproject.workflow.api.WorkflowException;
34import org.onosproject.workflow.api.DefaultWorkplace;
35import org.onosproject.workflow.api.JsonDataModelTree;
36import org.onosproject.workflow.api.WorkflowDescription;
37import org.onosproject.workflow.api.Workplace;
38import org.onosproject.workflow.api.WorkflowDataModelException;
39import org.onosproject.workflow.api.Workflow;
40import org.onosproject.workflow.api.Worklet;
41import org.onosproject.workflow.api.WorkflowContext;
42import org.onosproject.workflow.api.JsonDataModel;
Ray Milkeydf521292018-10-04 15:13:33 -070043import org.osgi.service.component.annotations.Activate;
44import org.osgi.service.component.annotations.Component;
45import org.osgi.service.component.annotations.Deactivate;
46import org.osgi.service.component.annotations.Reference;
47import org.osgi.service.component.annotations.ReferenceCardinality;
jaegonkim6a7b5242018-09-12 23:09:42 +090048import org.slf4j.Logger;
49
mohamedrahilr63a921c2019-02-27 19:48:25 +053050import java.lang.annotation.Annotation;
51import java.lang.reflect.Field;
jaegonkim6a7b5242018-09-12 23:09:42 +090052import java.net.URI;
jaegonkim13b25cb2019-03-22 06:23:23 +090053import java.util.ArrayList;
jaegonkim44628d62019-04-07 10:30:32 +090054import java.util.HashMap;
jaegonkim13b25cb2019-03-22 06:23:23 +090055import java.util.List;
jaegonkim44628d62019-04-07 10:30:32 +090056import java.util.Map;
jaegonkim6a7b5242018-09-12 23:09:42 +090057import java.util.Objects;
jaegonkim13b25cb2019-03-22 06:23:23 +090058import java.util.Optional;
mohamedrahilr63a921c2019-02-27 19:48:25 +053059import java.util.regex.Matcher;
60import java.util.regex.Pattern;
jaegonkim6a7b5242018-09-12 23:09:42 +090061
62import static org.slf4j.LoggerFactory.getLogger;
63
Ray Milkeydf521292018-10-04 15:13:33 -070064@Component(immediate = true, service = WorkflowService.class)
jaegonkim6a7b5242018-09-12 23:09:42 +090065public class WorkflowManager implements WorkflowService {
66
67 protected static final Logger log = getLogger(WorkflowManager.class);
68
Ray Milkeydf521292018-10-04 15:13:33 -070069 @Reference(cardinality = ReferenceCardinality.MANDATORY)
jaegonkim6a7b5242018-09-12 23:09:42 +090070 private WorkflowExecutionService workflowExecutionService;
71
Ray Milkeydf521292018-10-04 15:13:33 -070072 @Reference(cardinality = ReferenceCardinality.MANDATORY)
jaegonkim6a7b5242018-09-12 23:09:42 +090073 protected WorkplaceStore workplaceStore;
74
Ray Milkeydf521292018-10-04 15:13:33 -070075 @Reference(cardinality = ReferenceCardinality.MANDATORY)
jaegonkim6a7b5242018-09-12 23:09:42 +090076 protected WorkflowStore workflowStore;
77
Ray Milkeydf521292018-10-04 15:13:33 -070078 @Reference(cardinality = ReferenceCardinality.MANDATORY)
jaegonkim6a7b5242018-09-12 23:09:42 +090079 private NetworkConfigService networkConfigService;
80
Ray Milkeydf521292018-10-04 15:13:33 -070081 @Reference(cardinality = ReferenceCardinality.MANDATORY)
jaegonkim6a7b5242018-09-12 23:09:42 +090082 private NetworkConfigRegistry networkConfigRegistry;
83
84 private WorkflowNetConfigListener netcfgListener;
85
86 @Activate
87 public void activate() {
88 netcfgListener = new WorkflowNetConfigListener(this);
89 networkConfigRegistry.registerConfigFactory(netcfgListener.getConfigFactory());
90 networkConfigService.addListener(netcfgListener);
91 log.info("Started");
92 }
93
94 @Deactivate
95 public void deactivate() {
96 networkConfigService.removeListener(netcfgListener);
97 networkConfigRegistry.unregisterConfigFactory(netcfgListener.getConfigFactory());
98 log.info("Stopped");
99 }
100
101 @Override
jaegonkim44628d62019-04-07 10:30:32 +0900102 public void register(Workflow workflow) throws WorkflowException {
103 checkWorkflow(workflow);
104 workflowStore.register(workflow);
105 }
106
107 @Override
jaegonkim6a7b5242018-09-12 23:09:42 +0900108 public void createWorkplace(WorkplaceDescription wpDesc) throws WorkflowException {
109 log.info("createWorkplace: {}", wpDesc);
110
111 JsonNode root;
112 if (wpDesc.data().isPresent()) {
113 root = wpDesc.data().get();
114 } else {
115 root = JsonNodeFactory.instance.objectNode();
116 }
117 DefaultWorkplace workplace =
118 new DefaultWorkplace(wpDesc.name(), new JsonDataModelTree(root));
119 workplaceStore.registerWorkplace(wpDesc.name(), workplace);
120 }
121
122 @Override
123 public void removeWorkplace(WorkplaceDescription wpDesc) throws WorkflowException {
124 log.info("removeWorkplace: {}", wpDesc);
125 //TODO: Removing workflows belong to this workplace
126 workplaceStore.removeWorkplace(wpDesc.name());
127 }
128
129 @Override
130 public void clearWorkplace() throws WorkflowException {
131 log.info("clearWorkplace");
132 workplaceStore.getWorkplaces().stream()
133 .filter(wp -> !Objects.equals(wp.name(), Workplace.SYSTEM_WORKPLACE))
134 .forEach(wp -> workplaceStore.removeWorkplace(wp.name()));
135 }
136
137 @Override
138 public void invokeWorkflow(WorkflowDescription wfDesc) throws WorkflowException {
139 invokeWorkflow(wfDesc.toJson());
140 }
141
142 @Override
143 public void invokeWorkflow(JsonNode worklowDescJson) throws WorkflowException {
144 log.info("invokeWorkflow: {}", worklowDescJson);
jaegonkim6a7b5242018-09-12 23:09:42 +0900145 Workplace workplace = workplaceStore.getWorkplace(Workplace.SYSTEM_WORKPLACE);
146 if (Objects.isNull(workplace)) {
147 throw new WorkflowException("Invalid system workplace");
148 }
149
mohamedrahilr63a921c2019-02-27 19:48:25 +0530150 Workflow workflow = workflowStore.get(URI.create(worklowDescJson.get("id").asText()));
jaegonkim6a7b5242018-09-12 23:09:42 +0900151 if (Objects.isNull(workflow)) {
mohamedrahilr63a921c2019-02-27 19:48:25 +0530152 throw new WorkflowException("Invalid Workflow");
153 }
154
jaegonkim44628d62019-04-07 10:30:32 +0900155 checkWorkflowDataModelSchema(workflow, worklowDescJson);
mohamedrahilr63a921c2019-02-27 19:48:25 +0530156
157 Workflow wfCreationWf = workflowStore.get(URI.create(WorkplaceWorkflow.WF_CREATE_WORKFLOW));
158 if (Objects.isNull(wfCreationWf)) {
jaegonkim6a7b5242018-09-12 23:09:42 +0900159 throw new WorkflowException("Invalid workflow " + WorkplaceWorkflow.WF_CREATE_WORKFLOW);
160 }
161
mohamedrahilr63a921c2019-02-27 19:48:25 +0530162 WorkflowContext context = wfCreationWf.buildSystemContext(workplace, new JsonDataModelTree(worklowDescJson));
jaegonkim6a7b5242018-09-12 23:09:42 +0900163 workflowExecutionService.execInitWorklet(context);
164 }
165
mohamedrahilr63a921c2019-02-27 19:48:25 +0530166 /**
jaegonkim44628d62019-04-07 10:30:32 +0900167 * Checks the validity of workflow definition.
168 * @param workflow workflow to be checked
169 * @throws WorkflowException workflow exception
170 */
171 private void checkWorkflow(Workflow workflow) throws WorkflowException {
172
173 Map<String, WorkletDataModelFieldDesc> descMap = new HashMap<>();
174
175 List<String> errors = new ArrayList<>();
176
177 for (String workletType : workflow.getWorkletTypeList()) {
178
179 Worklet worklet = workflow.getWorkletInstance(workletType);
180 if (Worklet.Common.COMPLETED.equals(worklet) || Worklet.Common.INIT.equals(worklet)) {
181 continue;
182 }
183
184 Class cls = worklet.getClass();
185 for (Field field : cls.getDeclaredFields()) {
186
187 if (field.isSynthetic()) {
188 continue;
189 }
190
191 for (Annotation annotation : field.getAnnotations()) {
192
193 if (!(annotation instanceof JsonDataModel)) {
194 continue;
195 }
196
197 JsonDataModel jsonDataModel = (JsonDataModel) annotation;
198 Matcher matcher = Pattern.compile("(\\w+)").matcher(jsonDataModel.path());
199 if (!matcher.find()) {
200 throw new WorkflowException(
201 "Invalid Json Data Model Path(" + jsonDataModel.path() + ") in " + worklet.tag());
202 }
203 String path = matcher.group(1);
204
205 WorkletDataModelFieldDesc desc =
206 new WorkletDataModelFieldDesc(workletType, path, field.getType(), jsonDataModel.optional());
207
208 WorkletDataModelFieldDesc existing = descMap.get(path);
209
210 if (Objects.isNull(existing)) {
211 descMap.put(path, desc);
212 } else {
213 if (!desc.hasSameAttributes(existing)) {
214 errors.add("" + desc + " is conflicted with " + existing + " in workflow " + workflow.id());
215 }
216 }
217 }
218 }
219 }
220
221 if (!errors.isEmpty()) {
222 throw new WorkflowDefinitionException(workflow.id(), errors);
223 }
224 }
225
226 /**
227 * Description of worklet data model field.
228 */
229 private static class WorkletDataModelFieldDesc {
230
231 private final String workletType;
232
233 private final String path;
234
235 private final Class type;
236
237 private final boolean optional;
238
239 /**
240 * Constructor of worklet data model field description.
241 * @param workletType worklet type
242 * @param path path of data model
243 * @param type type of data model
244 * @param optional optional
245 */
246 public WorkletDataModelFieldDesc(String workletType, String path, Class type, boolean optional) {
247 this.workletType = workletType;
248 this.path = path;
249 this.type = type;
250 this.optional = optional;
251 }
252
253 /**
254 * Checks the attributes of worklet data model field.
255 * @param desc worklet data model description
256 * @return true means that this worklet data model field description has same attributes with desc
257 */
258 public boolean hasSameAttributes(WorkletDataModelFieldDesc desc) {
259
260 if (!Objects.equals(type, desc.type)) {
261 return false;
262 }
263
264 if (!Objects.equals(optional, desc.optional)) {
265 return false;
266 }
267
268 return true;
269 }
270
271 @Override
272 public String toString() {
273 return MoreObjects.toStringHelper(getClass())
274 .add("worklet", workletType)
275 .add("path", path)
276 .add("type", type)
277 .add("optional", optional)
278 .toString();
279 }
280 }
281
282 /**
283 * Checks the schema of workflow data.
mohamedrahilr63a921c2019-02-27 19:48:25 +0530284 *
285 * @param workflow workflow
jaegonkim13b25cb2019-03-22 06:23:23 +0900286 * @param worklowDescJson jsonNode
mohamedrahilr63a921c2019-02-27 19:48:25 +0530287 * @throws WorkflowException workflow exception
288 */
jaegonkim44628d62019-04-07 10:30:32 +0900289 private void checkWorkflowDataModelSchema(Workflow workflow, JsonNode worklowDescJson) throws WorkflowException {
mohamedrahilr63a921c2019-02-27 19:48:25 +0530290
jaegonkim13b25cb2019-03-22 06:23:23 +0900291 List<String> errors = new ArrayList<>();
mohamedrahilr63a921c2019-02-27 19:48:25 +0530292
jaegonkim13b25cb2019-03-22 06:23:23 +0900293 JsonNode dataNode = worklowDescJson.get("data");
294 if (Objects.isNull(dataNode) || dataNode instanceof MissingNode) {
295 errors.add("workflow description json does not have 'data'");
296 throw new WorkflowDataModelException(workflow.id(), worklowDescJson, errors);
297 }
298
mohamedrahilr63a921c2019-02-27 19:48:25 +0530299 for (String workletType : workflow.getWorkletTypeList()) {
jaegonkim13b25cb2019-03-22 06:23:23 +0900300
301 Worklet worklet = workflow.getWorkletInstance(workletType);
302 if (Worklet.Common.COMPLETED.equals(worklet) || Worklet.Common.INIT.equals(worklet)) {
mohamedrahilr63a921c2019-02-27 19:48:25 +0530303 continue;
304 }
jaegonkim13b25cb2019-03-22 06:23:23 +0900305
mohamedrahilr63a921c2019-02-27 19:48:25 +0530306 Class cls = worklet.getClass();
307 for (Field field : cls.getDeclaredFields()) {
jaegonkim13b25cb2019-03-22 06:23:23 +0900308
mohamedrahilr63a921c2019-02-27 19:48:25 +0530309 if (field.isSynthetic()) {
310 continue;
311 }
jaegonkim13b25cb2019-03-22 06:23:23 +0900312
313 for (Annotation annotation : field.getAnnotations()) {
314
jaegonkim44628d62019-04-07 10:30:32 +0900315 if (!(annotation instanceof JsonDataModel)) {
316 continue;
mohamedrahilr63a921c2019-02-27 19:48:25 +0530317 }
jaegonkim44628d62019-04-07 10:30:32 +0900318
319 JsonDataModel jsonDataModel = (JsonDataModel) annotation;
320 Matcher matcher = Pattern.compile("(\\w+)").matcher(jsonDataModel.path());
321 if (!matcher.find()) {
322 throw new WorkflowException(
323 "Invalid Json Data Model Path(" + jsonDataModel.path() + ") in " + worklet.tag());
324 }
325 String path = matcher.group(1);
326
327 Optional<String> optError =
328 getJsonNodeDataError(dataNode, worklet, field, path, jsonDataModel.optional());
329
330 if (optError.isPresent()) {
331 errors.add(optError.get());
332 }
333
mohamedrahilr63a921c2019-02-27 19:48:25 +0530334 }
335 }
jaegonkim13b25cb2019-03-22 06:23:23 +0900336 }
mohamedrahilr63a921c2019-02-27 19:48:25 +0530337
jaegonkim13b25cb2019-03-22 06:23:23 +0900338 if (!errors.isEmpty()) {
339 throw new WorkflowDataModelException(workflow.id(), worklowDescJson, errors);
mohamedrahilr63a921c2019-02-27 19:48:25 +0530340 }
mohamedrahilr63a921c2019-02-27 19:48:25 +0530341 }
342
jaegonkim13b25cb2019-03-22 06:23:23 +0900343 private Optional<String> getJsonNodeDataError(
344 JsonNode dataNode, Worklet worklet, Field field, String path, boolean isOptional) throws WorkflowException {
mohamedrahilr63a921c2019-02-27 19:48:25 +0530345
jaegonkim13b25cb2019-03-22 06:23:23 +0900346 // Checking the existence of path in dataNode
347 JsonNode pathNode = dataNode.get(path);
348 if (Objects.isNull(pathNode) || pathNode instanceof MissingNode) {
349
350 if (isOptional) {
351 return Optional.empty();
352
353 } else {
354 return Optional.of("data doesn't have '" + path + "' in worklet<" + worklet.tag() + ">");
mohamedrahilr63a921c2019-02-27 19:48:25 +0530355 }
mohamedrahilr63a921c2019-02-27 19:48:25 +0530356 }
jaegonkim13b25cb2019-03-22 06:23:23 +0900357
358 // Checking the type of path
359 JsonNodeType type = pathNode.getNodeType();
360
361 if (Objects.isNull(type)) {
362 throw new WorkflowException("Invalid type for " + pathNode);
363 }
364
365 switch (type) {
366 case NUMBER:
367 if (!(field.getType().isAssignableFrom(Integer.class))) {
368 return Optional.of("'" + path + "<NUMBER>' cannot be assigned to " +
369 field.getName() + "<" + field.getType() + "> in worklet<" + worklet.tag() + ">");
370 }
371 break;
372 case STRING:
373 if (!(field.getType().isAssignableFrom(String.class))) {
374 return Optional.of("'" + path + "<STRING>' cannot be assigned to " +
375 field.getName() + "<" + field.getType() + "> in worklet<" + worklet.tag() + ">");
376 }
377 break;
378 case BOOLEAN:
379 if (!(field.getType().isAssignableFrom(Boolean.class))) {
380 return Optional.of("'" + path + "<BOOLEAN>' cannot be assigned to " +
381 field.getName() + "<" + field.getType() + "> in worklet<" + worklet.tag() + ">");
382 }
383 break;
384 case OBJECT:
385 if (!(field.getType().isAssignableFrom(JsonNode.class))) {
386 return Optional.of("'" + path + "<OBJECT>' cannot be assigned to " +
387 field.getName() + "<" + field.getType() + "> in worklet<" + worklet.tag() + ">");
388 }
389 break;
390 case ARRAY:
391 if (!(field.getType().isAssignableFrom(ArrayNode.class))) {
392 return Optional.of("'" + path + "<ARRAY>' cannot be assigned to " +
393 field.getName() + "<" + field.getType() + "> in worklet<" + worklet.tag() + ">");
394 }
395 break;
396 default:
397 return Optional.of("'" + path + "<" + type + ">' is not supported");
398 }
399
400 return Optional.empty();
mohamedrahilr63a921c2019-02-27 19:48:25 +0530401 }
402
jaegonkim6a7b5242018-09-12 23:09:42 +0900403 @Override
404 public void terminateWorkflow(WorkflowDescription wfDesc) throws WorkflowException {
405 log.info("terminateWorkflow: {}", wfDesc);
406 if (Objects.nonNull(workplaceStore.getContext(wfDesc.workflowContextName()))) {
407 workplaceStore.removeContext(wfDesc.workflowContextName());
408 }
409 }
410}