From f3b8f846dec599050700b147f0e4824b54cf9d65 Mon Sep 17 00:00:00 2001 From: zm330 Date: Thu, 11 Mar 2021 16:49:23 +0800 Subject: Update NSI selection process support shared NSI and add sst parameter Issue-ID: SO-3381 Signed-off-by: zm330 Change-Id: I84f76e4e32fabc35fbd448ed1234d3427f89279d --- .../infrastructure/scripts/AAISliceUtil.groovy | 146 +++++++++++++++++++++ .../scripts/AllocateSliceSubnet.groovy | 3 + .../scripts/DoAllocateNSIandNSSI.groovy | 20 ++- .../scripts/DoCreateSliceServiceInstance.groovy | 1 + .../scripts/DoCreateSliceServiceOption.groovy | 35 +++-- .../scripts/DoCreateTnNssiInstance.groovy | 3 +- .../infrastructure/scripts/TnAllocateNssi.groovy | 3 + 7 files changed, 190 insertions(+), 21 deletions(-) create mode 100644 bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/AAISliceUtil.groovy (limited to 'bpmn/so-bpmn-infrastructure-common') diff --git a/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/AAISliceUtil.groovy b/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/AAISliceUtil.groovy new file mode 100644 index 0000000000..b2415e296f --- /dev/null +++ b/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/AAISliceUtil.groovy @@ -0,0 +1,146 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + # Copyright (c) 2019, CMCC Technologies Co., Ltd. + # + # Licensed under the Apache License, Version 2.0 (the "License") + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. + * ============LICENSE_END========================================================= + */ +package org.onap.so.bpmn.infrastructure.scripts + +import org.camunda.bpm.engine.delegate.BpmnError +import org.camunda.bpm.engine.delegate.DelegateExecution +import org.onap.aai.domain.yang.Relationship +import org.onap.aai.domain.yang.ServiceInstance +import org.onap.aaiclient.client.aai.AAIObjectName +import org.onap.aaiclient.client.aai.AAIResourcesClient +import org.onap.aaiclient.client.aai.entities.AAIResultWrapper +import org.onap.aaiclient.client.aai.entities.uri.AAIUriFactory +import org.onap.aaiclient.client.generated.fluentbuilders.AAIFluentTypeBuilder +import org.onap.so.bpmn.common.scripts.ExceptionUtil +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import javax.ws.rs.NotFoundException + +class AAISliceUtil { + private static final Logger LOGGER = LoggerFactory.getLogger(AAISliceUtil.class); + ExceptionUtil exceptionUtil = new ExceptionUtil() + /** + * Get NSSI Id from AAI + * @param execution + * @param nsiId + * @return + */ + List getNSSIIdList(DelegateExecution execution, String nsiId){ + List nssiIdList = [] + + try + { + String errorMsg = "query nssi from aai failed." + AAIResultWrapper wrapper = queryAAI(execution, AAIFluentTypeBuilder.Types.SERVICE_INSTANCE, nsiId, errorMsg) + Optional si = wrapper.asBean(ServiceInstance.class) + if(si.isPresent()) + { + List relationshipList = si.get().getRelationshipList()?.getRelationship() + for (Relationship relationship : relationshipList) + { + String relatedTo = relationship.getRelatedTo() + if (relatedTo == "service-instance") + { + String relatedLink = relationship.getRelatedLink()?:"" + String instanceId = relatedLink ? relatedLink.substring(relatedLink.lastIndexOf("/") + 1,relatedLink.length()) : "" + AAIResultWrapper wrapper1 = queryAAI(execution, AAIFluentTypeBuilder.Types.SERVICE_INSTANCE, instanceId, errorMsg) + Optional serviceInstance = wrapper1.asBean(ServiceInstance.class) + def nssiId + if (serviceInstance.isPresent()) { + ServiceInstance instance = serviceInstance.get() + if ("nssi".equalsIgnoreCase(instance.getServiceRole())) { + nssiId = instance.getServiceInstanceId() + nssiIdList.add(nssiId) + } + } + } + } + } + } + catch(BpmnError e){ + throw e + } + catch (Exception ex){ + String msg = "Exception in getNSIFromAAI " + ex.getMessage() + LOGGER.error(msg) + exceptionUtil.buildAndThrowWorkflowException(execution, 7000, msg) + } + return nssiIdList + } + + + /** + * get nssi service from AAI + * prepare list + * @param execution + */ + List getNSSIListFromAAI(DelegateExecution execution, List nssiIdList) + { + LOGGER.trace("***** Start getNSSIListFromAAI *****") + List nssiInstanceList = [] + String errorMsg = "query nssi list from aai failed" + for(String nssiId : nssiIdList){ + AAIResultWrapper wrapper = queryAAI(execution, AAIFluentTypeBuilder.Types.SERVICE_INSTANCE, nssiId, errorMsg) + Optional si =wrapper.asBean(ServiceInstance.class) + if(si.isPresent()){ + nssiInstanceList.add(si.get()) + } + } + LOGGER.trace(" ***** Exit getNSSIListFromAAI *****") + return nssiInstanceList + } + + + /** + * query AAI + * @param execution + * @param aaiObjectName + * @param instanceId + * @return AAIResultWrapper + */ + private AAIResultWrapper queryAAI(DelegateExecution execution, AAIObjectName aaiObjectName, String instanceId, String errorMsg) + { + LOGGER.trace(" ***** Start queryAAI *****") + String globalSubscriberId = execution.getVariable("globalSubscriberId") + String serviceType = execution.getVariable("serviceType") + + org.onap.aaiclient.client.generated.fluentbuilders.ServiceInstance serviceInstanceType = AAIFluentTypeBuilder.business().customer(globalSubscriberId).serviceSubscription(serviceType).serviceInstance(instanceId) + def type + if (aaiObjectName == AAIFluentTypeBuilder.Types.ALLOTTED_RESOURCE) { + type = serviceInstanceType.allottedResources() + } else if (aaiObjectName == AAIFluentTypeBuilder.Types.SLICE_PROFILES) { + type = serviceInstanceType.sliceProfiles() + } else { + type = serviceInstanceType + } + def uri = AAIUriFactory.createResourceUri(type) + if (!getAAIClient().exists(uri)) { + exceptionUtil.buildAndThrowWorkflowException(execution, 2500, errorMsg) + } + AAIResultWrapper wrapper = getAAIClient().get(uri, NotFoundException.class) + LOGGER.trace(" *****${PREFIX} Exit queryAAI *****") + return wrapper + } + + AAIResourcesClient getAAIClient(){ + return new AAIResourcesClient() + } +} diff --git a/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/AllocateSliceSubnet.groovy b/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/AllocateSliceSubnet.groovy index e2d9c16328..78cafa7be5 100644 --- a/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/AllocateSliceSubnet.groovy +++ b/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/AllocateSliceSubnet.groovy @@ -94,6 +94,9 @@ class AllocateSliceSubnet extends AbstractServiceTaskProcessor { String servicename = jsonUtil.getJsonValue(subnetInstanceReq, "name") execution.setVariable("servicename", servicename) + String sST = jsonUtil.getJsonValue(subnetInstanceReq, "sst") + execution.setVariable("sst", sST) + String nsiId = jsonUtil.getJsonValue(subnetInstanceReq, "additionalProperties.nsiInfo.nsiId") if (isBlank(nsiId)) { msg = "Input nsiId is null" diff --git a/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/DoAllocateNSIandNSSI.groovy b/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/DoAllocateNSIandNSSI.groovy index 059a209336..276b6f0500 100644 --- a/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/DoAllocateNSIandNSSI.groovy +++ b/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/DoAllocateNSIandNSSI.groovy @@ -263,9 +263,10 @@ class DoAllocateNSIandNSSI extends AbstractServiceTaskProcessor{ execution.getVariable("sliceTaskParams") as SliceTaskParamsAdapter SliceTaskInfo sliceTaskInfo = sliceParams.anSliceTaskInfo sliceTaskInfo.setSliceInstanceId(serviceInstanceId) + String sliceProfileName = "an_" + sliceParams.serviceName // create slice profile - ServiceInstance rspi = createSliceProfileInstance(sliceTaskInfo, oStatus) + ServiceInstance rspi = createSliceProfileInstance(sliceTaskInfo, sliceProfileName, oStatus) //timestamp format YYYY-MM-DD hh:mm:ss rspi.setCreatedAt(new Date(System.currentTimeMillis()).format("yyyy-MM-dd HH:mm:ss", TimeZone.getDefault())) @@ -338,10 +339,11 @@ class DoAllocateNSIandNSSI extends AbstractServiceTaskProcessor{ String routeId = UUID.randomUUID().toString() route.setRouteId(routeId) route.setType("endpoint") - route.setRole("an") + route.setRole("AN") route.setFunction("3gppTransportEP") route.setIpAddress( sliceTaskInfo.sliceProfile.ipAddress) route.setNextHop(sliceTaskInfo.sliceProfile.nextHopInfo) + route.setLogicalInterfaceId(sliceTaskInfo.sliceProfile.logicInterfaceId) route.setAddressFamily("ipv4") route.setPrefixLength(24) sliceTaskInfo.setEndPointId(routeId) @@ -363,10 +365,11 @@ class DoAllocateNSIandNSSI extends AbstractServiceTaskProcessor{ String routeId = UUID.randomUUID().toString() route.setRouteId(routeId) route.setType("endpoint") - route.setRole("cn") + route.setRole("CN") route.setFunction("3gppTransportEP") route.setIpAddress( sliceTaskInfo.sliceProfile.ipAddress) route.setNextHop(sliceTaskInfo.sliceProfile.nextHopInfo) + route.setLogicalInterfaceId(sliceTaskInfo.sliceProfile.logicInterfaceId) route.setAddressFamily("ipv4") route.setPrefixLength(24) @@ -454,9 +457,10 @@ class DoAllocateNSIandNSSI extends AbstractServiceTaskProcessor{ execution.getVariable("sliceTaskParams") as SliceTaskParamsAdapter SliceTaskInfo sliceTaskInfo = sliceParams.cnSliceTaskInfo sliceTaskInfo.setSliceInstanceId(serviceInstanceId) + String sliceProfileName = "cn_"+sliceParams.serviceName // create slice profile - ServiceInstance rspi = createSliceProfileInstance(sliceTaskInfo, oStatus) + ServiceInstance rspi = createSliceProfileInstance(sliceTaskInfo, sliceProfileName, oStatus) //timestamp format YYYY-MM-DD hh:mm:ss rspi.setCreatedAt(new Date(System.currentTimeMillis()).format("yyyy-MM-dd HH:mm:ss", TimeZone.getDefault())) @@ -597,10 +601,11 @@ class DoAllocateNSIandNSSI extends AbstractServiceTaskProcessor{ String serviceInstanceId = UUID.randomUUID().toString() sliceTaskInfo.setSliceInstanceId(serviceInstanceId) + String sliceProfileName = "tn_" + sliceParams.serviceName //execution.setVariable("cnSliceProfileInstanceId", serviceInstanceId) //todo: // create slice profile - ServiceInstance rspi = createSliceProfileInstance(sliceTaskInfo, oStatus) + ServiceInstance rspi = createSliceProfileInstance(sliceTaskInfo, sliceProfileName, oStatus) //timestamp format YYYY-MM-DD hh:mm:ss rspi.setCreatedAt(new Date(System.currentTimeMillis()).format("yyyy-MM-dd HH:mm:ss", TimeZone.getDefault())) @@ -701,6 +706,7 @@ class DoAllocateNSIandNSSI extends AbstractServiceTaskProcessor{ serviceInfo.serviceUuid = sliceTaskInfo.NSSTInfo.UUID serviceInfo.nssiId = sliceTaskInfo.suggestNssiId serviceInfo.sST = sliceTaskInfo.sliceProfile.sST ?: sliceParams.serviceProfile.get("sST") + serviceInfo.nssiName = "nssi_tn" + execution.getVariable("sliceServiceInstanceName") nbiRequest.setServiceInfo(serviceInfo) nbiRequest.setEsrInfo(esrInfo) @@ -923,10 +929,10 @@ class DoAllocateNSIandNSSI extends AbstractServiceTaskProcessor{ client.create(sourceInstanceUri, relationship) } - static def createSliceProfileInstance(SliceTaskInfo sliceTaskInfo, String oStatus) { + static def createSliceProfileInstance(SliceTaskInfo sliceTaskInfo, String sliceProfileName, String oStatus) { // create slice profile ServiceInstance rspi = new ServiceInstance() - rspi.setServiceInstanceName(sliceTaskInfo.NSSTInfo.name) + rspi.setServiceInstanceName(sliceProfileName) rspi.setServiceType(sliceTaskInfo.sliceProfile.getSST()) rspi.setServiceRole("slice-profile") rspi.setOrchestrationStatus(oStatus) diff --git a/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/DoCreateSliceServiceInstance.groovy b/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/DoCreateSliceServiceInstance.groovy index ccb04d9440..5476cb5afa 100644 --- a/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/DoCreateSliceServiceInstance.groovy +++ b/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/DoCreateSliceServiceInstance.groovy @@ -183,6 +183,7 @@ class DoCreateSliceServiceInstance extends AbstractServiceTaskProcessor{ serviceProfile.setUlThptPerSlice(Integer.parseInt(serviceProfileMap.get("uLThptPerSlice").toString())) serviceProfile.setUlThptPerUE(Integer.parseInt(serviceProfileMap.get("uLThptPerUE").toString())) serviceProfile.setActivityFactor(Integer.parseInt(serviceProfileMap.get("activityFactor").toString())) + serviceProfile.setMaxNumberOfConns(Integer.parseInt(serviceProfileMap.get("maxNumberofConns").toString())) serviceProfile.setJitter(Integer.parseInt(serviceProfileMap.get("jitter").toString())) serviceProfile.setSurvivalTime("0") diff --git a/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/DoCreateSliceServiceOption.groovy b/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/DoCreateSliceServiceOption.groovy index 25a7159264..b81347cc8c 100644 --- a/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/DoCreateSliceServiceOption.groovy +++ b/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/DoCreateSliceServiceOption.groovy @@ -22,6 +22,7 @@ package org.onap.so.bpmn.infrastructure.scripts import com.fasterxml.jackson.databind.ObjectMapper import org.camunda.bpm.engine.delegate.DelegateExecution +import org.onap.aai.domain.yang.ServiceInstance import org.onap.so.beans.nsmf.EsrInfo import org.onap.so.beans.nsmf.NetworkType import org.onap.so.beans.nsmf.NssmfAdapterNBIRequest @@ -55,6 +56,8 @@ class DoCreateSliceServiceOption extends AbstractServiceTaskProcessor{ OofUtils oofUtils = new OofUtils() + AAISliceUtil aaiSliceUtil = new AAISliceUtil() + private static final ObjectMapper objectMapper = new ObjectMapper() private NssmfAdapterUtils nssmfAdapterUtils = new NssmfAdapterUtils(httpClientFactory, jsonUtil) @@ -70,7 +73,7 @@ class DoCreateSliceServiceOption extends AbstractServiceTaskProcessor{ * prepare the params for decompose nst * @param execution */ - public void prepareDecomposeNST(DelegateExecution execution) { + void prepareDecomposeNST(DelegateExecution execution) { SliceTaskParamsAdapter sliceTaskParams = execution.getVariable("sliceTaskParams") as SliceTaskParamsAdapter @@ -346,7 +349,6 @@ class DoCreateSliceServiceOption extends AbstractServiceTaskProcessor{ } /** - * todo: need rewrite * process select nsi response * @param execution */ @@ -369,10 +371,7 @@ class DoCreateSliceServiceOption extends AbstractServiceTaskProcessor{ List> nsiSolutions = (List>) resMap.get("solutions") - Map solution = nsiSolutions.get(0) - - //String resourceSharingLevel = execution.getVariable("resourceSharingLevel") - //Boolean isSharable = resourceSharingLevel == "shared" + Map solution = nsiSolutions?.get(0) if (solution != null) { if (execution.getVariable("queryNsiFirst")) { @@ -384,7 +383,7 @@ class DoCreateSliceServiceOption extends AbstractServiceTaskProcessor{ } execution.setVariable("queryNsiFirst", false) } else { - processSharedNSI(solution, sliceTaskParams) + processSharedNSI(solution, sliceTaskParams, execution) execution.setVariable("needQuerySliceProfile", false) } } @@ -393,18 +392,28 @@ class DoCreateSliceServiceOption extends AbstractServiceTaskProcessor{ logger.debug("*** Completed options Call to OOF ***") } - private static void processSharedNSI(Map solution, SliceTaskParamsAdapter sliceParams) { + private void processSharedNSI(Map solution, SliceTaskParamsAdapter sliceParams, DelegateExecution execution) { Map sharedNSISolution = solution.get("sharedNSISolution") as Map String nsiId = sharedNSISolution.get("NSIId") String nsiName = sharedNSISolution.get("NSIName") sliceParams.setSuggestNsiId(nsiId) sliceParams.setSuggestNsiName(nsiName) + List nssiId = aaiSliceUtil.getNSSIIdList(execution,nsiId) + List nssiInstances = aaiSliceUtil.getNSSIListFromAAI(execution, nssiId) + List sliceProfiles = sharedNSISolution.get("sliceProfiles") as List handleSliceProfiles(sliceProfiles, sliceParams) + Map nssiSolution = new HashMap<>() + for(ServiceInstance instance: nssiInstances){ + nssiSolution.put("NSSIId", instance.getServiceInstanceId()) + nssiSolution.put("NSSIName", instance.getServiceInstanceName()) + processNssiResult(sliceParams, instance.getEnvironmentContext(), nssiSolution) + } + } - private static void processNewNSI(Map solution, SliceTaskParamsAdapter sliceParams) { + private void processNewNSI(Map solution, SliceTaskParamsAdapter sliceParams) { Map newNSISolution = solution.get("newNSISolution") as Map List sliceProfiles = newNSISolution.get("sliceProfiles") as List handleSliceProfiles(sliceProfiles, sliceParams) @@ -436,7 +445,7 @@ class DoCreateSliceServiceOption extends AbstractServiceTaskProcessor{ * get NSSI Selection Capability for AN * @param execution */ - public void getNSSISelectionCap4AN(DelegateExecution execution) { + void getNSSISelectionCap4AN(DelegateExecution execution) { def vendor = execution.getVariable("vendor") as String @@ -458,7 +467,7 @@ class DoCreateSliceServiceOption extends AbstractServiceTaskProcessor{ * get NSSI Selection Capability for TN * @param execution */ - public void getNSSISelectionCap4TN(DelegateExecution execution) { + void getNSSISelectionCap4TN(DelegateExecution execution) { def vendor = execution.getVariable("vendor") as String @@ -479,7 +488,7 @@ class DoCreateSliceServiceOption extends AbstractServiceTaskProcessor{ * get NSSI Selection Capability for CN * @param execution */ - public void getNSSISelectionCap4CN(DelegateExecution execution) { + void getNSSISelectionCap4CN(DelegateExecution execution) { def vendor = execution.getVariable("vendor") as String @@ -513,7 +522,7 @@ class DoCreateSliceServiceOption extends AbstractServiceTaskProcessor{ } /** - * if exist nssi need to select? + * if exist nssi need to select * @param execution */ public void handleNssiSelect(DelegateExecution execution) { diff --git a/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/DoCreateTnNssiInstance.groovy b/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/DoCreateTnNssiInstance.groovy index 78c6a084ef..9221067cce 100644 --- a/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/DoCreateTnNssiInstance.groovy +++ b/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/DoCreateTnNssiInstance.groovy @@ -111,6 +111,7 @@ class DoCreateTnNssiInstance extends AbstractServiceTaskProcessor { String serviceType = execution.getVariable("subscriptionServiceType") String ssInstanceId = execution.getVariable("sliceServiceInstanceId") String sliceProfileStr = execution.getVariable("sliceProfile") + String sst = execution.getVariable("sst") try { if (sliceProfileStr == null || sliceProfileStr.isEmpty()) { String msg = "ERROR: createServiceInstance: sliceProfile is null" @@ -126,7 +127,7 @@ class DoCreateTnNssiInstance extends AbstractServiceTaskProcessor { sliceInstanceName = ssInstanceId } ss.setServiceInstanceName(sliceInstanceName) - ss.setServiceType(serviceType) + ss.setServiceType(sst) String serviceStatus = "deactivated" ss.setOrchestrationStatus(serviceStatus) String modelInvariantUuid = execution.getVariable("modelInvariantUuid") diff --git a/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/TnAllocateNssi.groovy b/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/TnAllocateNssi.groovy index 019e836514..09bbb81b6b 100644 --- a/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/TnAllocateNssi.groovy +++ b/bpmn/so-bpmn-infrastructure-common/src/main/groovy/org/onap/so/bpmn/infrastructure/scripts/TnAllocateNssi.groovy @@ -87,6 +87,9 @@ class TnAllocateNssi extends AbstractServiceTaskProcessor { String sliceServiceInstanceName = execution.getVariable("servicename") execution.setVariable("sliceServiceInstanceName", sliceServiceInstanceName) + String sst = execution.getVariable("sst") + execution.setVariable("sst", sst) + //additional properties String sliceProfile = jsonUtil.getJsonValue(additionalPropJsonStr, "sliceProfile") if (isBlank(sliceProfile)) { -- cgit 1.2.3-korg