added ADAPT DO M24

parent 59e49e4a
The plan is to release the ADAPT Deployment Orchestrator component, developed by HPE, as open source software. HPE has to follow an internal process with reviews and decisions at corporate level to decide and approve the license under which to release the developed software. Unfortunately this process takes time and it’s not yet completed at the time of writing, therefore the licensing information for the released software is not yet available.
For more information please contact us through this website https://www.decide-h2020.eu/contact
\ No newline at end of file
FROM tiangolo/uwsgi-nginx-flask:python3.6
RUN apt-get update && apt-get install unzip && wget https://releases.hashicorp.com/terraform/0.10.7/terraform_0.10.7_linux_amd64.zip?_ga=2.121414664.102068769.1507033863-2054770415.1501495729 -O temp.zip && unzip temp.zip -d /usr/local/bin && rm temp.zip && mkdir -p /app/repo && mkdir -p /home/ubuntu/terraform/certs && mkdir -p /home/ubuntu/terraform/scripts && mkdir /home/ubuntu/terraform/keypairs && pip install flask-restplus && pip install -U flask-cors && pip install pymongo==3.7 && wget http://security.debian.org/debian-security/pool/updates/main/o/openssl/libssl1.0.0_1.0.1t-1+deb8u9_amd64.deb && dpkg -i libssl1.0.0_1.0.1t-1+deb8u9_amd64.deb && wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-4.0.0.tgz && tar -zxvf mongodb-linux-*-4.0.0.tgz && mkdir -p /data/db && export PATH=mongodb-linux-x86_64-ubuntu1604-4.0.0/bin:$PATH && cp mongodb-linux-x86_64-ubuntu1604-4.0.0/bin/* /usr/local/bin
RUN apt-get update && apt-get install unzip && apt-get install -y vim && wget https://releases.hashicorp.com/terraform/0.10.7/terraform_0.10.7_linux_amd64.zip?_ga=2.121414664.102068769.1507033863-2054770415.1501495729 -O temp.zip && unzip temp.zip -d /usr/local/bin && rm temp.zip && mkdir -p /app/repo && mkdir -p /home/ubuntu/terraform/certs && mkdir -p /home/ubuntu/terraform/scripts && mkdir /home/ubuntu/terraform/keypairs && pip install flask-restplus && pip install -U flask-cors && pip install pymongo==3.7 && pip install jsonschema==3.0.0a3 && wget http://security.debian.org/debian-security/pool/updates/main/o/openssl/libssl1.0.0_1.0.1t-1+deb8u9_amd64.deb && dpkg -i libssl1.0.0_1.0.1t-1+deb8u9_amd64.deb && wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-4.0.0.tgz && tar -zxvf mongodb-linux-*-4.0.0.tgz && mkdir -p /data/db && export PATH=mongodb-linux-x86_64-ubuntu1604-4.0.0/bin:$PATH && cp mongodb-linux-x86_64-ubuntu1604-4.0.0/bin/* /usr/local/bin
COPY app/ /app/
COPY tfplugin/terraform-provider-cloudbroker /usr/local/bin
......
......@@ -11,6 +11,9 @@ from threading import Thread
from flask_restplus import Resource, Api, fields, reqparse
from stat import *
from flask_cors import CORS
from jsonschema import validate
from jsonschema import Draft7Validator
from pymongo import MongoClient
import bson.objectid
from bson.json_util import dumps
......@@ -43,6 +46,10 @@ env_fields = api.model('EnvModel', {
'monitoring_port': fields.String(description='The ADAPT Monitoring REST Api endpoint port', required=True),
})
schemaFile = open('schemas/application_description.schema.json', 'r')
schemaContent = schemaFile.read()
appDescriptionSchema = json.loads(schemaContent)
@app.route("/")
def main():
index_path = os.path.join(app.static_folder, 'index.html')
......@@ -112,6 +119,11 @@ class Configuration(Resource):
data = {}
body = request.get_json()
print(body)
repo_user = body["repository_user"]
repo_pwd = body["repository_pwd"]
repo_url = body["repository_url"]
repo_url = repo_url[8:]
repo_url = "https://"+urllib.parse.quote(repo_user)+":"+urllib.parse.quote(repo_pwd)+"@"+repo_url
if not "repository_url" in body or not "filepath" in body or not body["repository_user"] or not body["repository_pwd"]:
data["result"] = "error"
data["resultCode"] = "400"
......@@ -122,33 +134,43 @@ class Configuration(Resource):
repository = body["repository_url"].split('/')[-1]
repository = repository[:repository.index('.git')]
filePath = body["filepath"]
if not os.path.exists("repo/"+repository):
print("Cloning repository: " + body["repository_url"])
if "branch" in body:
branch = body["branch"]
result = subprocess.check_output(['git', 'clone', repo_url, '-b', branch], cwd="repo/")
else:
result = subprocess.check_output(['git', 'clone', repo_url], cwd="repo/")
print("Pulling repository: " + body["repository_url"])
result = subprocess.check_output(['git', 'pull'], cwd="repo/"+repository)
if "revision" in body:
revision = body["revision"]
else:
revision = "HEAD"
if not os.path.exists("repo/"+repository):
print("Clone repository: " + body["repository_url"])
repo_user = body["repository_user"]
repo_pwd = body["repository_pwd"]
repo_url = body["repository_url"]
repo_url = repo_url[8:]
repo_url = "https://"+urllib.parse.quote(repo_user)+":"+urllib.parse.quote(repo_pwd)+"@"+repo_url
print("repo_url for clone: " + repo_url)
result = subprocess.check_output(['git', 'clone', repo_url], cwd="repo/")
print("Getting revision " + revision + " for file " + filePath + " from repository " + repository + " (" + body["repository_url"] + ")")
result = subprocess.check_output(['git', 'pull'], cwd="repo/"+repository)
result = subprocess.check_output(['git', 'checkout', '-m', revision, filePath], cwd="repo/"+repository)
file = open('repo/'+repository+"/"+filePath, 'r')
content = file.read()
print("Creating config for " + environment + " with input file:")
print(json.loads(content))
jsonContent = json.loads(content)
try:
jsonContent = json.loads(content)
print(">>>>>>>>>>>>>>>>Validating content against schema")
#validate(jsonContent, appDescriptionSchema)
Draft7Validator(appDescriptionSchema).is_valid(jsonContent)
except:
print("*************Validation error!****************", sys.exc_info()[0], sys.exc_info()[1])
if "cloudbroker_username" in body and "cloudbroker_password" in body and "cloudbroker_endpoint" in body and body["cloudbroker_username"] and body["cloudbroker_password"] and body["cloudbroker_endpoint"]:
jsonContent["cloudbrokerEndpoint"] = body["cloudbroker_endpoint"]
jsonContent["cloudbrokerUsername"] = body["cloudbroker_username"]
jsonContent["cloudbrokerPassword"] = body["cloudbroker_password"]
print("Changed credentials!")
if "privateClouds" in body and body["privateClouds"]:
jsonContent["privateClouds"] = body["privateClouds"]
data["result"] = "success"
data["resultCode"] = "202"
......@@ -260,16 +282,22 @@ class terraformOperation(Resource):
repository = body["repository_url"].split('/')[-1]
repository = repository[:repository.index('.git')]
filePath = body["filepath"]
if not os.path.exists("repo/"+repository):
print("Cloning repository: " + body["repository_url"])
if "branch" in body:
branch = body["branch"]
result = subprocess.check_output(['git', 'clone', repo_url, '-b', branch], cwd="repo/")
else:
result = subprocess.check_output(['git', 'clone', repo_url], cwd="repo/")
print("Pulling repository: " + body["repository_url"])
result = subprocess.check_output(['git', 'pull'], cwd="repo/"+repository)
if "revision" in body:
revision = body["revision"]
else:
revision = "HEAD"
if not os.path.exists("repo/"+repository):
print("Clone repository: " + body["repository_url"])
result = subprocess.check_output(['git', 'clone', repo_url], cwd="repo/")
print("Getting revision " + revision + " for file " + filePath + " from repository " + repository + " (" + body["repository_url"] + ")")
result = subprocess.check_output(['git', 'pull'], cwd="repo/"+repository)
result = subprocess.check_output(['git', 'checkout', '-m', revision, filePath], cwd="repo/"+repository)
with open('repo/'+repository+"/"+filePath, 'r') as ad:
content = ad.read()
print("Got application descriptor:")
......@@ -287,7 +315,7 @@ class terraformOperation(Resource):
nodeName = p['dockerHostNodeName']
nodeIpLabel = nodeName+".external_ip_address"
vm_ip = subprocess.check_output(['terraform', 'output', nodeIpLabel], cwd=appName+"/"+folder).decode(encoding='UTF-8').strip()
if result:
if vm_ip:
print("Got ip: " + vm_ip + " for node " + nodeName )
p['dockerHostPublicIp'] = vm_ip
status_urls.append(request.url_root+"vm/"+vm_ip+"/status")
......@@ -566,7 +594,7 @@ class adaptVmIpList(Resource):
except:
print("Unexpected error:", sys.exc_info()[0], sys.exc_info()[1])
respData["resultCode"] = "404"
respData["result"] = "Unexpected error: " + str(e)
respData["result"] = "Unexpected error: %s %s" % (sys.exc_info()[0], sys.exc_info()[1])
response = make_response(json.dumps(respData))
response.status_code = 404
finally:
......@@ -594,7 +622,7 @@ class adaptVmIpList(Resource):
except:
print("Unexpected error:", sys.exc_info()[0])
respData["resultCode"] = "404"
respData["result"] = "Unexpected error: " + str(e)
respData["result"] = "Unexpected error: %s %s" % (sys.exc_info()[0], sys.exc_info()[1])
response = make_response(json.dumps(respData))
response.status_code = 404
finally:
......@@ -615,14 +643,15 @@ class adaptVmStatus(Resource):
adaptDoDb = client['adapt-do-db']
vmStatusTable = adaptDoDb.vmStatus
db_result = vmStatusTable.update_one({"vmPublicIp": vmIp}, {'$set': jsonContent}, upsert=True)
print("POST operation: %s record(s) matched, %s record(s) modified, upserted id: %s" % (
db_result.matched_count, db_result.modified_count, db_result.upserted_id))
response = make_response(jsonContent)
responseStr = "POST operation: %s record(s) matched, %s record(s) modified, upserted id: %s" % (
db_result.matched_count, db_result.modified_count, db_result.upserted_id)
print(responseStr)
response = make_response("vm status saved: " + json.dumps(jsonContent))
response.status_code = 200
except:
print("Unexpected error:", sys.exc_info()[0], sys.exc_info()[1])
respData["resultCode"] = "400"
respData["result"] = "Unexpected error: " + str(e)
respData["result"] = "Unexpected error: %s %s" % (sys.exc_info()[0], sys.exc_info()[1])
response = make_response(json.dumps(respData))
response.status_code = 400
finally:
......@@ -646,7 +675,7 @@ class adaptVmStatus(Resource):
except:
print("Unexpected error:", sys.exc_info()[0], sys.exc_info()[1])
respData["resultCode"] = "400"
respData["result"] = "Unexpected error: " + str(e)
respData["result"] = "Unexpected error: %s %s" % (sys.exc_info()[0], sys.exc_info()[1])
response = make_response(json.dumps(respData))
response.status_code = 400
finally:
......@@ -807,6 +836,7 @@ def render_template(template_filename, context):
def create_infrastructure_tf(data_loaded):
appName = data_loaded["name"]
consulJoinIp = data_loaded["consulJoinIp"]
infrastructure_dir = appName+"/infrastructure"
service_dir = appName+"/services"
if not os.path.exists(appName):
......@@ -816,37 +846,69 @@ def create_infrastructure_tf(data_loaded):
if not os.path.exists(service_dir):
os.makedirs(service_dir)
cloudbrokerEndpoint = data_loaded["cloudbrokerEndpoint"]
cloudbrokerUsername = data_loaded["cloudbrokerUsername"]
cloudbrokerPassword = data_loaded["cloudbrokerPassword"]
privateClouds = {}
if 'privateClouds' in data_loaded:
for privateCloud in data_loaded["privateClouds"]:
privateClouds[privateCloud["id"]] = privateCloud
for element in data_loaded["virtualMachines"]:
print(element)
fname = infrastructure_dir+"/"+appName+"-vm-"+element["dockerHostNodeName"]+".tf"
context = {
'appName': appName,
'adaptHost': request.url_root,
#'docker_host_ip': element["docker-host-ip"],
'dockerHostNodeName': element["dockerHostNodeName"],
'cloudbrokerEndpoint': cloudbrokerEndpoint,
'cloudbrokerUsername': cloudbrokerUsername,
'cloudbrokerPassword': cloudbrokerPassword,
'vmSoftwareId': element["vmSoftwareId"],
'vmResourceId': element["vmResourceId"],
'vmRegionId': element["vmRegionId"],
'instanceTypeId': element["instanceTypeId"],
'keyPairId': element["keyPairId"],
'openedPort': element["openedPort"],
'consulJoinIp': element["consulJoinIp"],
}
if 'dockerPrivateRegistryIp' in element:
context['dockerPrivateRegistryIp'] = element["dockerPrivateRegistryIp"]
if 'dockerPrivateRegistryPort' in element:
context['dockerPrivateRegistryPort'] = element["dockerPrivateRegistryPort"]
with open(fname, 'w') as f:
tf = render_template('vm-tpl.tf', context)
f.write(tf)
if 'onPrivateCloud' in element:
privateCloud = privateClouds[element["privateCloudId"]]
context = {
'privateCloudUserName': privateCloud["user_name"],
'privateCloudTenantName': privateCloud["tenant_name"],
'privateCloudPassword': privateCloud["password"],
'privateCloudAuthUrl': privateCloud["auth_url"],
'privateCloudRegion': privateCloud["region"],
'appName': appName,
'consulJoinIp': consulJoinIp,
'adaptHost': request.url_root,
#'docker_host_ip': element["docker-host-ip"],
'dockerHostNodeName': element["dockerHostNodeName"],
'vmUser': element["vmUser"],
'keyPair': element["keyPair"],
'imageId': element["imageId"],
'flavorId': element["flavorId"],
'securityGroups': element["securityGroups"],
'networkName': element["networkName"],
'floatingIpPoolName': element["floatingIpPoolName"]
}
if 'dockerPrivateRegistryIp' in element:
context['dockerPrivateRegistryIp'] = element["dockerPrivateRegistryIp"]
if 'dockerPrivateRegistryPort' in element:
context['dockerPrivateRegistryPort'] = element["dockerPrivateRegistryPort"]
with open(fname, 'w') as f:
tf = render_template('vm-openstack-tpl.tf', context)
f.write(tf)
else:
context = {
'appName': appName,
'consulJoinIp': consulJoinIp,
'adaptHost': request.url_root,
#'docker_host_ip': element["docker-host-ip"],
'dockerHostNodeName': element["dockerHostNodeName"],
'cloudbrokerEndpoint': data_loaded["cloudbrokerEndpoint"],
'cloudbrokerUsername': data_loaded["cloudbrokerUsername"],
'cloudbrokerPassword': data_loaded["cloudbrokerPassword"],
'vmSoftwareId': element["vmSoftwareId"],
'vmResourceId': element["vmResourceId"],
'vmRegionId': element["vmRegionId"],
'vmUser': element["vmUser"],
'instanceTypeId': element["instanceTypeId"],
'keyPairId': element["keyPairId"],
'openedPort': element["openedPort"],
}
if 'dockerPrivateRegistryIp' in element:
context['dockerPrivateRegistryIp'] = element["dockerPrivateRegistryIp"]
if 'dockerPrivateRegistryPort' in element:
context['dockerPrivateRegistryPort'] = element["dockerPrivateRegistryPort"]
with open(fname, 'w') as f:
tf = render_template('vm-tpl.tf', context)
f.write(tf)
......@@ -861,6 +923,7 @@ def get_docker_host_node_name(container_name, containers):
def create_services_tf(data_loaded):
appName = data_loaded["name"]
consulJoinIp = data_loaded["consulJoinIp"]
infrastructure_dir = appName+"/infrastructure"
services_dir = appName+"/services"
......@@ -873,17 +936,25 @@ def create_services_tf(data_loaded):
networks_to_create = set()
docker_providers = set()
vmUser = {}
for virtualMachine in data_loaded["virtualMachines"]:
print(virtualMachine)
dockerHostNodeName = virtualMachine["dockerHostNodeName"]
vmUser[dockerHostNodeName] = virtualMachine["vmUser"]
for element in data_loaded["containers"]:
print(element)
fname = services_dir+"/"+appName+"-container-"+element["containerName"]+".tf"
dockerHostNodeName = element["dockerHostNodeName"]
context = {
'appName': appName,
'consulJoinIp': consulJoinIp,
'containerName': element["containerName"],
'imageName': element["imageName"],
#'docker_host_ip': element["docker-host-ip"],
'dockerHostNodeName': element["dockerHostNodeName"],
'hostname': element["hostname"],
'vmUser': vmUser[element["dockerHostNodeName"]],
}
docker_providers.add(element["dockerHostNodeName"])
if 'imageTag' in element:
......@@ -938,11 +1009,8 @@ def create_services_tf(data_loaded):
f.write(tf)
#Create common stuff section:
context = {
'appName': appName,
'dockerProviders': docker_providers
}
context['dockerProviders'] = docker_providers
fname = services_dir+"/"+appName+"-services-common.tf"
with open(fname, 'w') as f:
tf = render_template('services-common-tpl.tf', context)
......@@ -988,6 +1056,12 @@ def create_adapt_tf(data_loaded):
cloudbrokerUsername = data_loaded["cloudbrokerUsername"]
cloudbrokerPassword = data_loaded["cloudbrokerPassword"]
vmUser = {}
for virtualMachine in data_loaded["virtualMachines"]:
print(virtualMachine)
dockerHostNodeName = virtualMachine["dockerHostNodeName"]
vmUser[dockerHostNodeName] = virtualMachine["vmUser"]
for element in data_loaded["virtualMachines"]:
fname = infrastructure_dir+"/"+appName+"-vm-"+element["dockerHostNodeName"]+".tf"
print(element)
......@@ -1000,10 +1074,10 @@ def create_adapt_tf(data_loaded):
'vmSoftwareId': element["vmSoftwareId"],
'vmResourceId': element["vmResourceId"],
'vmRegionId': element["vmRegionId"],
'vmUser': element["vmUser"],
'instanceTypeId': element["instanceTypeId"],
'keyPairId': element["keyPairId"],
'openedPort': element["openedPort"],
'consulJoinIp': element["consulJoinIp"]
}
if 'dockerPrivateRegistryIp' in element:
context['dockerPrivateRegistryIp'] = element["dockerPrivateRegistryIp"]
......@@ -1023,6 +1097,7 @@ def create_adapt_tf(data_loaded):
context['imageName'] = element["imageName"]
context['dockerHostNodeName'] = element["dockerHostNodeName"]
context['hostname'] = element["hostname"]
context['vmUser'] = vmUser[element["vmUser"]],
docker_providers.add(element["dockerHostNodeName"])
if 'imageTag' in element:
context['imageTag'] = element["imageTag"]
......
......@@ -27,6 +27,10 @@ variable "vm_region_id" {
default = "4265ddb9-e862-4814-82a4-d6b92f25e8e5"
}
variable "vm_user" {
default = "ubuntu"
}
variable "instance_type_id" {
default = "e3ca8e4c-0f91-4e83-9bd9-4cef88d054a8"
}
......@@ -71,6 +75,7 @@ resource "cloudbroker_instance" "decide-vm" {
resource_id = "${var.vm_resource_id}"
region_id = "${var.vm_region_id}"
instance_type_id = "${var.instance_type_id}"
vm_user = "${var.vm_user}"
isolated = "false"
key_pair_id = "${var.key_pair_id}"
disable_autostop = "true"
......@@ -91,7 +96,7 @@ resource "cloudbroker_instance" "decide-vm" {
connection {
type = "ssh"
user = "ubuntu"
user = "${var.vm_user}"
private_key = "${file("/home/ubuntu/terraform/keypairs/${var.app_name}/private-key-openssh")}"
}
}
......@@ -105,7 +110,7 @@ resource "cloudbroker_instance" "decide-vm" {
connection {
type = "ssh"
user = "ubuntu"
user = "${var.vm_user}"
private_key = "${file("/home/ubuntu/terraform/keypairs/${var.app_name}/private-key-openssh")}"
}
}
......@@ -113,7 +118,10 @@ resource "cloudbroker_instance" "decide-vm" {
provisioner "local-exec" {
command = <<CMD
mkdir -p /home/ubuntu/terraform/certs/${cloudbroker_instance.decide-vm.external_ip_address} \
&& scp -i /home/ubuntu/terraform/keypairs/${var.app_name}/private-key-openssh -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null -r ubuntu@${cloudbroker_instance.decide-vm.external_ip_address}:~/.docker/client/keys/ /home/ubuntu/terraform/certs/${cloudbroker_instance.decide-vm.external_ip_address}
&& scp -i /home/ubuntu/terraform/keypairs/${var.app_name}/private-key-openssh -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null -r ${var.vm_user}@${cloudbroker_instance.decide-vm.external_ip_address}:/tmp/scripts/tempkeys/ /home/ubuntu/terraform/certs/${cloudbroker_instance.decide-vm.external_ip_address} \
&& mv /home/ubuntu/terraform/certs/${cloudbroker_instance.decide-vm.external_ip_address}/tempkeys /home/ubuntu/terraform/certs/${cloudbroker_instance.decide-vm.external_ip_address}/keys \
&& chmod -v 0400 /home/ubuntu/terraform/certs/${cloudbroker_instance.decide-vm.external_ip_address}/keys/key.pem \
&& chmod -v 0444 /home/ubuntu/terraform/certs/${cloudbroker_instance.decide-vm.external_ip_address}/keys/cert.pem
CMD
}
......
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AppDescription",
"description": "DECIDE Application Description",
"type": "object",
"required": [
"id",
"name",
"version",
"highTechnologicalRisk",
"microservices"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"version": {
"type": "string"
},
"highTechnologicalRisk": {
"type": "boolean"
},
"jenkinsEndpoint": {
"type": "string",
"format": "uri"
},
"jenkinsToken": {
"type": "string"
},
"monitoring": {
"$ref": "#/definitions/Monitoring"
},
"recommendedPatterns": {
"type": "array",
"items": {
"$ref": "#/definitions/Pattern"
}
},
"microservices": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/Microservice"
}
},
"mcsla": {
"$ref": "#/definitions/Mcsla"
},
"nfrs": {
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/definitions/AvailabilityNfr"
},
{
"$ref": "#/definitions/PerformanceNfr"
},
{
"$ref": "#/definitions/ScalabilityNfr"
},
{
"$ref": "#/definitions/LocationNfr"
},
{
"$ref": "#/definitions/CostNfr"
}
]
}
},
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/SchemaElement"
}
},
"virtualMachines": {
"type": "array",
"items": {
"$ref": "#/definitions/VirtualMachine"
}
},
"containers": {
"type": "array",
"items": {
"$ref": "#/definitions/Container"
}
},
"applicationInstanceId": {
"type": "string"
}
},
"definitions": {
"Microservice": {
"type": "object",
"required": [ "id", "endpoints" ],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"uniqueItems": true
},
"sourceRepository": {
"type": "string",
"format": "uri"
},
"deploymentOrder": {
"type": "number"
},
"programmingLanguage": {
"type": "string"
},
"containerId": {
"type": "string"
},
"containerRef": {
"type": "string"
},
"endpoints": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"stateful": {
"type": "boolean",
"default": "false"
},
"classification": {
"type": "string"
},
"dependencies": {
"type": "array",
"items": {
"type": "string"
}
},
"safeMethods": {
"type": "array",
"items": {
"type": "string"
}
},
"publicIP": {
"type": "boolean"
},
"infrastructureRequirements": {
"$ref": "#/definitions/InfrastructureRequirements"
},
"detachableResources": {
"type": "array",
"items": {