Skip to content

Commit 5716da2

Browse files
authored
Merge pull request #1559 from getredash/new_upgrade
New and shiny upgrade script
2 parents 18fee82 + d650995 commit 5716da2

File tree

1 file changed

+236
-0
lines changed

1 file changed

+236
-0
lines changed

bin/upgrade

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
#!/usr/bin/env python
2+
import os
3+
import argparse
4+
import subprocess
5+
import sys
6+
from collections import namedtuple
7+
from fnmatch import fnmatch
8+
9+
import requests
10+
11+
try:
12+
import semver
13+
except ImportError:
14+
print("Missing required library: semver.")
15+
exit(1)
16+
17+
REDASH_HOME = os.environ.get('REDASH_HOME', '/opt/redash')
18+
CURRENT_VERSION_PATH = '{}/current'.format(REDASH_HOME)
19+
20+
21+
def run(cmd, cwd=None):
22+
if not cwd:
23+
cwd = REDASH_HOME
24+
25+
return subprocess.check_output(cmd, cwd=cwd, shell=True, stderr=subprocess.STDOUT)
26+
27+
28+
def confirm(question):
29+
reply = str(raw_input(question + ' (y/n): ')).lower().strip()
30+
31+
if reply[0] == 'y':
32+
return True
33+
if reply[0] == 'n':
34+
return False
35+
else:
36+
return confirm("Please use 'y' or 'n'")
37+
38+
39+
def version_path(version_name):
40+
return "{}/{}".format(REDASH_HOME, version_name)
41+
42+
END_CODE = '\033[0m'
43+
44+
45+
def colored_string(text, color):
46+
if sys.stdout.isatty():
47+
return "{}{}{}".format(color, text, END_CODE)
48+
else:
49+
return text
50+
51+
52+
def h1(text):
53+
print(colored_string(text, '\033[4m\033[1m'))
54+
55+
56+
def green(text):
57+
print(colored_string(text, '\033[92m'))
58+
59+
60+
def red(text):
61+
print(colored_string(text, '\033[91m'))
62+
63+
64+
class Release(namedtuple('Release', ('version', 'download_url', 'filename', 'description'))):
65+
def v1_or_newer(self):
66+
return semver.compare(self.version, '1.0.0-alpha') >= 0
67+
68+
def is_newer(self, version):
69+
return semver.compare(self.version, version) > 0
70+
71+
@property
72+
def version_name(self):
73+
return self.filename.replace('.tar.gz', '')
74+
75+
76+
def get_latest_release_from_ci():
77+
response = requests.get('https://circleci.com/api/v1.1/project/github/getredash/redash/latest/artifacts')
78+
79+
if response.status_code != 200:
80+
exit("Failed getting releases (status code: %s)." % response.status_code)
81+
82+
tarball_asset = filter(lambda asset: asset['url'].endswith('.tar.gz'), response.json())[0]
83+
filename = tarball_asset['pretty_path'].replace('$CIRCLE_ARTIFACTS/', '')
84+
version = filename.replace('redash.', '').replace('.tar.gz', '')
85+
86+
release = Release(version, tarball_asset['url'], filename, '')
87+
88+
return release
89+
90+
91+
def get_release(channel):
92+
if channel == 'ci':
93+
return get_latest_release_from_ci()
94+
95+
response = requests.get('https://version.redash.io/api/releases?channel={}'.format(channel))
96+
release = response.json()[0]
97+
98+
filename = release['download_url'].split('/')[-1]
99+
release = Release(release['version'], release['download_url'], filename, release['description'])
100+
101+
return release
102+
103+
104+
def link_to_current(version_name):
105+
green("Linking to current version...")
106+
run('ln -nfs {} {}'.format(version_path(version_name), CURRENT_VERSION_PATH))
107+
108+
109+
def restart_services():
110+
# We're doing this instead of simple 'supervisorctl restart all' because
111+
# otherwise it won't notice that /opt/redash/current pointing at a different
112+
# directory.
113+
green("Restarting...")
114+
run('sudo /etc/init.d/redash_supervisord restart')
115+
116+
117+
def update_requirements(version_name):
118+
green("Installing new Python packages (if needed)...")
119+
new_requirements_file = '{}/requirements.txt'.format(version_path(version_name))
120+
121+
install_requirements = False
122+
123+
try:
124+
run('diff {}/requirements.txt {}'.format(CURRENT_VERSION_PATH, new_requirements_file)) != 0
125+
except subprocess.CalledProcessError as e:
126+
if e.returncode != 0:
127+
install_requirements = True
128+
129+
if install_requirements:
130+
run('sudo pip install -r {}'.format(new_requirements_file))
131+
132+
133+
def apply_migrations(release):
134+
green("Running migrations (if needed)...")
135+
if not release.v1_or_newer():
136+
return apply_migrations_pre_v1(release.version_name)
137+
138+
run("sudo -u redash bin/run ./manage.py db upgrade", cwd=version_path(release.version_name))
139+
140+
141+
def find_migrations(version_name):
142+
current_migrations = set([f for f in os.listdir("{}/migrations".format(CURRENT_VERSION_PATH)) if fnmatch(f, '*_*.py')])
143+
new_migrations = sorted([f for f in os.listdir("{}/migrations".format(version_path(version_name))) if fnmatch(f, '*_*.py')])
144+
145+
return [m for m in new_migrations if m not in current_migrations]
146+
147+
148+
def apply_migrations_pre_v1(version_name):
149+
new_migrations = find_migrations(version_name)
150+
151+
if new_migrations:
152+
green("New migrations to run: ")
153+
print(', '.join(new_migrations))
154+
else:
155+
print("No new migrations in this version.")
156+
157+
if new_migrations and confirm("Apply new migrations? (make sure you have backup)"):
158+
for migration in new_migrations:
159+
print("Applying {}...".format(migration))
160+
run("sudo sudo -u redash PYTHONPATH=. bin/run python migrations/{}".format(migration), cwd=version_path(version_name))
161+
162+
163+
def download_and_unpack(release):
164+
directory_name = release.version_name
165+
166+
green("Downloading release tarball...")
167+
run('sudo wget --header="Accept: application/octet-stream" -O {} {}'.format(release.filename, release.download_url))
168+
green("Unpacking to: {}...".format(directory_name))
169+
run('sudo mkdir -p {}'.format(directory_name))
170+
run('sudo tar -C {} -xvf {}'.format(directory_name, release.filename))
171+
172+
green("Changing ownership to redash...")
173+
run('sudo chown redash {}'.format(directory_name))
174+
175+
green("Linking .env file...")
176+
run('sudo ln -nfs {}/.env {}/.env'.format(REDASH_HOME, version_path(directory_name)))
177+
178+
179+
def current_version():
180+
real_current_path = os.path.realpath(CURRENT_VERSION_PATH).replace('.b', '+b')
181+
return real_current_path.replace(REDASH_HOME + '/', '').replace('redash.', '')
182+
183+
184+
def verify_minimum_version():
185+
green("Current version: " + current_version())
186+
if semver.compare(current_version(), '0.12.0') < 0:
187+
red("You need to have Redash v0.12.0 or newer to upgrade to post v1.0.0 releases.")
188+
green("To upgrade to v0.12.0, run the upgrade script set to the legacy channel (--channel legacy).")
189+
exit(1)
190+
191+
192+
def show_description_and_confirm(description):
193+
if description:
194+
print(description)
195+
196+
if not confirm("Continue with upgrade?"):
197+
red("Cancelling upgrade.")
198+
exit(1)
199+
200+
201+
def verify_newer_version(release):
202+
if not release.is_newer(current_version()):
203+
red("The found release is not newer than your current deployed release ({}). Aborting upgrade.".format(current_version()))
204+
exit(1)
205+
206+
207+
def deploy_release(channel):
208+
h1("Starting Redash upgrade:")
209+
210+
release = get_release(channel)
211+
green("Found version: {}".format(release.version))
212+
213+
if release.v1_or_newer():
214+
verify_minimum_version()
215+
216+
verify_newer_version(release)
217+
show_description_and_confirm(release.description)
218+
219+
try:
220+
download_and_unpack(release)
221+
update_requirements(release.version_name)
222+
apply_migrations(release)
223+
link_to_current(release.version_name)
224+
restart_services()
225+
green("Done! Enjoy.")
226+
except subprocess.CalledProcessError as e:
227+
red("Failed running: {}".format(e.cmd))
228+
red("Exit status: {}\nOutput:\n{}".format(e.returncode, e.output))
229+
230+
231+
if __name__ == '__main__':
232+
parser = argparse.ArgumentParser()
233+
parser.add_argument("--channel", help="The channel to get release from (default: stable).", default='stable')
234+
args = parser.parse_args()
235+
236+
deploy_release(args.channel)

0 commit comments

Comments
 (0)