aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRitu Sood <Ritu.Sood@intel.com>2020-03-05 00:12:14 +0000
committerGerrit Code Review <gerrit@onap.org>2020-03-05 00:12:14 +0000
commit6efed4bbe6894b7f804485241ff88ee8549572b2 (patch)
tree599eef78193d5a961c127b66bf9e683d93b99b55
parent403455ec9a6486c75053bbde6370c528c0369c22 (diff)
parent5b191f3b1c5a050d048dad8beabd60a96085af60 (diff)
Merge "Updating AAI wit resource info : part3"
-rw-r--r--src/inventory/api/aaipullapi.go77
-rw-r--r--src/inventory/api/aaipullapi_test.go68
-rw-r--r--src/inventory/api/aaipushapi.go189
-rw-r--r--src/inventory/api/aaipushapi_test.go131
-rw-r--r--src/inventory/api/k8spluginapi.go120
-rw-r--r--src/inventory/api/k8spluginapi_test.go64
6 files changed, 649 insertions, 0 deletions
diff --git a/src/inventory/api/aaipullapi.go b/src/inventory/api/aaipullapi.go
new file mode 100644
index 00000000..c8e4ca38
--- /dev/null
+++ b/src/inventory/api/aaipullapi.go
@@ -0,0 +1,77 @@
+/*
+Copyright 2020 Tech Mahindra.
+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.
+*/
+
+package api
+
+import (
+ "crypto/tls"
+ "encoding/json"
+ con "github.com/onap/multicloud-k8s/src/inventory/constants"
+ log "github.com/onap/multicloud-k8s/src/inventory/logutils"
+ util "github.com/onap/multicloud-k8s/src/inventory/utils"
+ "io/ioutil"
+ "net/http"
+ "os"
+)
+
+func GetTenant(cloudOwner, cloudRegion string) string {
+
+ AAI_URI := os.Getenv("onap-aai")
+ AAI_Port := os.Getenv("aai-port")
+
+ http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+
+ apiToCR := AAI_URI + ":" + AAI_Port + con.AAI_EP + con.AAI_CREP + "cloud-region/" + cloudOwner + "/" + cloudRegion + "?depth=all"
+ req, err := http.NewRequest(http.MethodGet, apiToCR, nil)
+ if err != nil {
+ log.Error("Error while constructing request for Tenant API")
+ return
+
+ }
+
+ util.SetRequestHeaders(req)
+
+ client := http.DefaultClient
+ res, err := client.Do(req)
+
+ if err != nil {
+ log.Error("Error while executing request for Tenant API")
+ return
+ }
+
+ defer res.Body.Close()
+
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+
+ log.Error("Can't read Tenant response")
+ return
+
+ }
+
+ var tenant con.Tenant
+
+ json.Unmarshal([]byte(body), &tenant)
+
+ for k, v := range tenant.Tenants {
+ if k == "tenant" {
+ for _, val := range v {
+ return val.TenantId
+
+ }
+ }
+ }
+
+ return ""
+
+}
diff --git a/src/inventory/api/aaipullapi_test.go b/src/inventory/api/aaipullapi_test.go
new file mode 100644
index 00000000..4d838e38
--- /dev/null
+++ b/src/inventory/api/aaipullapi_test.go
@@ -0,0 +1,68 @@
+/*
+Copyright 2020 Tech Mahindra.
+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.
+*/
+
+package api
+
+import (
+ "crypto/tls"
+ con "github.com/onap/multicloud-k8s/src/inventory/constants"
+ util "github.com/onap/multicloud-k8s/src/inventory/utils"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "strings"
+ "testing"
+)
+
+func TestGetTenant(t *testing.T) {
+
+ cloudOwner := "CloudOwner"
+ cloudRegion := "RegionOne"
+
+ AAI_URI := os.Getenv("onap-aai")
+ AAI_Port := os.Getenv("aai-port")
+
+ http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+
+ uri := AAI_URI + ":" + AAI_Port + con.AAI_EP + con.AAI_CREP + "cloud-region/" + cloudOwner + "/" + cloudRegion + "?depth=all"
+
+ req, err := http.NewRequest("GET", uri, nil)
+
+ util.SetRequestHeaders(req)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ client := http.DefaultClient
+ res, err := client.Do(req)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if res.StatusCode != http.StatusOK {
+
+ t.Fail()
+ }
+
+ if p, err := ioutil.ReadAll(res.Body); err != nil {
+ t.Fail()
+ } else {
+ if strings.Contains(string(p), "Error") {
+ t.Errorf("header response shouldn't return error: %s", p)
+ } else if !strings.Contains(string(p), `tenant`) {
+ t.Errorf("header response doen't match:\n%s", p)
+ }
+ }
+}
diff --git a/src/inventory/api/aaipushapi.go b/src/inventory/api/aaipushapi.go
new file mode 100644
index 00000000..7faf498e
--- /dev/null
+++ b/src/inventory/api/aaipushapi.go
@@ -0,0 +1,189 @@
+/*
+Copyright 2020 Tech Mahindra.
+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.
+*/
+
+package api
+
+import (
+ "bytes"
+ "crypto/tls"
+ "encoding/json"
+ con "github.com/onap/multicloud-k8s/src/inventory/constants"
+ log "github.com/onap/multicloud-k8s/src/inventory/logutils"
+ util "github.com/onap/multicloud-k8s/src/inventory/utils"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "strconv"
+)
+
+/* Pushes each pod related details as vservers inot A&AI
+
+{
+ "vserver-id": "example20",
+ "vserver-name": "POD-NAME",
+ "vserver-name2": "Relese-name/Profile-name of the POD (Labels:release=profile-k8s)",
+ "prov-status": "NAMESPACEofthPOD",
+ "vserver-selflink": "example-vserver-selflink-val-57201",
+ "in-maint": true,
+ "is-closed-loop-disabled": true,
+ "l-interfaces": {
+ "l-interface": [{
+ "interface-name": "example-interface-name-val-20080",
+ "is-port-mirrored": true,
+ "in-maint": true,
+ "is-ip-unnumbered": true,
+ "l3-interface-ipv4-address-list": [{
+ "l3-interface-ipv4-address": "IP_Address",
+ "l3-interface-ipv4-prefix-length": "PORT"
+ }]
+ }]
+ }
+}
+
+*/
+func PushVservers(podInfo con.PodInfoToAAI, cloudOwner, cloudRegion, tenantId string) string {
+
+ AAI_URI := os.Getenv("onap-aai")
+ AAI_Port := os.Getenv("aai-port")
+
+ payload := "{\"vserver-name\":" + "\"" + podInfo.VserverName + "\"" + ", \"vserver-name2\":" + "\"" + podInfo.VserverName2 + "\"" + ", \"prov-status\":" + "\"" + podInfo.ProvStatus + "\"" + ",\"vserver-selflink\":" + "\"example-vserver-selflink-val-57201\", \"l-interfaces\": {\"l-interface\": [{\"interface-name\": \"example-interface-name-val-20080\",\"is-port-mirrored\": true,\"in-maint\": true,\"is-ip-unnumbered\": true,\"l3-interface-ipv4-address-list\": [{\"l3-interface-ipv4-address\":" + "\"" + podInfo.I3InterfaceIPv4Address + "\"" + ",\"l3-interface-ipv4-prefix-length\":" + "\"" + strconv.FormatInt(int64(podInfo.I3InterfaceIPvPrefixLength), 10) + "\"" + "}]}]}}"
+
+ http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+
+ url := AAI_URI + ":" + AAI_Port + con.AAI_EP + "cloud-region/" + cloudOwner + "/" + cloudRegion + "/tenants/tenant/" + tenantId + "/vservers/vserver/" + podInfo.VserverName
+
+ var jsonStr = []byte(payload)
+
+ req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonStr))
+
+ if err != nil {
+ log.Error("Error while constructing Vserver PUT request")
+ return
+ }
+
+ util.SetRequestHeaders(req)
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ log.Error("Error while executing Vserver PUT api")
+ return
+ }
+ defer resp.Body.Close()
+
+ return podInfo.VserverName
+}
+
+/* This links vservers to vf-module request payload */
+func LinkVserverVFM(vnfID, vfmID, cloudOwner, cloudRegion, tenantId string, relList []con.RelationList) {
+
+ AAI_URI := os.Getenv("onap-aai")
+ AAI_Port := os.Getenv("aai-port")
+
+ http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+
+ apiToCR := AAI_URI + ":" + AAI_Port + con.AAI_EP + con.AAI_NEP + "/" + vnfID + "/vf-modules"
+ req, err := http.NewRequest(http.MethodGet, apiToCR, nil)
+ if err != nil {
+ log.Error("Error while constructing VFModules GET api request")
+ return
+
+ }
+
+ util.SetRequestHeaders(req)
+
+ client := http.DefaultClient
+ res, err := client.Do(req)
+
+ if err != nil {
+ log.Error("Error while executing VFModules GET api")
+ return
+ }
+
+ defer res.Body.Close()
+
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+
+ log.Error("Error while reading vfmodules API response")
+ return
+
+ }
+
+ var vfmodules con.VFModules
+
+ json.Unmarshal([]byte(body), &vfmodules)
+
+ vfmList := vfmodules.VFModules
+
+ for key, vfmodule := range vfmList {
+
+ if vfmodule.VFModuleId == vfmID {
+
+ vfmodule.RelationshipList = map[string][]con.RelationList{"relationship": relList}
+
+ vfmList = append(vfmList, vfmodule)
+
+ vfmList[key] = vfmList[len(vfmList)-1] // Copy last element to index i.
+ vfmList = vfmList[:len(vfmList)-1]
+
+ //update vfmodule with vserver data
+
+ vfmPayload, err := json.Marshal(vfmodule)
+
+ if err != nil {
+
+ log.Error("Error while marshalling vfmodule linked vserver info response")
+ return
+
+ }
+
+ pushVFModuleToAAI(string(vfmPayload), vfmID, vnfID)
+ }
+
+ }
+
+}
+
+/* Pushes vf-module enriched with vserver information */
+func pushVFModuleToAAI(vfmPayload, vfmID, vnfID string) {
+
+ AAI_URI := os.Getenv("onap-aai")
+ AAI_Port := os.Getenv("aai-port")
+
+ http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+ url := AAI_URI + ":" + AAI_Port + con.AAI_NEP + vnfID + "/vfmodules/vf-module/" + vfmID
+
+ var jsonStr = []byte(vfmPayload)
+
+ req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonStr))
+
+ if err != nil {
+ log.Error("Error while constructing a VFModule request to AAI")
+ return
+ }
+
+ util.SetRequestHeaders(req)
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+
+ log.Error("Error while executing PUT request of VFModule to AAI")
+ return
+
+ }
+
+ defer resp.Body.Close()
+
+}
diff --git a/src/inventory/api/aaipushapi_test.go b/src/inventory/api/aaipushapi_test.go
new file mode 100644
index 00000000..6c0dea3f
--- /dev/null
+++ b/src/inventory/api/aaipushapi_test.go
@@ -0,0 +1,131 @@
+/*
+Copyright 2020 Tech Mahindra.
+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.
+*/
+
+package api
+
+import (
+ "bytes"
+ "crypto/tls"
+ con "github.com/onap/multicloud-k8s/src/inventory/constants"
+ util "github.com/onap/multicloud-k8s/src/inventory/utils"
+ "net/http"
+ "os"
+ "testing"
+)
+
+func TestPushVservers(t *testing.T) {
+
+ cloudOwner := "CloudOwner"
+ cloudRegion := "RegionOne"
+ tenantId := "tenant123"
+
+ AAI_URI := os.Getenv("onap-aai")
+ AAI_Port := os.Getenv("aai-port")
+
+ payload := "{\"vserver-name\":" + "\"" + "pod123" + "\"" + ", \"vserver-name2\":" + "\"" + "profile123" + "\"" + ", \"prov-status\":" + "\"" + "default" + "\"" + ",\"vserver-selflink\":" + "\"example-vserver-selflink-val-57201\", \"l-interfaces\": {\"l-interface\": [{\"interface-name\": \"example-interface-name-val-20080\",\"is-port-mirrored\": true,\"in-maint\": true,\"is-ip-unnumbered\": true,\"l3-interface-ipv4-address-list\": [{\"l3-interface-ipv4-address\":" + "\"" + "10.214.22.220" + "\"" + ",\"l3-interface-ipv4-prefix-length\":" + "\"" + "667" + "\"" + "}]}]}}"
+
+ http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+
+ url := AAI_URI + ":" + AAI_Port + con.AAI_EP + "cloud-region/" + cloudOwner + "/" + cloudRegion + "/tenants/tenant/" + tenantId + "/vservers/vserver/" + "pod123"
+
+ var jsonStr = []byte(payload)
+
+ req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonStr))
+
+ if err != nil {
+
+ t.Error("Failed: Error consructing request")
+ }
+
+ util.SetRequestHeaders(req)
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+
+ t.Error("Failed: Error while executing request ")
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ t.Fatalf("recieved unexpeted response ")
+ }
+
+}
+
+func TestLinkVserverVFM(t *testing.T) {
+
+ vnfID := "vnf123456"
+ AAI_URI := os.Getenv("onap-aai")
+ AAI_Port := os.Getenv("aai-port")
+
+ http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+
+ apiToCR := AAI_URI + ":" + AAI_Port + con.AAI_EP + con.AAI_NEP + "/" + vnfID + "/vf-modules"
+ req, err := http.NewRequest(http.MethodGet, apiToCR, nil)
+ if err != nil {
+
+ t.Error("Failed: Error while constructing new request")
+
+ }
+
+ util.SetRequestHeaders(req)
+
+ client := http.DefaultClient
+ res, err := client.Do(req)
+
+ if err != nil {
+
+ t.Error("Failed: Error while executing request")
+ }
+
+ if res.StatusCode != http.StatusOK {
+ t.Fatalf("recieved unexpeted response ")
+ }
+
+}
+
+func TestPushVFModuleToAAI(t *testing.T) {
+
+ vnfID := "vnf123456"
+ vfmID := "vfm123456"
+ vfmPayload := ""
+
+ AAI_URI := os.Getenv("onap-aai")
+ AAI_Port := os.Getenv("aai-port")
+
+ http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+ url := AAI_URI + ":" + AAI_Port + con.AAI_NEP + vnfID + "/vfmodules/vf-module/" + vfmID
+
+ var jsonStr = []byte(vfmPayload)
+
+ req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonStr))
+
+ if err != nil {
+ t.Error("Failed: Error while executing request")
+
+ }
+
+ util.SetRequestHeaders(req)
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+
+ t.Error("Failed: Error while executing request")
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ t.Fatalf("recieved unexpeted response ")
+ }
+
+}
diff --git a/src/inventory/api/k8spluginapi.go b/src/inventory/api/k8spluginapi.go
new file mode 100644
index 00000000..4348d287
--- /dev/null
+++ b/src/inventory/api/k8spluginapi.go
@@ -0,0 +1,120 @@
+/*
+Copyright 2020 Tech Mahindra.
+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.
+*/
+
+package api
+
+import (
+ con "github.com/onap/multicloud-k8s/src/inventory/constants"
+ log "github.com/onap/multicloud-k8s/src/inventory/logutils"
+ utils "github.com/onap/multicloud-k8s/src/inventory/utils"
+ k8sint "github.com/onap/multicloud-k8s/src/k8splugin/internal/app"
+ k8scon "github.com/onap/multicloud-k8s/src/k8splugin/internal/connection"
+
+ "encoding/json"
+ "net/http"
+ "os"
+)
+
+func ListInstances() []string {
+
+ MK8S_URI := os.Getenv("onap-multicloud-k8s")
+ MK8S_Port := os.Getenv("multicloud-k8s-port")
+
+ instancelist := MK8S_URI + ":" + MK8S_Port + con.MK8S_EP
+ req, err := http.NewRequest(http.MethodGet, instancelist, nil)
+ if err != nil {
+
+ log.Error("Something went wrong while listing resources - contructing request")
+ return
+ }
+
+ client := http.DefaultClient
+ res, err := client.Do(req)
+
+ if err != nil {
+ log.Error("Something went wrong while listing resources - executing request")
+ return
+ }
+
+ defer res.Body.Close()
+
+ decoder := json.NewDecoder(res.Body)
+ var rlist []k8sint.InstanceMiniResponse
+ err = decoder.Decode(&rlist)
+
+ resourceList := utils.ParseListInstanceResponse(rlist)
+
+ return resourceList
+
+}
+
+func GetConnection(cregion string) k8scon.Connection {
+
+ MK8S_URI := os.Getenv("onap-multicloud-k8s")
+ MK8S_Port := os.Getenv("multicloud-k8s-port")
+
+ instancelist := MK8S_URI + ":" + MK8S_Port + con.MK8S_CEP + cregion
+ req, err := http.NewRequest(http.MethodGet, instancelist, nil)
+ if err != nil {
+
+ log.Error("Something went wrong while getting Connection resource - contructing request")
+ return
+ }
+
+ client := http.DefaultClient
+ res, err := client.Do(req)
+
+ if err != nil {
+ log.Error("Something went wrong while getting Connection resource - executing request")
+ return
+ }
+
+ defer res.Body.Close()
+
+ decoder := json.NewDecoder(res.Body)
+ var connection k8scon.Connection
+ err = decoder.Decode(&connection)
+
+ return connection
+
+}
+
+func CheckStatusForEachInstance(instanceID string) k8sint.InstanceStatus {
+
+ MK8S_URI := os.Getenv("onap-multicloud-k8s")
+ MK8S_Port := os.Getenv("multicloud-k8s-port")
+
+ instancelist := MK8S_URI + ":" + MK8S_Port + con.MK8S_EP + instanceID + "/status"
+
+ req, err := http.NewRequest(http.MethodGet, instancelist, nil)
+ if err != nil {
+ log.Error("Error while checking instance status - building http request")
+ return
+ }
+
+ client := http.DefaultClient
+ resp, err := client.Do(req)
+ if err != nil {
+
+ log.Error("Error while checking instance status - making rest request")
+ return
+ }
+
+ defer resp.Body.Close()
+
+ decoder := json.NewDecoder(resp.Body)
+ var instStatus k8sint.InstanceStatus
+ err = decoder.Decode(&instStatus)
+
+ return instStatus
+}
diff --git a/src/inventory/api/k8spluginapi_test.go b/src/inventory/api/k8spluginapi_test.go
new file mode 100644
index 00000000..6eb16486
--- /dev/null
+++ b/src/inventory/api/k8spluginapi_test.go
@@ -0,0 +1,64 @@
+/*
+Copyright 2020 Tech Mahindra.
+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.
+*/
+
+package api
+
+import (
+ "crypto/tls"
+ con "github.com/onap/multicloud-k8s/src/inventory/constants"
+ util "github.com/onap/multicloud-k8s/src/inventory/utils"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "strings"
+ "testing"
+)
+
+func TestListInstances(t *testing.T) {
+
+ http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+
+ MK8S_URI := os.Getenv("onap-multicloud-k8s")
+ MK8S_Port := os.Getenv("multicloud-k8s-port")
+
+ instancelist := MK8S_URI + ":" + MK8S_Port + con.MK8S_EP
+ req, err := http.NewRequest(http.MethodGet, instancelist, nil)
+
+ util.SetRequestHeaders(req)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ client := http.DefaultClient
+ res, err := client.Do(req)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if res.StatusCode != http.StatusOK {
+
+ t.Fail()
+ }
+
+ if p, err := ioutil.ReadAll(res.Body); err != nil {
+ t.Fail()
+ } else {
+ if strings.Contains(string(p), "Error") {
+ t.Errorf("header response shouldn't return error: %s", p)
+ } else if !strings.Contains(string(p), `cloud-region`) {
+ t.Errorf("header response doen't match:\n%s", p)
+ }
+ }
+}