Skip to content

Commit

Permalink
Generalize upgrades between different major versions (#46)
Browse files Browse the repository at this point in the history
* Support upgrade between different major versions: etcd.py will check etcdcluster version and make a decision whether it needs to run old binary first (to get the data) or it could just run a new binary.

* Support building of new docker images without updating Dockerfile:

```
$ docker build --build-arg ETCDVERSION_PREV=2.3.8 --build-arg ETCDVERSION=3.0.17 -t etcd-cluster:3.1.11-p18 .
$ docker build --build-arg ETCDVERSION_PREV=3.0.17 --build-arg ETCDVERSION=3.1.11 -t etcd-cluster:3.1.11-p18 .
$ docker build --build-arg ETCDVERSION_PREV=3.1.11 --build-arg ETCDVERSION=3.2.11 -t etcd-cluster:3.2.11-p18 .
```
  • Loading branch information
CyberDem0n authored Dec 19, 2017
1 parent b372776 commit d1e7fe8
Show file tree
Hide file tree
Showing 6 changed files with 32 additions and 23 deletions.
5 changes: 3 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ dist: trusty
language: python
python:
- "2.7"
- "3.3"
- "3.4"
- "3.5"
- "3.6"
install:
- pip install setuptools --upgrade
- pip install -r requirements.txt
- pip install coveralls
- pip install coveralls flake8
script:
- python setup.py test
- python setup.py flake8
Expand Down
8 changes: 5 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ RUN export DEBIAN_FRONTEND=noninteractive \
&& rm -rf /var/lib/apt/lists/*

## Install etcd
ENV ETCDVERSION 2.3.7
RUN curl -L https://github.com/coreos/etcd/releases/download/v${ETCDVERSION}/etcd-v${ETCDVERSION}-linux-amd64.tar.gz | tar xz -C /bin --xform='s/$/v2/x' --strip=1 --wildcards --no-anchored etcd

ENV ETCDVERSION 3.0.17
ARG ETCDVERSION_PREV=3.0.17
RUN curl -L https://github.com/coreos/etcd/releases/download/v${ETCDVERSION_PREV}/etcd-v${ETCDVERSION_PREV}-linux-amd64.tar.gz | tar xz -C /bin --xform='s/$/.old/x' --strip=1 --wildcards --no-anchored etcd

ARG ETCDVERSION=3.1.10
ENV ETCDVERSION=$ETCDVERSION
RUN curl -L https://github.com/coreos/etcd/releases/download/v${ETCDVERSION}/etcd-v${ETCDVERSION}-linux-amd64.tar.gz | tar xz -C /bin --strip=1 --wildcards --no-anchored etcd etcdctl

COPY etcd.py /bin/etcd.py
Expand Down
28 changes: 16 additions & 12 deletions etcd.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def adjust_security_groups(self, action, *members):
ToPort=self.peer_port,
CidrIp='{}/32'.format(m.addr)
)
except:
except Exception:
logging.exception('Exception on %s for for %s', action, m.addr)

def add_member(self, member):
Expand Down Expand Up @@ -275,8 +275,12 @@ def __init__(self, manager):
self.members = []

@property
def is_v3(self):
return self.cluster_version is not None and self.cluster_version.startswith('3.')
def is_upgraded(self):
etcdversion = os.environ.get('ETCDVERSION')
if etcdversion:
etcdversion = etcdversion[:etcdversion.rfind('.') + 1]

return etcdversion and self.cluster_version is not None and self.cluster_version.startswith(etcdversion)

@staticmethod
def is_multiregion():
Expand Down Expand Up @@ -314,7 +318,7 @@ def load_members(self):
self.leader_id = member.get_leader() # Let's ask him about leader of etcd-cluster
self.cluster_version = member.get_cluster_version() # and about cluster-wide etcd version
break
except:
except Exception:
logging.exception('Load members from etcd')

# combine both lists together
Expand Down Expand Up @@ -350,7 +354,7 @@ def __init__(self):
self.instance_id = None
self.me = None
self.etcd_pid = 0
self.runv2 = False
self.run_old = False
self._access_granted = False

def load_my_identities(self):
Expand Down Expand Up @@ -409,7 +413,7 @@ def clean_data_dir(self):
os.remove(path)
elif os.path.isdir(path):
shutil.rmtree(path)
except:
except Exception:
logging.exception('Can not remove %s', path)

def register_me(self, cluster):
Expand Down Expand Up @@ -445,7 +449,7 @@ def register_me(self, cluster):
raise EtcdClusterException('Can not register myself in etcd cluster')
time.sleep(self.NAPTIME)

self.runv2 = add_member and cluster_state == 'existing' and not cluster.is_v3
self.run_old = add_member and cluster_state == 'existing' and not cluster.is_upgraded

peers = ','.join(['{}={}'.format(m.instance_id or m.name, m.peer_url) for m in cluster.members
if (include_ec2_instances and m.instance_id) or m.peer_urls])
Expand All @@ -462,7 +466,7 @@ def run(self):

if cluster.is_healthy(self.me):
args = self.register_me(cluster)
binary = self.ETCD_BINARY + ('v2' if self.runv2 else '')
binary = self.ETCD_BINARY + ('.old' if self.run_old else '')

self.etcd_pid = os.fork()
if self.etcd_pid == 0:
Expand All @@ -474,7 +478,7 @@ def run(self):
self.etcd_pid = 0
except SystemExit:
break
except:
except Exception:
logging.exception('Exception in main loop')
logging.warning('Sleeping %s seconds before next try...', self.NAPTIME)
time.sleep(self.NAPTIME)
Expand Down Expand Up @@ -596,7 +600,7 @@ def run(self):
else:
self.members = {}
update_required = False
if self.manager.etcd_pid != 0 and self.manager.runv2 \
if self.manager.etcd_pid != 0 and self.manager.run_old \
and not self.cluster_unhealthy() and self.take_upgrade_lock(600):
logging.info('Performing upgrade of member %s', self.manager.me.name)
os.kill(self.manager.etcd_pid, signal.SIGTERM)
Expand All @@ -610,7 +614,7 @@ def run(self):
break
else:
logging.error('upgrade: giving up...')
except:
except Exception:
logging.exception('Exception in HouseKeeper main loop')
logging.debug('Sleeping %s seconds...', self.NAPTIME)
time.sleep(self.NAPTIME)
Expand Down Expand Up @@ -649,7 +653,7 @@ def main():
logging.error('Can not remove myself from cluster')
else:
logging.error('Cluster does not have accessible member')
except:
except Exception:
logging.exception('Failed to remove myself from cluster')


Expand Down
10 changes: 5 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
VERSION = '1.0'
DESCRIPTION = 'Etcd cluster appliance for the STUPS (AWS) environment'
LICENSE = 'Apache License Version 2.0'
URL = 'https://github.com/zalando/stups-etcd-cluster'
URL = 'https://github.com/zalando-stups/stups-etcd-cluster'
AUTHOR = 'Alexander Kukushkin'
AUTHOR_EMAIL = 'alexander.kukushkin@zalando.de'
KEYWORDS = 'etcd cluster etcd-cluster stups aws'
Expand All @@ -33,8 +33,9 @@
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: Implementation :: CPython',
]

Expand Down Expand Up @@ -66,7 +67,7 @@ def finalize_options(self):
def run_tests(self):
try:
import pytest
except:
except Exception:
raise RuntimeError('py.test is not installed, run: pip install pytest')
params = {'args': self.test_args}
if self.cov:
Expand Down Expand Up @@ -117,9 +118,8 @@ def setup_package():
test_suite='tests',
packages=[],
install_requires=get_install_requirements('requirements.txt'),
setup_requires=['flake8'],
cmdclass=cmdclass,
tests_require=['pytest-cov', 'pytest', 'mock'],
tests_require=['pytest-cov', 'pytest', 'mock', 'flake8'],
command_options=command_options,
entry_points={'console_scripts': CONSOLE_SCRIPTS},
)
Expand Down
2 changes: 2 additions & 0 deletions tests/test_etcd_cluster.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import unittest

from etcd import EtcdCluster, EtcdManager, EtcdMember
Expand All @@ -18,6 +19,7 @@ def setUp(self, res):
self.cluster = EtcdCluster(self.manager)
self.cluster.load_members()
self.assertFalse(EtcdCluster.is_multiregion())
os.environ['ETCDVERSION'] = '3.2.10'

@patch('boto3.resource')
def test_load_members(self, res):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_etcd_housekeeper.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def test_run(self, cli, res):
self.assertRaises(Exception, self.keeper.run)
with patch('time.sleep', Mock()):
self.keeper.is_leader = Mock(return_value=False)
self.keeper.manager.runv2 = True
self.keeper.manager.run_old = True
self.keeper.cluster_unhealthy = Mock(side_effect=[False, True, False])
self.assertRaises(Exception, self.keeper.run)
self.keeper.cluster_unhealthy = Mock(side_effect=[False] + [True]*100)
Expand Down

0 comments on commit d1e7fe8

Please sign in to comment.