Remove etcd db backend
The etcd backend is duplicated for removal. This commit removes the etcd db backend related code. Change-Id: Id7849576a94f66b681d8d346a90d4e151548eaa7
This commit is contained in:
parent
271ca60ce2
commit
08fe07743f
@ -7,7 +7,6 @@ eventlet!=0.18.3,!=0.20.1,>=0.18.2 # MIT
|
||||
keystonemiddleware>=4.18.0 # Apache-2.0
|
||||
pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD
|
||||
psutil>=3.2.2 # BSD
|
||||
python-etcd>=0.4.3 # MIT License
|
||||
python-glanceclient>=2.8.0 # Apache-2.0
|
||||
python-neutronclient>=6.7.0 # Apache-2.0
|
||||
python-cinderclient>=3.3.0 # Apache-2.0
|
||||
|
@ -115,10 +115,6 @@ class ContainersActionsController(base.Controller):
|
||||
raise exception.ResourceNotFound(name="Action", id=request_ident)
|
||||
|
||||
action_id = action.id
|
||||
if CONF.database.backend == 'etcd':
|
||||
# etcd using action.uuid get the unique action instead of action.id
|
||||
action_id = action.uuid
|
||||
|
||||
action = actions_view.format_action(action)
|
||||
show_traceback = False
|
||||
if policy.enforce(context, "container:action:events",
|
||||
|
@ -21,28 +21,12 @@ sql_opts = [
|
||||
help='MySQL engine to use.')
|
||||
]
|
||||
|
||||
etcd_opts = [
|
||||
cfg.HostAddressOpt('etcd_host',
|
||||
default='$my_ip',
|
||||
help="Host IP address on which etcd service "
|
||||
"running. The default is ``$my_ip``, "
|
||||
"the IP address of this host."),
|
||||
cfg.PortOpt('etcd_port',
|
||||
default=2379,
|
||||
help="Port on which etcd listen client request.")
|
||||
]
|
||||
|
||||
etcd_group = cfg.OptGroup(name='etcd', title='Options for etcd connection')
|
||||
|
||||
DEFAULT_OPTS = (sql_opts)
|
||||
ETCD_OPTS = (etcd_opts)
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
conf.register_opts(sql_opts, 'database')
|
||||
conf.register_group(etcd_group)
|
||||
conf.register_opts(etcd_opts, etcd_group)
|
||||
|
||||
|
||||
def list_opts():
|
||||
return {"DEFAULT": DEFAULT_OPTS, etcd_group: ETCD_OPTS}
|
||||
return {"DEFAULT": DEFAULT_OPTS}
|
||||
|
@ -31,7 +31,6 @@ Possible values:
|
||||
Related options:
|
||||
|
||||
* docker_remote_api_host
|
||||
* etcd_host
|
||||
* wsproxy_host
|
||||
* host_ip
|
||||
* my_block_storage_ip
|
||||
|
@ -23,8 +23,7 @@ import zun.conf
|
||||
"""Add the database backend mapping here"""
|
||||
|
||||
CONF = zun.conf.CONF
|
||||
_BACKEND_MAPPING = {'sqlalchemy': 'zun.db.sqlalchemy.api',
|
||||
'etcd': 'zun.db.etcd.api'}
|
||||
_BACKEND_MAPPING = {'sqlalchemy': 'zun.db.sqlalchemy.api'}
|
||||
IMPL = db_api.DBAPI.from_config(CONF,
|
||||
backend_mapping=_BACKEND_MAPPING,
|
||||
lazy=True)
|
||||
|
1352
zun/db/etcd/api.py
1352
zun/db/etcd/api.py
File diff suppressed because it is too large
Load Diff
@ -1,463 +0,0 @@
|
||||
# Copyright 2016 IBM, Corp.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
etcd models
|
||||
"""
|
||||
|
||||
import etcd
|
||||
from oslo_serialization import jsonutils as json
|
||||
|
||||
from zun.common import exception
|
||||
import zun.db.etcd as db
|
||||
from zun import objects
|
||||
|
||||
|
||||
class Base(object):
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
setattr(self, key, value)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
def get(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
def etcd_path(self, sub_path):
|
||||
return self.path + '/' + sub_path
|
||||
|
||||
def as_dict(self):
|
||||
d = {}
|
||||
for f in self._fields:
|
||||
d[f] = getattr(self, f, None)
|
||||
|
||||
return d
|
||||
|
||||
def path_already_exist(self, client, path):
|
||||
try:
|
||||
client.read(path)
|
||||
except etcd.EtcdKeyNotFound:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update(self, values):
|
||||
"""Make the model object behave like a dict."""
|
||||
for k, v in values.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
def save(self, session=None):
|
||||
if session is None:
|
||||
session = db.api.get_backend()
|
||||
client = session.client
|
||||
path = self.etcd_path(self.uuid)
|
||||
|
||||
if self.path_already_exist(client, path):
|
||||
raise exception.ResourceExists(name=getattr(self, '__class__'))
|
||||
|
||||
client.write(path, json.dump_as_bytes(self.as_dict()))
|
||||
return
|
||||
|
||||
def items(self):
|
||||
"""Make the model object behave like a dict."""
|
||||
return self.as_dict().items()
|
||||
|
||||
def iteritems(self):
|
||||
"""Make the model object behave like a dict."""
|
||||
return self.as_dict().items()
|
||||
|
||||
def keys(self):
|
||||
"""Make the model object behave like a dict."""
|
||||
return [key for key, value in self.iteritems()]
|
||||
|
||||
|
||||
class ZunService(Base):
|
||||
"""Represents health status of various zun services"""
|
||||
|
||||
_path = '/zun_services'
|
||||
|
||||
_fields = objects.ZunService.fields.keys()
|
||||
|
||||
def __init__(self, service_data):
|
||||
self.path = ZunService.path()
|
||||
for f in ZunService.fields():
|
||||
setattr(self, f, None)
|
||||
self.id = 1
|
||||
self.disabled = False
|
||||
self.forced_down = False
|
||||
self.report_count = 0
|
||||
self.update(service_data)
|
||||
|
||||
@classmethod
|
||||
def path(cls):
|
||||
return cls._path
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return cls._fields
|
||||
|
||||
def save(self, session=None):
|
||||
if session is None:
|
||||
session = db.api.get_backend()
|
||||
client = session.client
|
||||
path = self.etcd_path(self.host + '_' + self.binary)
|
||||
|
||||
if self.path_already_exist(client, path):
|
||||
raise exception.ZunServiceAlreadyExists(host=self.host,
|
||||
binary=self.binary)
|
||||
|
||||
client.write(path, json.dump_as_bytes(self.as_dict()))
|
||||
return
|
||||
|
||||
|
||||
class Container(Base):
|
||||
"""Represents a container."""
|
||||
|
||||
_path = '/containers'
|
||||
|
||||
_fields = objects.Container.fields.keys()
|
||||
|
||||
def __init__(self, container_data):
|
||||
self.path = Container.path()
|
||||
for f in Container.fields():
|
||||
setattr(self, f, None)
|
||||
self.id = 1
|
||||
self.disk = 0
|
||||
self.auto_remove = False
|
||||
self.interactive = False
|
||||
self.auto_heal = False
|
||||
self.privileged = False
|
||||
self.update(container_data)
|
||||
|
||||
@classmethod
|
||||
def path(cls):
|
||||
return cls._path
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return cls._fields
|
||||
|
||||
|
||||
class Image(Base):
|
||||
"""Represents a container image."""
|
||||
|
||||
_path = '/images'
|
||||
|
||||
_fields = objects.Image.fields.keys()
|
||||
|
||||
def __init__(self, image_data):
|
||||
self.path = Image.path()
|
||||
for f in Image.fields():
|
||||
setattr(self, f, None)
|
||||
self.id = 1
|
||||
self.update(image_data)
|
||||
|
||||
@classmethod
|
||||
def path(cls):
|
||||
return cls._path
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return cls._fields
|
||||
|
||||
|
||||
class ResourceClass(Base):
|
||||
"""Represents a resource class."""
|
||||
|
||||
_path = '/resource_classes'
|
||||
|
||||
_fields = objects.ResourceClass.fields.keys()
|
||||
|
||||
def __init__(self, resource_class_data):
|
||||
self.path = ResourceClass.path()
|
||||
for f in ResourceClass.fields():
|
||||
setattr(self, f, None)
|
||||
self.id = 1
|
||||
self.update(resource_class_data)
|
||||
|
||||
@classmethod
|
||||
def path(cls):
|
||||
return cls._path
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return cls._fields
|
||||
|
||||
|
||||
class Capsule(Base):
|
||||
"""Represents a capsule."""
|
||||
|
||||
_path = '/capsules'
|
||||
|
||||
_fields = objects.Capsule.fields.keys()
|
||||
|
||||
def __init__(self, capsule_data):
|
||||
self.path = Capsule.path()
|
||||
for f in Capsule.fields():
|
||||
setattr(self, f, None)
|
||||
self.id = 1
|
||||
self.update(capsule_data)
|
||||
|
||||
@classmethod
|
||||
def path(cls):
|
||||
return cls._path
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return cls._fields
|
||||
|
||||
|
||||
class ComputeNode(Base):
|
||||
"""Represents a compute node. """
|
||||
_path = '/compute_nodes'
|
||||
|
||||
# NOTE(kiennt): Use list(fields) instead of fields.keys()
|
||||
# because in Python 3, the dict.keys() method
|
||||
# returns a dictionary view object, which acts
|
||||
# as a set. To do the replacement, _fields should
|
||||
# be a list.
|
||||
_fields = list(objects.ComputeNode.fields)
|
||||
|
||||
def __init__(self, compute_node_data):
|
||||
self.path = ComputeNode.path()
|
||||
for f in ComputeNode.fields():
|
||||
setattr(self, f, None)
|
||||
self.cpus = 0
|
||||
self.cpu_used = 0
|
||||
self.mem_used = 0
|
||||
self.mem_total = 0
|
||||
self.mem_free = 0
|
||||
self.mem_available = 0
|
||||
self.total_containers = 0
|
||||
self.stopped_containers = 0
|
||||
self.paused_containers = 0
|
||||
self.running_containers = 0
|
||||
self.disk_used = 0
|
||||
self.disk_total = 0
|
||||
self.disk_quota_supported = False
|
||||
self.enable_cpu_pinning = False
|
||||
self.update(compute_node_data)
|
||||
|
||||
@classmethod
|
||||
def path(cls):
|
||||
return cls._path
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
# NOTE(kiennt): The pci_device_pools field in object maps to the
|
||||
# pci_stats field in the database. Therefore, need
|
||||
# replace these fields.
|
||||
for index, value in enumerate(cls._fields):
|
||||
if value == 'pci_device_pools':
|
||||
cls._fields.pop(index)
|
||||
cls._fields.insert(index, 'pci_stats')
|
||||
break
|
||||
return cls._fields
|
||||
|
||||
def save(self, session=None):
|
||||
if session is None:
|
||||
session = db.api.get_backend()
|
||||
client = session.client
|
||||
path = self.etcd_path(self.uuid)
|
||||
if self.path_already_exist(client, path):
|
||||
raise exception.ComputeNodeAlreadyExists(
|
||||
field='UUID', value=self.uuid)
|
||||
|
||||
client.write(path, json.dump_as_bytes(self.as_dict()))
|
||||
return
|
||||
|
||||
|
||||
class PciDevice(Base):
|
||||
"""Represents a PciDevice. """
|
||||
_path = '/pcidevices'
|
||||
|
||||
_fields = objects.PciDevice.fields.keys()
|
||||
|
||||
def __init__(self, pci_data):
|
||||
self.path = PciDevice.path()
|
||||
for f in PciDevice.fields():
|
||||
setattr(self, f, None)
|
||||
self.id = 1
|
||||
self.numa_node = 0
|
||||
self.update(pci_data)
|
||||
|
||||
@classmethod
|
||||
def path(cls):
|
||||
return cls._path
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return cls._fields
|
||||
|
||||
|
||||
class VolumeMapping(Base):
|
||||
"""Represents a VolumeMapping."""
|
||||
_path = '/volume_mapping'
|
||||
|
||||
_fields = objects.VolumeMapping.fields.keys()
|
||||
|
||||
def __init__(self, volume_mapping_data):
|
||||
self.path = VolumeMapping.path()
|
||||
for f in VolumeMapping.fields():
|
||||
setattr(self, f, None)
|
||||
self.id = 1
|
||||
self.auto_remove = False
|
||||
self.update(volume_mapping_data)
|
||||
|
||||
@classmethod
|
||||
def path(cls):
|
||||
return cls._path
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return cls._fields
|
||||
|
||||
|
||||
class ContainerAction(Base):
|
||||
"""Represents a container action.
|
||||
|
||||
The intention is that there will only be one of these pre user request. A
|
||||
lookup by(container_uuid, request_id) should always return a single result.
|
||||
"""
|
||||
_path = '/container_actions'
|
||||
|
||||
_fields = list(objects.ContainerAction.fields) + ['uuid']
|
||||
|
||||
def __init__(self, action_data):
|
||||
self.path = ContainerAction.path(action_data['container_uuid'])
|
||||
for f in ContainerAction.fields():
|
||||
setattr(self, f, None)
|
||||
self.id = 1
|
||||
self.update(action_data)
|
||||
|
||||
@classmethod
|
||||
def path(cls, container_uuid):
|
||||
return cls._path + '/' + container_uuid
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return cls._fields
|
||||
|
||||
|
||||
class ContainerActionEvent(Base):
|
||||
"""Track events that occur during an ContainerAction."""
|
||||
|
||||
_path = '/container_actions_events'
|
||||
|
||||
_fields = list(objects.ContainerActionEvent.fields) + ['action_uuid',
|
||||
'uuid']
|
||||
|
||||
def __init__(self, event_data):
|
||||
self.path = ContainerActionEvent.path(event_data['action_uuid'])
|
||||
for f in ContainerActionEvent.fields():
|
||||
setattr(self, f, None)
|
||||
self.id = 1
|
||||
self.action_id = 0
|
||||
self.update(event_data)
|
||||
|
||||
@classmethod
|
||||
def path(cls, action_uuid):
|
||||
return cls._path + '/' + action_uuid
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return cls._fields
|
||||
|
||||
|
||||
class Quota(Base):
|
||||
|
||||
"""Represents a Quota."""
|
||||
_path = '/quotas'
|
||||
|
||||
_fields = list(objects.Quota.fields) + ['uuid']
|
||||
|
||||
def __init__(self, quota_data):
|
||||
self.path = Quota.path(project_id=quota_data.get('class_name'),
|
||||
resource=quota_data.get('resource'))
|
||||
for f in Quota.fields():
|
||||
setattr(self, f, None)
|
||||
self.id = 1
|
||||
self.update(quota_data)
|
||||
|
||||
@classmethod
|
||||
def path(cls, project_id, resource=None):
|
||||
if resource is not None:
|
||||
path = '{}/{}/{}' . format(cls._path, project_id, resource)
|
||||
else:
|
||||
path = '{}/{}' . format(cls._path, project_id)
|
||||
return path
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return cls._fields
|
||||
|
||||
|
||||
class QuotaClass(Base):
|
||||
|
||||
"""Represents a QuotaClass."""
|
||||
_path = '/quota_classes'
|
||||
|
||||
_fields = list(objects.QuotaClass.fields) + ['uuid']
|
||||
|
||||
def __init__(self, quota_class_data):
|
||||
self.path = QuotaClass.path(
|
||||
class_name=quota_class_data.get('class_name'),
|
||||
resource=quota_class_data.get('resource'))
|
||||
|
||||
for f in QuotaClass.fields():
|
||||
setattr(self, f, None)
|
||||
|
||||
self.id = 1
|
||||
self.update(quota_class_data)
|
||||
|
||||
@classmethod
|
||||
def path(cls, class_name, resource=None):
|
||||
if resource is not None:
|
||||
path = '{}/{}/{}' . format(cls._path, class_name, resource)
|
||||
else:
|
||||
path = '{}/{}' . format(cls._path, class_name)
|
||||
return path
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return cls._fields
|
||||
|
||||
|
||||
class QuotaUsage(Base):
|
||||
|
||||
"""Represents the current usage for a given resource."""
|
||||
|
||||
_path = '/quota_usages'
|
||||
|
||||
_fields = ['id', 'project_id', 'resource', 'in_use', 'reserved']
|
||||
|
||||
def __init__(self, quota_usage_data):
|
||||
self.path = QuotaUsage.path(
|
||||
project_id=quota_usage_data['project_id'],
|
||||
resource=quota_usage_data['resource'])
|
||||
|
||||
for f in QuotaUsage.fields():
|
||||
setattr(self, f, None)
|
||||
|
||||
self.id = 1
|
||||
self.update(quota_usage_data)
|
||||
|
||||
@classmethod
|
||||
def path(cls, project_id, resource):
|
||||
return '{}/{}/{}' . format(cls._path, project_id, resource)
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return cls._fields
|
@ -28,7 +28,8 @@ class ContainerAction(base.ZunPersistentObject, base.ZunObject):
|
||||
|
||||
# Version 1.0: Initial version
|
||||
# Version 1.1: Add uuid column.
|
||||
VERSION = '1.1'
|
||||
# Version 1.2: Remove uuid column.
|
||||
VERSION = '1.2'
|
||||
|
||||
fields = {
|
||||
'id': fields.IntegerField(),
|
||||
@ -40,9 +41,6 @@ class ContainerAction(base.ZunPersistentObject, base.ZunObject):
|
||||
'start_time': fields.DateTimeField(tzinfo_aware=False, nullable=True),
|
||||
'finish_time': fields.DateTimeField(tzinfo_aware=False, nullable=True),
|
||||
'message': fields.StringField(nullable=True),
|
||||
# NOTE: By now, this field is only used for etcd. If using sql,
|
||||
# this field will be None.
|
||||
'uuid': fields.StringField(nullable=True),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
@ -21,16 +21,14 @@ class Quota(base.ZunPersistentObject, base.ZunObject):
|
||||
# Version 1.0: Initial version
|
||||
# Version 1.1: Add uuid column
|
||||
# Version 1.2: Add destroy_all_by_project method
|
||||
VERSION = '1.2'
|
||||
# Version 1.3: Remove uuid column
|
||||
VERSION = '1.3'
|
||||
|
||||
fields = {
|
||||
'id': fields.IntegerField(),
|
||||
'project_id': fields.StringField(nullable=True),
|
||||
'resource': fields.StringField(),
|
||||
'hard_limit': fields.IntegerField(nullable=True),
|
||||
# NOTE(kiennt): By now, this field is only used for etcd. If using sql,
|
||||
# this field will be None.
|
||||
'uuid': fields.StringField(nullable=True),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
@ -20,16 +20,14 @@ from zun.objects import base
|
||||
class QuotaClass(base.ZunPersistentObject, base.ZunObject):
|
||||
# Version 1.0: Initial version
|
||||
# Version 1.1: Add uuid column
|
||||
VERSION = '1.1'
|
||||
# Version 1.2: Remove uuid column
|
||||
VERSION = '1.2'
|
||||
|
||||
fields = {
|
||||
'id': fields.IntegerField(),
|
||||
'class_name': fields.StringField(nullable=True),
|
||||
'resource': fields.StringField(nullable=True),
|
||||
'hard_limit': fields.IntegerField(nullable=True),
|
||||
# NOTE(kiennt): By now, this field is only used for etcd. If using sql,
|
||||
# this field will be None.
|
||||
'uuid': fields.StringField(nullable=True),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
@ -16,7 +16,6 @@ import fixtures
|
||||
|
||||
import zun.conf
|
||||
from zun.db import api as db_api
|
||||
from zun.db.etcd import api as etcd_api
|
||||
from zun.db.sqlalchemy import api as sqla_api
|
||||
from zun.db.sqlalchemy import migration
|
||||
from zun.db.sqlalchemy import models
|
||||
@ -61,9 +60,7 @@ class DbTestCase(base.TestCase):
|
||||
def setUp(self):
|
||||
super(DbTestCase, self).setUp()
|
||||
|
||||
self.dbapi = (db_api._get_dbdriver_instance()
|
||||
if CONF.database.backend == "sqlalchemy"
|
||||
else etcd_api.get_backend())
|
||||
self.dbapi = db_api._get_dbdriver_instance()
|
||||
|
||||
global _DB_CACHE
|
||||
if not _DB_CACHE:
|
||||
|
@ -16,7 +16,6 @@ from oslo_config import cfg
|
||||
from zun.common import consts
|
||||
from zun.common import name_generator
|
||||
from zun.db import api as db_api
|
||||
from zun.db.etcd import api as etcd_api
|
||||
from zun.objects.container import Cpuset
|
||||
|
||||
CONF = cfg.CONF
|
||||
@ -106,10 +105,7 @@ def get_test_container(**kwargs):
|
||||
|
||||
|
||||
def _get_dbapi():
|
||||
if CONF.database.backend == 'sqlalchemy':
|
||||
dbapi = db_api._get_dbdriver_instance()
|
||||
else:
|
||||
dbapi = etcd_api.get_backend()
|
||||
dbapi = db_api._get_dbdriver_instance()
|
||||
return dbapi
|
||||
|
||||
|
||||
|
@ -362,11 +362,11 @@ object_data = {
|
||||
'ComputeNode': '1.13-3c122f455c38d3665d327c05d2df6617',
|
||||
'PciDevicePool': '1.0-3f5ddc3ff7bfa14da7f6c7e9904cc000',
|
||||
'PciDevicePoolList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e',
|
||||
'Quota': '1.2-3a7d520d119fe1e886baad968ef7990a',
|
||||
'QuotaClass': '1.1-239ae335b32036b86504684d3fdbeb7f',
|
||||
'Quota': '1.3-fcaaaf4b6e983207edba27a1cf8e51ab',
|
||||
'QuotaClass': '1.2-4739583a70891fbc145031228fb8001e',
|
||||
'ContainerPCIRequest': '1.0-b060f9f9f734bedde79a71a4d3112ee0',
|
||||
'ContainerPCIRequests': '1.0-7b8f7f044661fe4e24e6949c035af2c4',
|
||||
'ContainerAction': '1.1-bdf41c7cc36a0cb113eb6e0f5800a0d8',
|
||||
'ContainerAction': '1.2-e8c494b11ea259655256d87aef877eef',
|
||||
'ContainerActionEvent': '1.0-2974d0a6f5d4821fd4e223a88c10181a',
|
||||
'Network': '1.1-26e8d37a54e5fc905ede657744a221d9',
|
||||
'ExecInstance': '1.0-59464e7b96db847c0abb1e96d3cec30a',
|
||||
|
Loading…
x
Reference in New Issue
Block a user