Browse Source

First Release

master
giancarlo 1 year ago
commit
a6411ba7ea
  1. 4
      .gitignore
  2. 17
      .project
  3. 8
      .pydevproject
  4. 13
      .settings/org.eclipse.core.resources.prefs
  5. 2
      .settings/org.eclipse.ltk.core.refactoring.prefs
  6. 11
      README.md
  7. 4
      __main__.py
  8. 98
      dockerimageexecutor.py
  9. BIN
      dockerimageexecutor.zip
  10. 1
      engine/__init__.py
  11. 33
      engine/dmonitor.py
  12. 73
      engine/dockerengine.py
  13. 108
      engine/swarmengine.py
  14. 47
      issupport.py
  15. 1
      storagehub/__init__.py
  16. 17
      storagehub/storagehubcommand.py
  17. 45
      storagehub/storagehubcommandcreatefolder.py
  18. 45
      storagehub/storagehubcommandcreatetempfolder.py
  19. 34
      storagehub/storagehubcommanditemdelete.py
  20. 36
      storagehub/storagehubcommanditemdownload.py
  21. 45
      storagehub/storagehubcommanditemupload.py
  22. 35
      storagehub/storagehubcommandrootitemid.py

4
.gitignore

@ -0,0 +1,4 @@
/globalvariables.csv
/sortableelemnts.txt
/sortapp-img.tar.gz
/result.zip

17
.project

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>dockerimageexecutor</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.python.pydev.PyDevBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.python.pydev.pythonNature</nature>
</natures>
</projectDescription>

8
.pydevproject

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python interpreter</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/${PROJECT_DIR_NAME}</path>
</pydev_pathproperty>
</pydev_project>

13
.settings/org.eclipse.core.resources.prefs

@ -0,0 +1,13 @@
eclipse.preferences.version=1
encoding//engine/dmonitor.py=utf-8
encoding//engine/dockerengine.py=utf-8
encoding//engine/swarmengine.py=utf-8
encoding//storagehub/storagehubcommand.py=utf-8
encoding//storagehub/storagehubcommandcreatefolder.py=utf-8
encoding//storagehub/storagehubcommandcreatetempfolder.py=utf-8
encoding//storagehub/storagehubcommanditemdelete.py=utf-8
encoding//storagehub/storagehubcommanditemdownload.py=utf-8
encoding//storagehub/storagehubcommanditemupload.py=utf-8
encoding//storagehub/storagehubcommandrootitemid.py=utf-8
encoding/dockerimageexecutor.py=utf-8
encoding/issupport.py=utf-8

2
.settings/org.eclipse.ltk.core.refactoring.prefs

@ -0,0 +1,2 @@
eclipse.preferences.version=1
org.eclipse.ltk.core.refactoring.enable.project.refactoring.history=false

11
README.md

@ -0,0 +1,11 @@
#Docker Image Executor
This is a simple algorithm that execute image on Docker using a image file or Swarm Cluster using local registry.
- python3 dockerimageexecutor.py <software-image> <software-execute-command-name> <file-item-id>
##Example
- python3 dockerimageexecutor.py "microservices-VirtualBox:443/sortapp" sortapp 548eade8-25cf-4978-9f61-0f0c652900be

4
__main__.py

@ -0,0 +1,4 @@
print('dockerimageexecutor/__main__.py executed')
#import dockerimageexecutor

98
dockerimageexecutor.py

@ -0,0 +1,98 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# @author: Giancarlo Panichi
#
# Created on 2020/06/12
#
import os
import sys
from issupport import ISSupport
from engine.dockerengine import DockerEngine
from engine.swarmengine import SwarmEngine
from storagehub.storagehubcommanditemdownload import StorageHubCommandItemDownload
from storagehub.storagehubcommandcreatetempfolder import StorageHubCommandCreateTempFolder
from storagehub.storagehubcommanditemdelete import StorageHubCommandItemDelete
class DockerImageExecutor:
def __init__(self):
self.resultFile = "result.zip"
self.globalVariablesFile = "globalvariables.csv"
self.gcubeToken = None
self.storageHubUrl = None
self.softwareImage = sys.argv[1] # Software Image
self.softwareExecuteCommandName = sys.argv[2] # Command to Run
self.fileItemId = sys.argv[3] # Input Data File
self.tempFolderItemId = None # '32c0422f-a777-4452-adea-007347ec4484'
def main(self):
print(self)
self.retrieveToken()
issup = ISSupport()
self.storageHubUrl = issup.discoverStorageHub(self.gcubeToken)
self.createTempFolder()
# self.executeOnDocker()
self.executeOnSwarm()
def retrieveToken(self):
print("Retrieve gcubeToken")
if not os.path.isfile(self.globalVariablesFile):
print("File does not exist: " + self.globalVariablesFile)
raise Exception("File does not exist: " + self.globalVariablesFile)
with open(self.globalVariablesFile) as fp:
for line in fp:
if line.find("gcube_token") != -1:
tk = line[14:]
self.gcubeToken = tk.replace('"', '').strip()
print("Found gcube_token")
break
if self.gcubeToken == None:
print('Error gcube_token not found!')
raise Exception('Error gcube_token not found!')
def createTempFolder(self):
print("Create Temp Folder")
cmdCreateTempFolder = StorageHubCommandCreateTempFolder(self.gcubeToken, self.storageHubUrl)
self.tempFolderItemId = cmdCreateTempFolder.execute()
def deleteTempFolder(self):
print("Delete Temp Folder")
cmdDeleteTempFolder = StorageHubCommandItemDelete(self.gcubeToken, self.storageHubUrl, self.tempFolderItemId)
cmdDeleteTempFolder.execute()
def downloadResults(self):
print("Get Results")
cmdItemDownload = StorageHubCommandItemDownload(self.gcubeToken, self.storageHubUrl,
self.tempFolderItemId, self.resultFile)
cmdItemDownload.execute()
self.deleteTempFolder()
def executeOnDocker(self):
print("Execute On Docker")
dEngine = DockerEngine(self.gcubeToken, self.storageHubUrl,
self.softwareImage, self.softwareExecuteCommandName, self.fileItemId, self.tempFolderItemId)
dEngine.execute()
self.downloadResults()
def executeOnSwarm(self):
print("Execute On Swarm")
sEngine = SwarmEngine(self.gcubeToken, self.storageHubUrl,
self.softwareImage, self.softwareExecuteCommandName, self.fileItemId, self.tempFolderItemId)
sEngine.execute()
self.downloadResults()
def __str__(self):
return 'DockerImageExecutor'
def main():
print('docker_image_executor')
dockerImageExecutor = DockerImageExecutor()
dockerImageExecutor.main()
main()

BIN
dockerimageexecutor.zip

Binary file not shown.

1
engine/__init__.py

@ -0,0 +1 @@
print("dockerimageexecutor/engine init")

33
engine/dmonitor.py

@ -0,0 +1,33 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# @author: Giancarlo Panichi
#
# Created on 2020/06/12
#
from threading import Thread
class DMonitor (Thread):
def __init__(self, name, interval, client):
Thread.__init__(self)
self.name = name
self.interval = interval
self.client = client
self.end = False
self.count = 0
def conclude(self):
print("Call End")
self.end = True
def run(self):
print ("Thread '" + self.name + "' start")
for event in self.client.events(decode=True):
print(event)
if self.end:
break
print ("Thread '" + self.name + "' end")

73
engine/dockerengine.py

@ -0,0 +1,73 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# @author: Giancarlo Panichi
#
# Created on 2020/06/12
#
import docker
import json
import gzip
class DockerEngine:
def __init__(self, gcubeToken, storageHubUrl, softwareImage, softwareExecuteCommandName, fileItemId, tempFolderItemId):
self.gcubeToken = gcubeToken
self.storageHubUrl = storageHubUrl
# Software image for example: "microservices-VirtualBox:443/sortapp-img"
self.softwareImage = softwareImage
# Software Execute Command Name for example: "sortapp"
self.softwareExecuteCommandName = softwareExecuteCommandName
# Input Data File
self.fileItemId = fileItemId
self.tempFolderItemId = tempFolderItemId
self.dockerImageArchive = "sortapp-img.tar.gz"
def execute(self):
print("Execute DockerImageEngine")
print("Create Client")
# TLS support
# tls_config = docker.tls.TLSConfig(
# client_cert=('/path/to/client-cert.pem', '/path/to/client-key.pem')
# )
# client = docker.DockerClient(base_url='<https_url>', tls=tls_config)
client = docker.DockerClient(base_url='unix://var/run/docker.sock')
print(json.dumps(client.info(), indent=2))
# print(json.dumps(client.version(), indent=2))
print("Add File Image")
with gzip.open(self.dockerImageArchive, "rb") as imageZip:
imagedata = imageZip.read()
sortAppImg = client.images.load(imagedata)
print(sortAppImg)
imagesList = client.images.list(all=True)
print ("Images found: ")
print(*imagesList, sep="\n")
print("Create Container")
cmdValue = "{} {} {} {}".format(self.softwareExecuteCommandName, self.gcubeToken, self.fileItemId, self.tempFolderItemId)
print("CommandValue: " + cmdValue)
hostConf = client.api.create_host_config(auto_remove=True);
container = client.api.create_container(
image='sortapp-img:latest',
command=cmdValue,
host_config=hostConf)
print("Start Container")
client.api.start(container=container.get('Id'))
client.api.wait(container=container.get('Id'), timeout=3600)
print("Container Execution End")
def __str__(self):
return ('DEngine[storageHubUrl=' + str(self.storageHubUrl) +
', softwareImage=' + str(self.softwareImage) +
', softwareExecuteCommandName=' + str(self.softwareExecuteCommandName) +
', fileItemId=' + str(self.fileItemId) +
', tempFolderItemId=' + str(self.tempFolderItemId) + ']')

108
engine/swarmengine.py

@ -0,0 +1,108 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# @author: Giancarlo Panichi
#
# Created on 2020/06/12
#
import docker
import json
import time
from docker.types.services import RestartPolicy
class SwarmEngine:
def __init__(self, gcubeToken, storageHubUrl, softwareImage, softwareExecuteCommandName, fileItemId, tempFolderItemId):
self.gcubeToken = gcubeToken
self.storageHubUrl = storageHubUrl
# Software image for example: "microservices-VirtualBox:443/sortapp"
self.softwareImage = softwareImage
# Software Execute Command Name for example: "sortapp"
self.softwareExecuteCommandName = softwareExecuteCommandName
# Input Data File
self.fileItemId = fileItemId
self.tempFolderItemId = tempFolderItemId
def monitorTask(self, srv):
print("Monitor Task")
end = False
sId=None
while not end:
for task in srv.tasks():
print("Task: " + str(task))
sId = task['ServiceID']
if sId and sId == srv.id:
status = task['DesiredState']
print("Task DesiredState: " + str(status))
if status == 'shutdown' or status == 'complete':
print("Task End")
srv.remove()
print("Service Removed")
end = True
elif (status == 'failed' or status == 'rejected'
or status == 'orphaned' or status == 'remove'):
print("Error in execute Docker Image on Swarm Node: " + str(status))
raise Exception("Error in execute Docker Image on Swarm Node: " + str(status))
else:
time.sleep(2)
else:
continue
if sId==None:
print("Waiting Task load on Service")
time.sleep(2)
def execute(self):
print("Execute SwarmRegistryEngine")
print("Create Client")
# DockerClient
# for example: http://microservices-VirtualBox:2375/
#
# TLS support
# tls_config = docker.tls.TLSConfig(
# client_cert=('/path/to/client-cert.pem', '/path/to/client-key.pem')
# )
# client = docker.DockerClient(base_url='<https_url>', tls=tls_config)
client = docker.DockerClient(base_url='http://docker-swarm1.int.d4science.net:2376/')
# client = docker.DockerClient(base_url='unix://var/run/docker.sock')
print(json.dumps(client.info(), indent=2))
image=client.images.pull(self.softwareImage, tag='latest')
print("Pulled: "+str(image))
print("Services: " + str(client.services.list()))
cmdValue = "{} {} {} {}".format(self.softwareExecuteCommandName, self.gcubeToken, self.fileItemId, self.tempFolderItemId)
print("CommandValue: " + cmdValue)
print("Create Service")
srv = client.services.create(self.softwareImage, command=cmdValue,
restart_policy=RestartPolicy(condition='none', delay=0, max_attempts=0, window=0))
print("Service: " + str(srv))
print("Service Id: " + str(srv.id))
print("Node: " + str(client.nodes.list()))
self.monitorTask(srv)
print("Service Execution End")
def __str__(self):
return ('SwarmRegistryEngine[storageHubUrl=' + str(self.storageHubUrl) +
', softwareImage=' + str(self.softwareImage) +
', softwareExecuteCommandName=' + str(self.softwareExecuteCommandName) +
', fileItemId=' + str(self.fileItemId) +
', tempFolderItemId=' + str(self.tempFolderItemId) + ']')

47
issupport.py

@ -0,0 +1,47 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# @author: Giancarlo Panichi
#
# Created on 2018/06/15
#
import requests
from xml.etree import ElementTree
class ISSupport:
def __init__(self):
# dev
# self.serviceUrl = "https://node10-d-d4s.d4science.org"
# prod
self.serviceUrl = "http://registry.d4science.org"
self.storageHubServiceClass = "DataAccess"
self.storageHubServiceName = "StorageHub"
def discoverStorageHub(self, gcubeToken):
print("Discover StorageHub")
urlString = self.serviceUrl + "/icproxy/gcube/service/GCoreEndpoint/" + self.storageHubServiceClass + "/" + self.storageHubServiceName + "?gcube-token=" + gcubeToken
r = requests.get(urlString)
print(r.status_code)
print(r.text)
if r.status_code != 200:
print("Error discovering StorageHub: " + r.status_code)
raise Exception("Error retrieving StorageHub url info: " + r.status_code)
else:
root = ElementTree.fromstring(r.text)
print(root)
gcoreEndpoint = root.findall("Result/Resource/Profile/AccessPoint/RunningInstanceInterfaces/Endpoint")
print(gcoreEndpoint)
for child in gcoreEndpoint:
print(child.tag, child.attrib)
if child.attrib["EntryName"] == "org.gcube.data.access.storagehub.StorageHub":
print("Endpoint Found")
print(child.text)
return child.text
print("Error discovering StorageHub url not found")
raise Exception("Error retrieving StorageHub url not found!")
def __str__(self):
return 'ISSupport[serviceUrl=' + str(self.serviceUrl) + ']'

1
storagehub/__init__.py

@ -0,0 +1 @@
print("sortapp/command init")

17
storagehub/storagehubcommand.py

@ -0,0 +1,17 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# @author: Giancarlo Panichi
#
# Created on 2018/06/15
#
class StorageHubCommand:
def execute(self):
print("StorageHubCommand")
def __str__(self):
return 'StorageHubCommand'

45
storagehub/storagehubcommandcreatefolder.py

@ -0,0 +1,45 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# @author: Giancarlo Panichi
#
# Created on 2018/06/15
#
import requests
from .storagehubcommand import StorageHubCommand
class StorageHubCommandCreateFolder(StorageHubCommand):
def __init__(self, gcubeToken, storageHubUrl, folderItemId, folderName, folderDescription, folderHidden="false"):
self.gcubeToken = gcubeToken
self.storageHubUrl = storageHubUrl
self.folderItemId = folderItemId
self.folderName = folderName
self.folderDescription = folderDescription
self.folderHidden = folderHidden
def execute(self):
print("Execute StorageHubCommandCreateFolder")
print(self.storageHubUrl + "/items/" + self.folderItemId + "/create/FOLDER?");
folderdata = {'name': self.folderName, 'description': self.folderDescription, 'hidden': self.folderHidden}
print(str(folderdata))
urlString = self.storageHubUrl + "/items/" + self.folderItemId + "/create/FOLDER?gcube-token=" + self.gcubeToken
r = requests.post(urlString, data=folderdata)
print(r)
print(r.status_code)
if r.status_code != 200:
print("Error in execute StorageHubCommandCreateFolder: " + r.status_code)
raise Exception("Error in execute StorageHubCommandCreateFolder: " + r.status_code)
print('Created Folder ItemId: ' + str(r.text))
return r.text
def __str__(self):
return ('StorageHubCommandItemUpload[ storageHubUrl=' + str(self.storageHubUrl) +
' folderItemId=' + str(self.folderItemId) +
', folderName=' + str(self.folderName) +
', folderDescription=' + str(self.folderDescription) +
', folderHidden=' + str(self.folderHidden) + ']')

45
storagehub/storagehubcommandcreatetempfolder.py

@ -0,0 +1,45 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# @author: Giancarlo Panichi
#
# Created on 2018/06/15
#
import uuid
from .storagehubcommand import StorageHubCommand
from .storagehubcommandrootitemid import StorageHubCommandRootItemId
from .storagehubcommandcreatefolder import StorageHubCommandCreateFolder
class StorageHubCommandCreateTempFolder(StorageHubCommand):
def __init__(self, gcubeToken, storageHubUrl):
self.gcubeToken = gcubeToken
self.storageHubUrl = storageHubUrl
self.tempFolderName = "DataMinerResult"
self.tempFolderDescription = "DataMiner temporary result folder"
self.tempFolderHidden = "false"
def execute(self):
print("Execute StorageHubCommandCreateTempFolder")
print("Retrieve RootFolder")
cmdRootItemId = StorageHubCommandRootItemId(self.gcubeToken, self.storageHubUrl)
rootFolderItemId = cmdRootItemId.execute()
print("Create Temp Folder Name")
tempFolderPostfix = uuid.uuid4().hex
self.tempFolderName = self.tempFolderName + tempFolderPostfix
print("Temp Folder Name: " + str(self.tempFolderName))
cmdCreateFolder = StorageHubCommandCreateFolder(self.gcubeToken, self.storageHubUrl, rootFolderItemId,
self.tempFolderName, self.tempFolderDescription, self.tempFolderHidden)
tempFolderItemId = cmdCreateFolder.execute()
print("Temp Folder Item Id: " + str(tempFolderItemId))
return tempFolderItemId
def __str__(self):
return 'StorageHubCommandCreateTempFolder[storageHubUrl=' + str(self.storageHubUrl) + ']'

34
storagehub/storagehubcommanditemdelete.py

@ -0,0 +1,34 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# @author: Giancarlo Panichi
#
# Created on 2018/06/15
#
import requests
from .storagehubcommand import StorageHubCommand
class StorageHubCommandItemDelete(StorageHubCommand):
def __init__(self, gcubeToken, storageHubUrl, itemId, permanently="false"):
self.gcubeToken = gcubeToken
self.storageHubUrl = storageHubUrl
self.itemId = itemId
self.permanently = permanently
def execute(self):
print("Execute StorageHubCommandItemDelete")
print(self.storageHubUrl + "/items/" + self.itemId + "?force=" + self.permanently);
urlString = self.storageHubUrl + "/items/" + self.itemId + "?force=" + self.permanently + "&gcube-token=" + self.gcubeToken
r = requests.delete(urlString)
print(r.status_code)
if r.status_code != 200:
print("Error in execute StorageHubCommandItemDelete: " + r.status_code)
raise Exception("Error in execute StorageHubCommandItemDelete: " + r.status_code)
def __str__(self):
return ('StorageHubCommandItemDelete[storageHubUrl=' + str(self.storageHubUrl) +
', itemId=' + self.itemId +
', permanently=' + self.permanently + ']')

36
storagehub/storagehubcommanditemdownload.py

@ -0,0 +1,36 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# @author: Giancarlo Panichi
#
# Created on 2018/06/15
#
import requests
from .storagehubcommand import StorageHubCommand
class StorageHubCommandItemDownload(StorageHubCommand):
def __init__(self, gcubeToken, storageHubUrl, itemId, destinationFile):
self.gcubeToken = gcubeToken
self.storageHubUrl = storageHubUrl
self.itemId = itemId
self.destinationFile = destinationFile
def execute(self):
print("Execute StorageHubCommandItemDownload")
print(self.storageHubUrl + "/items/" + self.itemId + "/download?");
urlString = self.storageHubUrl + "/items/" + self.itemId + "/download?gcube-token=" + self.gcubeToken
r = requests.get(urlString)
print(r.status_code)
if r.status_code != 200:
print("Error in execute StorageHubCommandItemDownload: " + r.status_code)
raise Exception("Error in execute StorageHubCommandItemDownload: " + r.status_code)
with open(self.destinationFile, 'wb') as file:
file.write(r.content)
def __str__(self):
return ('StorageHubCommandItemDownload[storageHubUrl=' + str(self.storageHubUrl) +
', itemId=' + self.itemId +
', destinationFile=' + self.destinationFile + ']')

45
storagehub/storagehubcommanditemupload.py

@ -0,0 +1,45 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# @author: Giancarlo Panichi
#
# Created on 2018/06/15
#
import requests
from .storagehubcommand import StorageHubCommand
class StorageHubCommandItemUpload(StorageHubCommand):
def __init__(self, gcubeToken, storageHubUrl, folderItemId, file, filename, fileDescription):
self.gcubeToken = gcubeToken
self.storageHubUrl = storageHubUrl
self.folderItemId=folderItemId
self.file=file
self.filename=filename
self.fileDescription=fileDescription
def execute(self):
print("Execute StorageHubCommandItemUpload")
print(self.storageHubUrl + "/items/" + self.folderItemId + "/create/FILE?");
filedata = {'name': self.filename, 'description': self.fileDescription, "file": ("file", open(self.file, "rb"))}
urlString = self.storageHubUrl + "/items/" + self.folderItemId + "/create/FILE?gcube-token=" + self.gcubeToken
r = requests.post(urlString, files=filedata)
print(r)
print(r.status_code)
if r.status_code != 200:
print("Error in execute StorageHubCommandItemUpload: " + r.status_code)
raise Exception("Error in execute StorageHubCommandItemUpload: " + r.status_code)
print(str(r.text))
return r.text
def __str__(self):
return ('StorageHubCommandItemUpload[itemId='+self.folderItemId+
', storageHubUrl=' + str(self.storageHubUrl)+
', folderItemId=' + str(self.folderItemId) +
', filename=' + str(self.filename)+
', fileDescription=' + str(self.fileDescription) + ']')

35
storagehub/storagehubcommandrootitemid.py

@ -0,0 +1,35 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# @author: Giancarlo Panichi
#
# Created on 2018/06/15
#
import requests
import json
from .storagehubcommand import StorageHubCommand
class StorageHubCommandRootItemId(StorageHubCommand):
def __init__(self, gcubeToken, storageHubUrl):
self.gcubeToken = gcubeToken
self.storageHubUrl = storageHubUrl
def execute(self):
print("Execute StorageHubCommandRootItemId")
print(self.storageHubUrl + "/?exclude=hl:accounting");
urlString = self.storageHubUrl + "/?exclude=hl:accounting&gcube-token=" + self.gcubeToken
r = requests.get(urlString)
print(r.status_code)
if r.status_code != 200:
print("Error in execute StorageHubCommandRootItemId: " + r.status_code)
raise Exception("Error in execute StorageHubCommandRootItemId: " + r.status_code)
rootItemJ = json.loads(r.text)
rootId = rootItemJ['item']['id']
print('RootItemId:' + str(rootId))
return rootId
def __str__(self):
return 'StorageHubCommandRootItemId[storageHubUrl=' + str(self.storageHubUrl) + ']'
Loading…
Cancel
Save