From c8229b9c8aca6513454ed13aa6b148d5b9791866 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 23 Jan 2015 17:19:15 -0500 Subject: [PATCH] Implement new step-by-step setup --- data/migrations/env.py | 3 +- data/runmigration.py | 20 ++ endpoints/api/suconfig.py | 115 +++++- endpoints/web.py | 8 + static/css/core-ui.css | 286 ++++++++++++++- .../directives/config/config-setup-tool.html | 83 ++--- static/directives/cor-loader-inline.html | 5 + static/directives/cor-loader.html | 5 + static/directives/cor-step-bar.html | 3 + static/directives/cor-step.html | 6 + static/js/app.js | 12 +- static/js/controllers.js | 340 ------------------ static/js/controllers/setup.js | 281 +++++++++++++++ static/js/controllers/superuser.js | 205 +++++++++++ static/js/core-config-setup.js | 11 +- static/js/core-ui.js | 93 ++++- static/partials/setup.html | 289 +++++++++++++++ static/partials/super-user.html | 203 +---------- test/test_suconfig_api.py | 5 +- util/config/provider.py | 19 + 20 files changed, 1393 insertions(+), 599 deletions(-) create mode 100644 data/runmigration.py create mode 100644 static/directives/cor-loader-inline.html create mode 100644 static/directives/cor-loader.html create mode 100644 static/directives/cor-step-bar.html create mode 100644 static/directives/cor-step.html create mode 100644 static/js/controllers/setup.js create mode 100644 static/js/controllers/superuser.js create mode 100644 static/partials/setup.html diff --git a/data/migrations/env.py b/data/migrations/env.py index 3b2df5186..108c4c496 100644 --- a/data/migrations/env.py +++ b/data/migrations/env.py @@ -18,7 +18,8 @@ config.set_main_option('sqlalchemy.url', unquote(app.config['DB_URI'])) # Interpret the config file for Python logging. # This line sets up loggers basically. -fileConfig(config.config_file_name) +if config.config_file_name: + fileConfig(config.config_file_name) # add your model's MetaData object here # for 'autogenerate' support diff --git a/data/runmigration.py b/data/runmigration.py new file mode 100644 index 000000000..b06cf861d --- /dev/null +++ b/data/runmigration.py @@ -0,0 +1,20 @@ +import logging + +from alembic.config import Config +from alembic.script import ScriptDirectory +from alembic.environment import EnvironmentContext +from alembic.migration import __name__ as migration_name + +def run_alembic_migration(log_handler=None): + if log_handler: + logging.getLogger(migration_name).addHandler(log_handler) + + config = Config() + config.set_main_option("script_location", "data:migrations") + script = ScriptDirectory.from_config(config) + + def fn(rev, context): + return script._upgrade_revs('head', rev) + + with EnvironmentContext(config, script, fn=fn, destination_rev='head'): + script.run_env() \ No newline at end of file diff --git a/endpoints/api/suconfig.py b/endpoints/api/suconfig.py index 05efb4cd7..daaba41ce 100644 --- a/endpoints/api/suconfig.py +++ b/endpoints/api/suconfig.py @@ -1,19 +1,22 @@ import logging import os import json +import signal -from flask import abort +from flask import abort, Response from endpoints.api import (ApiResource, nickname, resource, internal_only, show_if, require_fresh_login, request, validate_json_request, verify_not_prod) from endpoints.common import common_login from app import app, CONFIG_PROVIDER, superusers from data import model +from data.database import configure from auth.permissions import SuperUserPermission from auth.auth_context import get_authenticated_user from data.database import User from util.config.configutil import add_enterprise_config_defaults from util.config.validator import validate_service_for_config, SSL_FILENAMES +from data.runmigration import run_alembic_migration import features @@ -21,12 +24,16 @@ logger = logging.getLogger(__name__) def database_is_valid(): """ Returns whether the database, as configured, is valid. """ + if app.config['TESTING']: + return False + try: - User.select().limit(1) + list(User.select().limit(1)) return True except: return False + def database_has_users(): """ Returns whether the database has any users defined. """ return bool(list(User.select().limit(1))) @@ -42,17 +49,107 @@ class SuperUserRegistryStatus(ApiResource): @nickname('scRegistryStatus') @verify_not_prod def get(self): - """ Returns whether a valid configuration, database and users exist. """ - file_exists = CONFIG_PROVIDER.yaml_exists() + """ Returns the status of the registry. """ + # If there is no conf/stack volume, then report that status. + if not CONFIG_PROVIDER.volume_exists(): + return { + 'status': 'missing-config-dir' + } + + # If there is no config file, we need to setup the database. + if not CONFIG_PROVIDER.yaml_exists(): + return { + 'status': 'config-db' + } + + # If the database isn't yet valid, then we need to set it up. + if not database_is_valid(): + return { + 'status': 'setup-db' + } + + # If we have SETUP_COMPLETE, then we're ready to go! + if app.config.get('SETUP_COMPLETE', False): + return { + 'requires_restart': CONFIG_PROVIDER.requires_restart(app.config), + 'status': 'ready' + } + return { - 'dir_exists': CONFIG_PROVIDER.volume_exists(), - 'file_exists': file_exists, - 'is_testing': app.config['TESTING'], - 'valid_db': database_is_valid(), - 'ready': not app.config['TESTING'] and file_exists and superusers.has_superusers() + 'status': 'create-superuser' if not database_has_users() else 'config' } +class _AlembicLogHandler(logging.Handler): + def __init__(self): + super(_AlembicLogHandler, self).__init__() + self.records = [] + + def emit(self, record): + self.records.append({ + 'level': record.levelname, + 'message': record.getMessage() + }) + +@resource('/v1/superuser/setupdb') +@internal_only +@show_if(features.SUPER_USERS) +class SuperUserSetupDatabase(ApiResource): + """ Resource for invoking alembic to setup the database. """ + @verify_not_prod + @nickname('scSetupDatabase') + def get(self): + """ Invokes the alembic upgrade process. """ + # Note: This method is called after the database configured is saved, but before the + # database has any tables. Therefore, we only allow it to be run in that unique case. + if CONFIG_PROVIDER.yaml_exists() and not database_is_valid(): + # Note: We need to reconfigure the database here as the config has changed. + combined = dict(**app.config) + combined.update(CONFIG_PROVIDER.get_yaml()) + + configure(combined) + app.config['DB_URI'] = combined['DB_URI'] + + log_handler = _AlembicLogHandler() + + try: + run_alembic_migration(log_handler) + except Exception as ex: + return { + 'error': str(ex) + } + + return { + 'logs': log_handler.records + } + + abort(403) + + + +@resource('/v1/superuser/shutdown') +@internal_only +@show_if(features.SUPER_USERS) +class SuperUserShutdown(ApiResource): + """ Resource for sending a shutdown signal to the container. """ + + @verify_not_prod + @nickname('scShutdownContainer') + def post(self): + """ Sends a signal to the phusion init system to shut down the container. """ + # Note: This method is called to set the database configuration before super users exists, + # so we also allow it to be called if there is no valid registry configuration setup. + if app.config['TESTING'] or not database_has_users() or SuperUserPermission().can(): + # Note: We skip if debugging locally. + if app.config.get('DEBUGGING') == True: + return {} + + os.kill(1, signal.SIGINT) + return {} + + abort(403) + + @resource('/v1/superuser/config') @internal_only @show_if(features.SUPER_USERS) diff --git a/endpoints/web.py b/endpoints/web.py index cf4c94bc7..6a1d4f076 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -98,6 +98,7 @@ def organizations(): def user(): return index('') + @web.route('/superuser/') @no_cache @route_show_if(features.SUPER_USERS) @@ -105,6 +106,13 @@ def superuser(): return index('') +@web.route('/setup/') +@no_cache +@route_show_if(features.SUPER_USERS) +def setup(): + return index('') + + @web.route('/signin/') @no_cache def signin(redirect=None): diff --git a/static/css/core-ui.css b/static/css/core-ui.css index 02ffbb34a..1a4e34816 100644 --- a/static/css/core-ui.css +++ b/static/css/core-ui.css @@ -366,10 +366,6 @@ bottom: 0px; } -.config-setup-tool .cor-floating-bottom-bar { - text-align: right; -} - .config-setup-tool .cor-floating-bottom-bar button i.fa { margin-right: 6px; } @@ -418,3 +414,285 @@ font-family: Consolas, "Lucida Console", Monaco, monospace; font-size: 12px; } + +.co-m-loader, .co-m-inline-loader { + min-width: 28px; } + +.co-m-loader { + display: block; + position: absolute; + left: 50%; + top: 50%; + margin: -11px 0 0 -13px; } + +.co-m-inline-loader { + display: inline-block; + cursor: default; } + .co-m-inline-loader:hover { + text-decoration: none; } + +.co-m-loader-dot__one, .co-m-loader-dot__two, .co-m-loader-dot__three { + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + -ms-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; + animation-fill-mode: both; + -webkit-animation-fill-mode: both; + -moz-animation-fill-mode: both; + -ms-animation-fill-mode: both; + -o-animation-fill-mode: both; + animation-name: bouncedelay; + animation-duration: 1s; + animation-timing-function: ease-in-out; + animation-delay: 0; + animation-direction: normal; + animation-iteration-count: infinite; + animation-fill-mode: forwards; + animation-play-state: running; + -webkit-animation-name: bouncedelay; + -webkit-animation-duration: 1s; + -webkit-animation-timing-function: ease-in-out; + -webkit-animation-delay: 0; + -webkit-animation-direction: normal; + -webkit-animation-iteration-count: infinite; + -webkit-animation-fill-mode: forwards; + -webkit-animation-play-state: running; + -moz-animation-name: bouncedelay; + -moz-animation-duration: 1s; + -moz-animation-timing-function: ease-in-out; + -moz-animation-delay: 0; + -moz-animation-direction: normal; + -moz-animation-iteration-count: infinite; + -moz-animation-fill-mode: forwards; + -moz-animation-play-state: running; + display: inline-block; + height: 6px; + width: 6px; + background: #419eda; + border-radius: 100%; + display: inline-block; } + +.co-m-loader-dot__one { + animation-delay: -0.32s; + -webkit-animation-delay: -0.32s; + -moz-animation-delay: -0.32s; + -ms-animation-delay: -0.32s; + -o-animation-delay: -0.32s; } + +.co-m-loader-dot__two { + animation-delay: -0.16s; + -webkit-animation-delay: -0.16s; + -moz-animation-delay: -0.16s; + -ms-animation-delay: -0.16s; + -o-animation-delay: -0.16s; } + +@-webkit-keyframes bouncedelay { + 0%, 80%, 100% { + -webkit-transform: scale(0.25, 0.25); + -moz-transform: scale(0.25, 0.25); + -ms-transform: scale(0.25, 0.25); + -o-transform: scale(0.25, 0.25); + transform: scale(0.25, 0.25); } + + 40% { + -webkit-transform: scale(1, 1); + -moz-transform: scale(1, 1); + -ms-transform: scale(1, 1); + -o-transform: scale(1, 1); + transform: scale(1, 1); } } + +@-moz-keyframes bouncedelay { + 0%, 80%, 100% { + -webkit-transform: scale(0.25, 0.25); + -moz-transform: scale(0.25, 0.25); + -ms-transform: scale(0.25, 0.25); + -o-transform: scale(0.25, 0.25); + transform: scale(0.25, 0.25); } + + 40% { + -webkit-transform: scale(1, 1); + -moz-transform: scale(1, 1); + -ms-transform: scale(1, 1); + -o-transform: scale(1, 1); + transform: scale(1, 1); } } + +@-ms-keyframes bouncedelay { + 0%, 80%, 100% { + -webkit-transform: scale(0.25, 0.25); + -moz-transform: scale(0.25, 0.25); + -ms-transform: scale(0.25, 0.25); + -o-transform: scale(0.25, 0.25); + transform: scale(0.25, 0.25); } + + 40% { + -webkit-transform: scale(1, 1); + -moz-transform: scale(1, 1); + -ms-transform: scale(1, 1); + -o-transform: scale(1, 1); + transform: scale(1, 1); } } + +@keyframes bouncedelay { + 0%, 80%, 100% { + -webkit-transform: scale(0.25, 0.25); + -moz-transform: scale(0.25, 0.25); + -ms-transform: scale(0.25, 0.25); + -o-transform: scale(0.25, 0.25); + transform: scale(0.25, 0.25); } + + 40% { + -webkit-transform: scale(1, 1); + -moz-transform: scale(1, 1); + -ms-transform: scale(1, 1); + -o-transform: scale(1, 1); + transform: scale(1, 1); } } + +.co-dialog .modal-body { + padding: 10px; + min-height: 100px; +} + +.co-dialog .modal-content { + border-radius: 0px; +} + +.co-dialog.fatal-error .modal-content { + padding-left: 175px; +} + +.co-dialog.fatal-error .alert-icon-container-container { + position: absolute; + top: -36px; + left: -175px; + bottom: 20px; +} + +.co-dialog.fatal-error .alert-icon-container { + height: 100%; + display: table; +} + +.co-dialog.fatal-error .alert-icon { + display: table-cell; + vertical-align: middle; + border-right: 1px solid #eee; + margin-right: 20px; +} + +.co-dialog.fatal-error .alert-icon:before { + content: "\f071"; + font-family: FontAwesome; + font-size: 60px; + padding-left: 50px; + padding-right: 50px; + color: #c53c3f; + text-align: center; +} + + +.co-dialog .modal-header .cor-step-bar { + float: right; +} + +.co-dialog .modal-footer.working { + text-align: left; +} + +.co-dialog .modal-footer.working .cor-loader-inline { + margin-right: 10px; +} + +.co-dialog .modal-footer .left-align { + float: left; + vertical-align: middle; + font-size: 16px; + margin-top: 8px; +} + +.co-dialog .modal-footer .left-align i.fa-warning { + color: #ffba35; + display: inline-block; + margin-right: 6px; +} + +.co-dialog .modal-footer .left-align i.fa-check { + color: green; + display: inline-block; + margin-right: 6px; +} + +.co-step-bar .co-step-element { + cursor: default; + display: inline-block; + width: 28px; + height: 28px; + + position: relative; + color: #ddd; + + text-align: center; + line-height: 24px; + font-size: 16px; +} + +.co-step-bar .co-step-element.text { + margin-left: 24px; + background: white; +} + +.co-step-bar .co-step-element.icon { + margin-left: 22px; +} + +.co-step-bar .co-step-element:first-child { + margin-left: 0px; +} + +.co-step-bar .co-step-element.active { + color: #53a3d9; +} + +.co-step-bar .co-step-element:first-child:before { + display: none; +} + +.co-step-bar .co-step-element:before { + content: ""; + position: absolute; + top: 12px; + width: 14px; + border-top: 2px solid #ddd; +} + +.co-step-bar .co-step-element.icon:before { + left: -20px; +} + +.co-step-bar .co-step-element.text:before { + left: -22px; +} + +.co-step-bar .co-step-element.active:before { + border-top: 2px solid #53a3d9; +} + + +.co-step-bar .co-step-element.text { + border-radius: 100%; + border: 2px solid #ddd; +} + +.co-step-bar .co-step-element.text.active { + border: 2px solid #53a3d9; +} + +@media screen and (min-width: 900px) { + .co-dialog .modal-dialog { + width: 800px; + } +} + +.co-alert .co-step-bar { + float: right; + margin-top: 6px; +} \ No newline at end of file diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html index 2c8397ecb..6b40f1fd5 100644 --- a/static/directives/config/config-setup-tool.html +++ b/static/directives/config/config-setup-tool.html @@ -553,42 +553,15 @@ -