Add python-path option to node

This change adds a new python_path Node attribute so that zuul executor
can remove the default hard-coded ansible_python_interpreter.

Change-Id: Iddf2cc6b2df579636ec39b091edcfe85a4a4ed10
This commit is contained in:
Tristan Cacqueray 2019-02-16 00:31:22 +00:00
parent 2e8f286c4f
commit 76aa62230c
21 changed files with 93 additions and 5 deletions

View File

@ -309,6 +309,12 @@ Options
The username that a consumer should use when connecting to the The username that a consumer should use when connecting to the
node. node.
.. attr:: python-path
:type: string
:default: /usr/bin/python2
The path of the default python interpreter.
.. attr:: providers .. attr:: providers
:type: list :type: list
@ -682,6 +688,12 @@ Selecting the OpenStack driver adds the following options to the
The username that a consumer should use when connecting to the The username that a consumer should use when connecting to the
node. node.
.. attr:: python-path
:type: str
:default: /usr/bin/python2
The path of the default python interpreter.
.. attr:: connection-type .. attr:: connection-type
:type: str :type: str
@ -1071,6 +1083,12 @@ Selecting the static driver adds the following options to the
The username nodepool will use to validate it can connect to the The username nodepool will use to validate it can connect to the
node. node.
.. attr:: python-path
:type: str
:default: /usr/bin/python2
The path of the default python interpreter.
.. attr:: max-parallel-jobs .. attr:: max-parallel-jobs
:type: int :type: int
:default: 1 :default: 1
@ -1188,6 +1206,12 @@ Selecting the kubernetes driver adds the following options to the
:value:`providers.[kubernetes].labels.type.pod` label type; :value:`providers.[kubernetes].labels.type.pod` label type;
specifies the image name used by the pod. specifies the image name used by the pod.
.. attr:: python-path
:type: str
:default: /usr/bin/python2
The path of the default python interpreter.
Openshift Driver Openshift Driver
---------------- ----------------
@ -1309,6 +1333,12 @@ Selecting the openshift driver adds the following options to the
The ImagePullPolicy, can be IfNotPresent, Always or Never. The ImagePullPolicy, can be IfNotPresent, Always or Never.
.. attr:: python-path
:type: str
:default: /usr/bin/python2
The path of the default python interpreter.
.. attr:: cpu .. attr:: cpu
:type: int :type: int
@ -1451,6 +1481,12 @@ section of the configuration.
The username that a consumer should use when connecting to the node. The username that a consumer should use when connecting to the node.
.. attr:: python-path
:type: str
:default: /usr/bin/python2
The path of the default python interpreter.
.. attr:: connection-type .. attr:: connection-type
:type: str :type: str

View File

@ -879,6 +879,7 @@ class BuildWorker(BaseWorker):
build_data.builder_id = self._builder_id build_data.builder_id = self._builder_id
build_data.builder = self._hostname build_data.builder = self._hostname
build_data.username = diskimage.username build_data.username = diskimage.username
build_data.python_path = diskimage.python_path
if self._statsd: if self._statsd:
pipeline = self._statsd.pipeline() pipeline = self._statsd.pipeline()
@ -991,7 +992,7 @@ class UploadWorker(BaseWorker):
self._config = new_config self._config = new_config
def _uploadImage(self, build_id, upload_id, image_name, images, provider, def _uploadImage(self, build_id, upload_id, image_name, images, provider,
username): username, python_path):
''' '''
Upload a local DIB image build to a provider. Upload a local DIB image build to a provider.
@ -1002,6 +1003,7 @@ class UploadWorker(BaseWorker):
that available for uploading. that available for uploading.
:param provider: The provider from the parsed config file. :param provider: The provider from the parsed config file.
:param username: :param username:
:param python_path:
''' '''
start_time = time.time() start_time = time.time()
timestamp = int(start_time) timestamp = int(start_time)
@ -1073,6 +1075,7 @@ class UploadWorker(BaseWorker):
data.external_name = ext_image_name data.external_name = ext_image_name
data.format = image.extension data.format = image.extension
data.username = username data.username = username
data.python_path = python_path
return data return data
@ -1170,13 +1173,14 @@ class UploadWorker(BaseWorker):
data = zk.ImageUpload() data = zk.ImageUpload()
data.state = zk.UPLOADING data.state = zk.UPLOADING
data.username = build.username data.username = build.username
data.python_path = build.python_path
upnum = self._zk.storeImageUpload( upnum = self._zk.storeImageUpload(
image.name, build.id, provider.name, data) image.name, build.id, provider.name, data)
data = self._uploadImage(build.id, upnum, image.name, data = self._uploadImage(build.id, upnum, image.name,
local_images, provider, local_images, provider,
build.username) build.username, build.python_path)
# Set final state # Set final state
self._zk.storeImageUpload(image.name, build.id, self._zk.storeImageUpload(image.name, build.id,

View File

@ -44,6 +44,7 @@ class ConfigValidator:
'rebuild-age': int, 'rebuild-age': int,
'env-vars': {str: str}, 'env-vars': {str: str},
'username': str, 'username': str,
'python-path': str,
'build-timeout': int, 'build-timeout': int,
} }

View File

@ -118,6 +118,7 @@ class Config(ConfigValue):
d.image_types = set(diskimage.get('formats', [])) d.image_types = set(diskimage.get('formats', []))
d.pause = bool(diskimage.get('pause', False)) d.pause = bool(diskimage.get('pause', False))
d.username = diskimage.get('username', 'zuul') d.username = diskimage.get('username', 'zuul')
d.python_path = diskimage.get('python-path', '/usr/bin/python2')
d.build_timeout = diskimage.get('build-timeout', (8 * 60 * 60)) d.build_timeout = diskimage.get('build-timeout', (8 * 60 * 60))
self.diskimages[d.name] = d self.diskimages[d.name] = d
@ -180,6 +181,7 @@ class DiskImage(ConfigValue):
self.image_types = None self.image_types = None
self.pause = False self.pause = False
self.username = None self.username = None
self.python_path = None
self.build_timeout = None self.build_timeout = None
def __eq__(self, other): def __eq__(self, other):
@ -192,6 +194,7 @@ class DiskImage(ConfigValue):
other.image_types == self.image_types and other.image_types == self.image_types and
other.pause == self.pause and other.pause == self.pause and
other.username == self.username and other.username == self.username and
other.python_path == self.python_path and
other.build_timeout == self.build_timeout) other.build_timeout == self.build_timeout)
return False return False

View File

@ -34,6 +34,7 @@ class ProviderCloudImage(ConfigValue):
return (self.name == other.name return (self.name == other.name
and self.image_id == other.image_id and self.image_id == other.image_id
and self.username == other.username and self.username == other.username
and self.python_path == other.python_path
and self.connection_type == other.connection_type and self.connection_type == other.connection_type
and self.connection_port == other.connection_port) and self.connection_port == other.connection_port)
return False return False
@ -189,6 +190,7 @@ class AwsProviderConfig(ProviderConfig):
i.name = image['name'] i.name = image['name']
i.image_id = image.get('image-id', None) i.image_id = image.get('image-id', None)
i.username = image.get('username', None) i.username = image.get('username', None)
i.python_path = image.get('python-path', '/usr/bin/python2')
i.connection_type = image.get('connection-type', 'ssh') i.connection_type = image.get('connection-type', 'ssh')
i.connection_port = image.get( i.connection_port = image.get(
'connection-port', 'connection-port',
@ -224,6 +226,7 @@ class AwsProviderConfig(ProviderConfig):
'connection-port': int, 'connection-port': int,
'image-id': str, 'image-id': str,
'username': str, 'username': str,
'python-path': str,
} }
provider = ProviderConfig.getCommonSchemaDict() provider = ProviderConfig.getCommonSchemaDict()

View File

@ -101,6 +101,7 @@ class AwsInstanceLauncher(NodeLauncher):
self.node.public_ipv4 = server_ip self.node.public_ipv4 = server_ip
self.node.host_keys = keys self.node.host_keys = keys
self.node.username = self.label.cloud_image.username self.node.username = self.label.cloud_image.username
self.node.python_path = self.label.cloud_image.python_path
self.zk.storeNode(self.node) self.zk.storeNode(self.node)
self.log.info("Instance %s is ready", instance_id) self.log.info("Instance %s is ready", instance_id)

View File

@ -27,6 +27,7 @@ class KubernetesLabel(ConfigValue):
return (other.name == self.name and return (other.name == self.name and
other.type == self.type and other.type == self.type and
other.image_pull == self.image_pull and other.image_pull == self.image_pull and
other.python_path == self.python_path and
other.image == self.image) other.image == self.image)
return False return False
@ -55,6 +56,7 @@ class KubernetesPool(ConfigPool):
pl.type = label['type'] pl.type = label['type']
pl.image = label.get('image') pl.image = label.get('image')
pl.image_pull = label.get('image-pull', 'IfNotPresent') pl.image_pull = label.get('image-pull', 'IfNotPresent')
pl.python_path = label.get('python-path', '/usr/bin/python2')
pl.pool = self pl.pool = self
self.labels[pl.name] = pl self.labels[pl.name] = pl
full_config.labels[label['name']].pools.append(self) full_config.labels[label['name']].pools.append(self)
@ -96,6 +98,7 @@ class KubernetesProviderConfig(ProviderConfig):
v.Required('type'): str, v.Required('type'): str,
'image': str, 'image': str,
'image-pull': str, 'image-pull': str,
'python-path': str,
} }
pool = ConfigPool.getCommonSchemaDict() pool = ConfigPool.getCommonSchemaDict()

View File

@ -39,6 +39,7 @@ class K8SLauncher(NodeLauncher):
self.node, self.handler.pool.name, self.label) self.node, self.handler.pool.name, self.label)
self.node.state = zk.READY self.node.state = zk.READY
self.node.python_path = self.label.python_path
# NOTE: resource access token may be encrypted here # NOTE: resource access token may be encrypted here
self.node.connection_port = resource self.node.connection_port = resource
if self.label.type == "namespace": if self.label.type == "namespace":

View File

@ -30,7 +30,8 @@ class OpenshiftLabel(ConfigValue):
other.image_pull == self.image_pull and other.image_pull == self.image_pull and
other.image == self.image and other.image == self.image and
other.cpu == self.cpu and other.cpu == self.cpu and
other.memory == self.memory) other.memory == self.memory and
other.python_path == self.python_path)
return False return False
def __repr__(self): def __repr__(self):
@ -60,6 +61,7 @@ class OpenshiftPool(ConfigPool):
pl.image_pull = label.get('image-pull', 'IfNotPresent') pl.image_pull = label.get('image-pull', 'IfNotPresent')
pl.cpu = label.get('cpu') pl.cpu = label.get('cpu')
pl.memory = label.get('memory') pl.memory = label.get('memory')
pl.python_path = label.get('python-path', '/usr/bin/python2')
pl.pool = self pl.pool = self
self.labels[pl.name] = pl self.labels[pl.name] = pl
full_config.labels[label['name']].pools.append(self) full_config.labels[label['name']].pools.append(self)
@ -104,6 +106,7 @@ class OpenshiftProviderConfig(ProviderConfig):
'image-pull': str, 'image-pull': str,
'cpu': int, 'cpu': int,
'memory': int, 'memory': int,
'python-path': str,
} }
pool = { pool = {

View File

@ -47,6 +47,7 @@ class OpenShiftLauncher(NodeLauncher):
self.node.connection_type = "project" self.node.connection_type = "project"
self.node.state = zk.READY self.node.state = zk.READY
self.node.python_path = self.label.python_path
# NOTE: resource access token may be encrypted here # NOTE: resource access token may be encrypted here
self.node.connection_port = resource self.node.connection_port = resource
self.zk.storeNode(self.node) self.zk.storeNode(self.node)

View File

@ -52,6 +52,7 @@ class ProviderCloudImage(ConfigValue):
self.image_id = None self.image_id = None
self.image_name = None self.image_name = None
self.username = None self.username = None
self.python_path = None
self.connection_type = None self.connection_type = None
self.connection_port = None self.connection_port = None
@ -62,6 +63,7 @@ class ProviderCloudImage(ConfigValue):
self.image_id == other.image_id and self.image_id == other.image_id and
self.image_name == other.image_name and self.image_name == other.image_name and
self.username == other.username and self.username == other.username and
self.python_path == other.python_path and
self.connection_type == other.connection_type and self.connection_type == other.connection_type and
self.connection_port == other.connection_port) self.connection_port == other.connection_port)
return False return False
@ -319,6 +321,7 @@ class OpenStackProviderConfig(ProviderConfig):
i.image_id = image.get('image-id', None) i.image_id = image.get('image-id', None)
i.image_name = image.get('image-name', None) i.image_name = image.get('image-name', None)
i.username = image.get('username', None) i.username = image.get('username', None)
i.python_path = image.get('python-path', '/usr/bin/python2')
i.connection_type = image.get('connection-type', 'ssh') i.connection_type = image.get('connection-type', 'ssh')
i.connection_port = image.get( i.connection_port = image.get(
'connection-port', 'connection-port',
@ -348,6 +351,7 @@ class OpenStackProviderConfig(ProviderConfig):
v.Exclusive('image-id', 'cloud-image-name-or-id'): str, v.Exclusive('image-id', 'cloud-image-name-or-id'): str,
v.Exclusive('image-name', 'cloud-image-name-or-id'): str, v.Exclusive('image-name', 'cloud-image-name-or-id'): str,
'username': str, 'username': str,
'python-path': str,
} }
pool_label_main = { pool_label_main = {

View File

@ -88,6 +88,7 @@ class OpenStackNodeLauncher(NodeLauncher):
upload_id=cloud_image.id) upload_id=cloud_image.id)
image_name = diskimage.name image_name = diskimage.name
username = cloud_image.username username = cloud_image.username
python_path = cloud_image.python_path
connection_type = diskimage.connection_type connection_type = diskimage.connection_type
connection_port = diskimage.connection_port connection_port = diskimage.connection_port
@ -105,6 +106,7 @@ class OpenStackNodeLauncher(NodeLauncher):
image_id = self.label.cloud_image.name image_id = self.label.cloud_image.name
image_name = self.label.cloud_image.name image_name = self.label.cloud_image.name
username = self.label.cloud_image.username username = self.label.cloud_image.username
python_path = self.label.cloud_image.python_path
connection_type = self.label.cloud_image.connection_type connection_type = self.label.cloud_image.connection_type
connection_port = self.label.cloud_image.connection_port connection_port = self.label.cloud_image.connection_port
@ -158,6 +160,8 @@ class OpenStackNodeLauncher(NodeLauncher):
self.node.resources = resources.quota['compute'] self.node.resources = resources.quota['compute']
if username: if username:
self.node.username = username self.node.username = username
self.node.python_path = python_path
self.node.connection_type = connection_type self.node.connection_type = connection_type
self.node.connection_port = connection_port self.node.connection_port = connection_port

View File

@ -58,6 +58,7 @@ class StaticPool(ConfigPool):
'connection-type': node.get('connection-type', 'ssh'), 'connection-type': node.get('connection-type', 'ssh'),
'username': node.get('username', 'zuul'), 'username': node.get('username', 'zuul'),
'max-parallel-jobs': int(node.get('max-parallel-jobs', 1)), 'max-parallel-jobs': int(node.get('max-parallel-jobs', 1)),
'python-path': node.get('python-path', '/usr/bin/python2'),
}) })
if isinstance(node['labels'], str): if isinstance(node['labels'], str):
for label in node['labels'].split(): for label in node['labels'].split():
@ -106,6 +107,7 @@ class StaticProviderConfig(ProviderConfig):
'connection-port': int, 'connection-port': int,
'connection-type': str, 'connection-type': str,
'max-parallel-jobs': int, 'max-parallel-jobs': int,
'python-path': str,
} }
pool = ConfigPool.getCommonSchemaDict() pool = ConfigPool.getCommonSchemaDict()
pool.update({ pool.update({

View File

@ -143,6 +143,7 @@ class StaticNodeProvider(Provider):
node.interface_ip = static_node["name"] node.interface_ip = static_node["name"]
node.connection_port = static_node["connection-port"] node.connection_port = static_node["connection-port"]
node.connection_type = static_node["connection-type"] node.connection_type = static_node["connection-type"]
node.python_path = static_node["python-path"]
nodeutils.set_node_ip(node) nodeutils.set_node_ip(node)
node.host_keys = host_keys node.host_keys = host_keys
self.zk.storeNode(node) self.zk.storeNode(node)

View File

@ -85,6 +85,7 @@ providers:
config-drive: true config-drive: true
- name: windows-unmanaged - name: windows-unmanaged
username: winzuul username: winzuul
python-path: A:/python3.7.exe
connection-type: winrm connection-type: winrm
connection-port: 5986 connection-port: 5986
pools: pools:
@ -144,6 +145,7 @@ providers:
- name: openshift-pod - name: openshift-pod
type: pod type: pod
image: docker.io/fedora:28 image: docker.io/fedora:28
python-path: /usr/bin/python3
memory: 512 memory: 512
cpu: 2 cpu: 2
@ -183,6 +185,7 @@ diskimages:
release: trusty release: trusty
rebuild-age: 3600 rebuild-age: 3600
build-timeout: 3600 build-timeout: 3600
python-path: /bin/python3.6
env-vars: env-vars:
TMPDIR: /opt/dib_tmp TMPDIR: /opt/dib_tmp
DIB_IMAGE_CACHE: /opt/dib_cache DIB_IMAGE_CACHE: /opt/dib_cache

View File

@ -47,6 +47,7 @@ diskimages:
- fedora - fedora
- vm - vm
release: 21 release: 21
python-path: /usr/bin/python3
env-vars: env-vars:
TMPDIR: /opt/dib_tmp TMPDIR: /opt/dib_tmp
DIB_IMAGE_CACHE: /opt/dib_cache DIB_IMAGE_CACHE: /opt/dib_cache

View File

@ -23,6 +23,7 @@ providers:
rate: 0.0001 rate: 0.0001
cloud-images: cloud-images:
- name: fake-image - name: fake-image
python-path: /usr/bin/python3
- name: fake-image-windows - name: fake-image-windows
username: zuul username: zuul
connection-type: winrm connection-type: winrm

View File

@ -19,3 +19,4 @@ providers:
- name: pod-fedora - name: pod-fedora
type: pod type: pod
image: docker.io/fedora:28 image: docker.io/fedora:28
python-path: '/usr/bin/python3'

View File

@ -120,6 +120,7 @@ class TestDriverOpenshift(tests.DBTestCase):
self.assertIsNotNone(node.launcher) self.assertIsNotNone(node.launcher)
self.assertEqual(node.connection_type, 'kubectl') self.assertEqual(node.connection_type, 'kubectl')
self.assertEqual(node.connection_port.get('token'), 'fake-token') self.assertEqual(node.connection_port.get('token'), 'fake-token')
self.assertEqual(node.python_path, '/usr/bin/python3')
node.state = zk.DELETING node.state = zk.DELETING
self.zk.storeNode(node) self.zk.storeNode(node)

View File

@ -66,6 +66,7 @@ class TestLauncher(tests.DBTestCase):
self.assertEqual(node.username, "zuul") self.assertEqual(node.username, "zuul")
self.assertEqual(node.connection_type, 'ssh') self.assertEqual(node.connection_type, 'ssh')
self.assertEqual(node.connection_port, 22) self.assertEqual(node.connection_port, 22)
self.assertEqual(node.python_path, '/usr/bin/python3')
p = "{path}/{id}".format( p = "{path}/{id}".format(
path=self.zk._imageUploadPath(image.image_name, path=self.zk._imageUploadPath(image.image_name,
image.build_id, image.build_id,
@ -146,6 +147,7 @@ class TestLauncher(tests.DBTestCase):
self.assertEqual(nodes[1].type, ['fake-label1']) self.assertEqual(nodes[1].type, ['fake-label1'])
self.assertEqual(nodes[2].type, ['fake-label4']) self.assertEqual(nodes[2].type, ['fake-label4'])
self.assertEqual(nodes[3].type, ['fake-label2']) self.assertEqual(nodes[3].type, ['fake-label2'])
self.assertEqual(nodes[0].python_path, '/usr/bin/python2')
def _test_node_assignment_at_quota(self, def _test_node_assignment_at_quota(self,
config, config,
@ -1313,6 +1315,7 @@ class TestLauncher(tests.DBTestCase):
nodes = self.waitForNodes('fake-label') nodes = self.waitForNodes('fake-label')
self.assertEqual(len(nodes), 1) self.assertEqual(len(nodes), 1)
self.assertIsNone(nodes[0].username) self.assertIsNone(nodes[0].username)
self.assertEqual(nodes[0].python_path, '/usr/bin/python3')
nodes = self.waitForNodes('fake-label-windows') nodes = self.waitForNodes('fake-label-windows')
self.assertEqual(len(nodes), 1) self.assertEqual(len(nodes), 1)
@ -1320,6 +1323,7 @@ class TestLauncher(tests.DBTestCase):
self.assertEqual('winrm', nodes[0].connection_type) self.assertEqual('winrm', nodes[0].connection_type)
self.assertEqual(5986, nodes[0].connection_port) self.assertEqual(5986, nodes[0].connection_port)
self.assertEqual(nodes[0].host_keys, []) self.assertEqual(nodes[0].host_keys, [])
self.assertEqual(nodes[0].python_path, '/usr/bin/python2')
nodes = self.waitForNodes('fake-label-arbitrary-port') nodes = self.waitForNodes('fake-label-arbitrary-port')
self.assertEqual(len(nodes), 1) self.assertEqual(len(nodes), 1)

View File

@ -283,6 +283,7 @@ class ImageBuild(BaseModel):
self.builder = None # Hostname self.builder = None # Hostname
self.builder_id = None # Unique ID self.builder_id = None # Unique ID
self.username = None self.username = None
self.python_path = None
def __repr__(self): def __repr__(self):
d = self.toDict() d = self.toDict()
@ -315,6 +316,7 @@ class ImageBuild(BaseModel):
if len(self.formats): if len(self.formats):
d['formats'] = ','.join(self.formats) d['formats'] = ','.join(self.formats)
d['username'] = self.username d['username'] = self.username
d['python_path'] = self.python_path
return d return d
@staticmethod @staticmethod
@ -332,6 +334,7 @@ class ImageBuild(BaseModel):
o.builder = d.get('builder') o.builder = d.get('builder')
o.builder_id = d.get('builder_id') o.builder_id = d.get('builder_id')
o.username = d.get('username', 'zuul') o.username = d.get('username', 'zuul')
o.python_path = d.get('python_path', '/usr/bin/python2')
# Only attempt the split on non-empty string # Only attempt the split on non-empty string
if d.get('formats', ''): if d.get('formats', ''):
o.formats = d.get('formats', '').split(',') o.formats = d.get('formats', '').split(',')
@ -345,13 +348,14 @@ class ImageUpload(BaseModel):
VALID_STATES = set([UPLOADING, READY, DELETING, FAILED]) VALID_STATES = set([UPLOADING, READY, DELETING, FAILED])
def __init__(self, build_id=None, provider_name=None, image_name=None, def __init__(self, build_id=None, provider_name=None, image_name=None,
upload_id=None, username=None): upload_id=None, username=None, python_path=None):
super(ImageUpload, self).__init__(upload_id) super(ImageUpload, self).__init__(upload_id)
self.build_id = build_id self.build_id = build_id
self.provider_name = provider_name self.provider_name = provider_name
self.image_name = image_name self.image_name = image_name
self.format = None self.format = None
self.username = username self.username = username
self.python_path = python_path
self.external_id = None # Provider ID of the image self.external_id = None # Provider ID of the image
self.external_name = None # Provider name of the image self.external_name = None # Provider name of the image
@ -383,6 +387,7 @@ class ImageUpload(BaseModel):
d['external_name'] = self.external_name d['external_name'] = self.external_name
d['format'] = self.format d['format'] = self.format
d['username'] = self.username d['username'] = self.username
d['python_path'] = self.python_path
return d return d
@staticmethod @staticmethod
@ -404,6 +409,7 @@ class ImageUpload(BaseModel):
o.external_name = d.get('external_name') o.external_name = d.get('external_name')
o.format = d.get('format') o.format = d.get('format')
o.username = d.get('username', 'zuul') o.username = d.get('username', 'zuul')
o.python_path = d.get('python_path', '/usr/bin/python2')
return o return o
@ -541,6 +547,7 @@ class Node(BaseModel):
self.hold_expiration = None self.hold_expiration = None
self.resources = None self.resources = None
self.attributes = None self.attributes = None
self.python_path = None
def __repr__(self): def __repr__(self):
d = self.toDict() d = self.toDict()
@ -578,7 +585,8 @@ class Node(BaseModel):
self.host_keys == other.host_keys and self.host_keys == other.host_keys and
self.hold_expiration == other.hold_expiration and self.hold_expiration == other.hold_expiration and
self.resources == other.resources and self.resources == other.resources and
self.attributes == other.attributes) self.attributes == other.attributes and
self.python_path == other.python_path)
else: else:
return False return False
@ -627,6 +635,7 @@ class Node(BaseModel):
d['hold_expiration'] = self.hold_expiration d['hold_expiration'] = self.hold_expiration
d['resources'] = self.resources d['resources'] = self.resources
d['attributes'] = self.attributes d['attributes'] = self.attributes
d['python_path'] = self.python_path
return d return d
@staticmethod @staticmethod
@ -690,6 +699,7 @@ class Node(BaseModel):
self.hold_expiration = hold_expiration self.hold_expiration = hold_expiration
self.resources = d.get('resources') self.resources = d.get('resources')
self.attributes = d.get('attributes') self.attributes = d.get('attributes')
self.python_path = d.get('python_path')
class ZooKeeper(object): class ZooKeeper(object):