From 180019380234421fd89617131f64d7e6343b580f Mon Sep 17 00:00:00 2001 From: termie Date: Thu, 21 Nov 2013 14:48:36 -0800 Subject: [PATCH 1/8] update deployment add a bunch of boilerplate from fbone based on MIT licensed code at: https://github.com/imwilsonxu/fbone --- README.md | 7 -- README.rst | 35 ++++++ alembic.ini | 7 ++ alembic/env.py | 9 +- refstack/app.py | 209 ++++++++++++++++++++++++++++------- refstack/config.py | 78 +++++++++++++ refstack/decorators.py | 17 +++ refstack/default_settings.py | 47 ++++++++ refstack/extensions.py | 19 ++++ refstack/utils.py | 106 ++++++++++++++++++ refstack/web.py | 20 +++- 11 files changed, 498 insertions(+), 56 deletions(-) delete mode 100755 README.md create mode 100755 README.rst mode change 100755 => 100644 refstack/app.py create mode 100644 refstack/config.py create mode 100644 refstack/decorators.py create mode 100644 refstack/default_settings.py create mode 100644 refstack/extensions.py create mode 100644 refstack/utils.py diff --git a/README.md b/README.md deleted file mode 100755 index cb46fb67..00000000 --- a/README.md +++ /dev/null @@ -1,7 +0,0 @@ -RefStack -======== - -Vendor-facing API for registration of interop-compliance endpoints and credentials for on-demand testing. - -Running at http://refstack.org -See (living) documentation at https://etherpad.openstack.org/RefStackBlueprint diff --git a/README.rst b/README.rst new file mode 100755 index 00000000..87f262dc --- /dev/null +++ b/README.rst @@ -0,0 +1,35 @@ +RefStack +======== + +Vendor-facing API for registration of interop-compliance endpoints and credentials for on-demand testing. + +Running at http://refstack.org +See (living) documentation at https://etherpad.openstack.org/RefStackBlueprint + + +Okay, I'm Sold, How Do I Run This Myself? +----------------------------------------- + +This is our documentation for how we get this set up:: + + # Git you clonin' + git clone http://github.com/openstack-ops/refstack + + cd refstack + + # Setup or update the database + # NOTE: you are going to have to modify the db connection string in + # `alembic.ini` to get this working + # PROTIP: if you just want to test this out, use `-n alembic_sqlite` to + # make a local sqlite db + # $ alembic -n alembic_sqlite update head + alembic update head + + # Plug this bad boy into your server infrastructure. + # We use nginx and gunicorn, you may use something else if you are smarter + # than we are. + # For the most basic setup that you can try right now, just kick off + # gunicorn: + gunicorn refstack.web:app + + # Now browse to http://localhost:8000 diff --git a/alembic.ini b/alembic.ini index 848a3873..d26c4925 100755 --- a/alembic.ini +++ b/alembic.ini @@ -14,6 +14,13 @@ script_location = alembic sqlalchemy.url = driver://user:pass@localhost/dbname +[alembic_sqlite] +# path to migration scripts +script_location = alembic + +sqlalchemy.url = sqlite:///refstack.db + + # Logging configuration [loggers] keys = root,sqlalchemy,alembic diff --git a/alembic/env.py b/alembic/env.py index afed5bdd..6449b1da 100755 --- a/alembic/env.py +++ b/alembic/env.py @@ -24,14 +24,10 @@ sys.path.append("./") from alembic import context from sqlalchemy import engine_from_config, pool from logging.config import fileConfig -from refstack import app # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config -cur_db_uri = config.get_section_option('alembic', 'sqlalchemy.url') -my_db_uri = 'sqlite:////tmp/refstack.db' # app.config.get('SQLALCHEMY_DATABASE_URI', cur_db_uri) -config.set_section_option('alembic', 'sqlalchemy.url', my_db_uri) # Interpret the config file for Python logging. # This line sets up loggers basically. @@ -45,6 +41,10 @@ fileConfig(config.config_file_name) from refstack.models import * target_metadata = Base.metadata +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. def run_migrations_offline(): """Run migrations in 'offline' mode. @@ -92,4 +92,3 @@ if context.is_offline_mode(): run_migrations_offline() else: run_migrations_online() - diff --git a/refstack/app.py b/refstack/app.py old mode 100755 new mode 100644 index eda49674..fbdcfb3d --- a/refstack/app.py +++ b/refstack/app.py @@ -1,50 +1,181 @@ -#!/usr/bin/env python -# -# Copyright (c) 2013 Piston Cloud Computing, Inc. -# All Rights Reserved. -# -# 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. +# -*- coding: utf-8 -*- -"""Common Flask app config.""" +# This file based on MIT licensed code at: https://github.com/imwilsonxu/fbone import os -import flask +from flask import Flask, request, render_template +#from flask.ext.babel import Babel + +from .config import DefaultConfig +#from .user import User, user +#from .settings import settings +#from .frontend import frontend +#from .api import api +#from .admin import admin +#from .extensions import db, mail, cache, login_manager, oid +from .extensions import db, mail, login_manager, oid +from .utils import INSTANCE_FOLDER_PATH -#db_path = os.path.abspath( -# os.path.join(os.path.basename(__file__), "../")) +# For import * +__all__ = ['create_app'] -db_path = 'tmp' +DEFAULT_BLUEPRINTS = tuple() +# frontend, +# user, +# settings, +# api, +# admin, +#) -app = flask.Flask(__name__) +def create_app(config=None, app_name=None, blueprints=None): + """Create a Flask app.""" + + if app_name is None: + app_name = DefaultConfig.PROJECT + if blueprints is None: + blueprints = DEFAULT_BLUEPRINTS + + app = Flask(app_name, + instance_path=INSTANCE_FOLDER_PATH, + instance_relative_config=True) + configure_app(app, config) + configure_hook(app) + configure_blueprints(app, blueprints) + # NOTE(termie): commented out until we switch the web config to this + #configure_extensions(app) + configure_logging(app) + configure_template_filters(app) + configure_error_handlers(app) + + return app -app.config['MAILGUN_KEY'] = '#@#@#@#@' -app.config['MAILGUN_DOMAIN'] = 'refstack.org' -app.config['SECRET_KEY'] = '#@#@#@#@#@#@#@#@#@#@#@#@#@#@#@#@' -app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get( - 'DATABASE_URL', 'sqlite:///%s/refstack.db' % (db_path)) -app.config['DEBUG'] = True -app.config['SECURITY_PASSWORD_HASH'] = 'sha512_crypt' -app.config['SECURITY_PASSWORD_SALT'] = app.config['SECRET_KEY'] -app.config['SECURITY_POST_LOGIN_VIEW'] = 'dashboard' -app.config['SECURITY_RECOVERABLE'] = True -app.config['SECURITY_REGISTERABLE'] = True -app.config['SECURITY_EMAIL_SENDER'] = "refstack.org" -app.config['MAIL_SERVER'] = 'smtp.refstack.org' -app.config['MAIL_PORT'] = 465 -app.config['MAIL_USE_SSL'] = True -app.config['MAIL_USERNAME'] = 'postmaster@refstack.org' -app.config['MAIL_PASSWORD'] = '1234' +def configure_app(app, config=None): + """Different ways of configurations.""" + + # http://flask.pocoo.org/docs/api/#configuration + app.config.from_object(DefaultConfig) + + # http://flask.pocoo.org/docs/config/#instance-folders + app.config.from_pyfile('production.cfg', silent=True) + + if config: + app.config.from_object(config) + + # Use instance folder instead of env variables to make deployment easier. + #app.config.from_envvar('%s_APP_CONFIG' % DefaultConfig.PROJECT.upper(), silent=True) + + +def configure_extensions(app): + # flask-sqlalchemy + db.init_app(app) + + # flask-mail + mail.init_app(app) + + # flask-cache + cache.init_app(app) + + # flask-babel + babel = Babel(app) + + @babel.localeselector + def get_locale(): + accept_languages = app.config.get('ACCEPT_LANGUAGES') + return request.accept_languages.best_match(accept_languages) + + # flask-login + login_manager.login_view = 'frontend.login' + login_manager.refresh_view = 'frontend.reauth' + + @login_manager.user_loader + def load_user(id): + return User.query.get(id) + login_manager.setup_app(app) + + # flask-openid + oid.init_app(app) + + +def configure_blueprints(app, blueprints): + """Configure blueprints in views.""" + + for blueprint in blueprints: + app.register_blueprint(blueprint) + + +def configure_template_filters(app): + + @app.template_filter() + def pretty_date(value): + return pretty_date(value) + + @app.template_filter() + def format_date(value, format='%Y-%m-%d'): + return value.strftime(format) + + +def configure_logging(app): + """Configure file(info) and email(error) logging.""" + + if app.debug or app.testing: + # Skip debug and test mode. Just check standard output. + return + + import logging + from logging.handlers import SMTPHandler + + # Set info level on logger, which might be overwritten by handers. + # Suppress DEBUG messages. + app.logger.setLevel(logging.INFO) + + info_log = os.path.join(app.config['LOG_FOLDER'], 'info.log') + info_file_handler = logging.handlers.RotatingFileHandler(info_log, maxBytes=100000, backupCount=10) + info_file_handler.setLevel(logging.INFO) + info_file_handler.setFormatter(logging.Formatter( + '%(asctime)s %(levelname)s: %(message)s ' + '[in %(pathname)s:%(lineno)d]') + ) + app.logger.addHandler(info_file_handler) + + # Testing + #app.logger.info("testing info.") + #app.logger.warn("testing warn.") + #app.logger.error("testing error.") + + mail_handler = SMTPHandler(app.config['MAIL_SERVER'], + app.config['MAIL_USERNAME'], + app.config['ADMINS'], + 'O_ops... %s failed!' % app.config['PROJECT'], + (app.config['MAIL_USERNAME'], + app.config['MAIL_PASSWORD'])) + mail_handler.setLevel(logging.ERROR) + mail_handler.setFormatter(logging.Formatter( + '%(asctime)s %(levelname)s: %(message)s ' + '[in %(pathname)s:%(lineno)d]') + ) + app.logger.addHandler(mail_handler) + + +def configure_hook(app): + @app.before_request + def before_request(): + pass + + +def configure_error_handlers(app): + + @app.errorhandler(403) + def forbidden_page(error): + return render_template("errors/forbidden_page.html"), 403 + + @app.errorhandler(404) + def page_not_found(error): + return render_template("errors/page_not_found.html"), 404 + + @app.errorhandler(500) + def server_error_page(error): + return render_template("errors/server_error.html"), 500 diff --git a/refstack/config.py b/refstack/config.py new file mode 100644 index 00000000..168efb46 --- /dev/null +++ b/refstack/config.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +# This file based on MIT licensed code at: https://github.com/imwilsonxu/fbone + +import os + +from utils import make_dir, INSTANCE_FOLDER_PATH + + +class BaseConfig(object): + + PROJECT = "fbone" + + # Get app root path, also can use flask.root_path. + # ../../config.py + PROJECT_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) + + DEBUG = False + TESTING = False + + ADMINS = ['youremail@yourdomain.com'] + + # http://flask.pocoo.org/docs/quickstart/#sessions + SECRET_KEY = 'secret key' + + LOG_FOLDER = os.path.join(INSTANCE_FOLDER_PATH, 'logs') + make_dir(LOG_FOLDER) + + # Fild upload, should override in production. + # Limited the maximum allowed payload to 16 megabytes. + # http://flask.pocoo.org/docs/patterns/fileuploads/#improving-uploads + MAX_CONTENT_LENGTH = 16 * 1024 * 1024 + UPLOAD_FOLDER = os.path.join(INSTANCE_FOLDER_PATH, 'uploads') + make_dir(UPLOAD_FOLDER) + + +class DefaultConfig(BaseConfig): + + DEBUG = True + + # Flask-Sqlalchemy: http://packages.python.org/Flask-SQLAlchemy/config.html + SQLALCHEMY_ECHO = True + # SQLITE for prototyping. + SQLALCHEMY_DATABASE_URI = 'sqlite:///' + INSTANCE_FOLDER_PATH + '/db.sqlite' + # MYSQL for production. + #SQLALCHEMY_DATABASE_URI = 'mysql://username:password@server/db?charset=utf8' + + # Flask-babel: http://pythonhosted.org/Flask-Babel/ + ACCEPT_LANGUAGES = ['zh'] + BABEL_DEFAULT_LOCALE = 'en' + + # Flask-cache: http://pythonhosted.org/Flask-Cache/ + CACHE_TYPE = 'simple' + CACHE_DEFAULT_TIMEOUT = 60 + + # Flask-mail: http://pythonhosted.org/flask-mail/ + # https://bitbucket.org/danjac/flask-mail/issue/3/problem-with-gmails-smtp-server + MAIL_DEBUG = DEBUG + MAIL_SERVER = 'smtp.gmail.com' + MAIL_PORT = 587 + MAIL_USE_TLS = True + MAIL_USE_SSL = False + # Should put MAIL_USERNAME and MAIL_PASSWORD in production under instance folder. + MAIL_USERNAME = 'yourmail@gmail.com' + MAIL_PASSWORD = 'yourpass' + MAIL_DEFAULT_SENDER = MAIL_USERNAME + + # Flask-openid: http://pythonhosted.org/Flask-OpenID/ + OPENID_FS_STORE_PATH = os.path.join(INSTANCE_FOLDER_PATH, 'openid') + make_dir(OPENID_FS_STORE_PATH) + + +class TestConfig(BaseConfig): + TESTING = True + WTF_CSRF_ENABLED = False + + SQLALCHEMY_ECHO = False + SQLALCHEMY_DATABASE_URI = 'sqlite://' diff --git a/refstack/decorators.py b/refstack/decorators.py new file mode 100644 index 00000000..2e0539f8 --- /dev/null +++ b/refstack/decorators.py @@ -0,0 +1,17 @@ +#-*- coding: utf-8 -*- + +# This file based on MIT licensed code at: https://github.com/imwilsonxu/fbone + +from functools import wraps + +from flask import abort +from flask.ext.login import current_user + + +def admin_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if not current_user.is_admin(): + abort(403) + return f(*args, **kwargs) + return decorated_function diff --git a/refstack/default_settings.py b/refstack/default_settings.py new file mode 100644 index 00000000..a02e8314 --- /dev/null +++ b/refstack/default_settings.py @@ -0,0 +1,47 @@ +# +# Copyright (c) 2013 Piston Cloud Computing, Inc. +# All Rights Reserved. +# +# 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. +import os + +############################################################### +# +# +# THIS FILE IS NOT USED +# (leaveing it around briefly for reference) +# +# +############################################################### + +db_path = '/tmp' + + +class Default(object): + MAILGUN_KEY = '#@#@#@#@' + MAILGUN_DOMAIN = 'refstack.org' + SECRET_KEY = '#@#@#@#@#@#@#@#@#@#@#@#@#@#@#@#@' + SQLALCHEMY_DATABASE_URI = os.environ.get( + 'DATABASE_URL', 'sqlite:///%s/refstack.db' % (db_path)) + DEBUG = True + SECURITY_PASSWORD_HASH = 'sha512_crypt' + SECURITY_PASSWORD_SALT = SECRET_KEY + SECURITY_POST_LOGIN_VIEW = 'dashboard' + SECURITY_RECOVERABLE = True + SECURITY_REGISTERABLE = True + SECURITY_EMAIL_SENDER = "refstack.org" + MAIL_SERVER = 'smtp.refstack.org' + MAIL_PORT = 465 + MAIL_USE_SSL = True + MAIL_USERNAME = 'postmaster@refstack.org' + MAIL_PASSWORD = '1234' diff --git a/refstack/extensions.py b/refstack/extensions.py new file mode 100644 index 00000000..e83656c8 --- /dev/null +++ b/refstack/extensions.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +# This file based on MIT licensed code at: https://github.com/imwilsonxu/fbone + +from flask.ext.sqlalchemy import SQLAlchemy +db = SQLAlchemy() + +from flask.ext.mail import Mail +mail = Mail() + +# TODO(termie): not used yet +#from flask.ext.cache import Cache +#cache = Cache() + +from flask.ext.login import LoginManager +login_manager = LoginManager() + +from flask.ext.openid import OpenID +oid = OpenID() diff --git a/refstack/utils.py b/refstack/utils.py new file mode 100644 index 00000000..5def7aa9 --- /dev/null +++ b/refstack/utils.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- + +# This file based on MIT licensed code at: https://github.com/imwilsonxu/fbone + +""" + Utils has nothing to do with models and views. +""" + +import string +import random +import os + +from datetime import datetime + + +# Instance folder path, make it independent. +INSTANCE_FOLDER_PATH = os.path.join('/tmp', 'instance') + +ALLOWED_AVATAR_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif']) + +# Form validation + +USERNAME_LEN_MIN = 4 +USERNAME_LEN_MAX = 25 + +REALNAME_LEN_MIN = 4 +REALNAME_LEN_MAX = 25 + +PASSWORD_LEN_MIN = 6 +PASSWORD_LEN_MAX = 16 + +AGE_MIN = 1 +AGE_MAX = 300 + +DEPOSIT_MIN = 0.00 +DEPOSIT_MAX = 9999999999.99 + +# Sex type. +MALE = 1 +FEMALE = 2 +OTHER = 9 +SEX_TYPE = { + MALE: u'Male', + FEMALE: u'Female', + OTHER: u'Other', +} + +# Model +STRING_LEN = 64 + + +def get_current_time(): + return datetime.utcnow() + + +def pretty_date(dt, default=None): + """ + Returns string representing "time since" e.g. + 3 days ago, 5 hours ago etc. + Ref: https://bitbucket.org/danjac/newsmeme/src/a281babb9ca3/newsmeme/ + """ + + if default is None: + default = 'just now' + + now = datetime.utcnow() + diff = now - dt + + periods = ( + (diff.days / 365, 'year', 'years'), + (diff.days / 30, 'month', 'months'), + (diff.days / 7, 'week', 'weeks'), + (diff.days, 'day', 'days'), + (diff.seconds / 3600, 'hour', 'hours'), + (diff.seconds / 60, 'minute', 'minutes'), + (diff.seconds, 'second', 'seconds'), + ) + + for period, singular, plural in periods: + + if not period: + continue + + if period == 1: + return u'%d %s ago' % (period, singular) + else: + return u'%d %s ago' % (period, plural) + + return default + + +def allowed_file(filename): + return '.' in filename and filename.rsplit('.', 1)[1] in ALLOWED_AVATAR_EXTENSIONS + + +def id_generator(size=10, chars=string.ascii_letters + string.digits): + #return base64.urlsafe_b64encode(os.urandom(size)) + return ''.join(random.choice(chars) for x in range(size)) + + +def make_dir(dir_path): + try: + if not os.path.exists(dir_path): + os.mkdir(dir_path) + except Exception, e: + raise e diff --git a/refstack/web.py b/refstack/web.py index 3a7d60bc..675ab692 100755 --- a/refstack/web.py +++ b/refstack/web.py @@ -13,6 +13,10 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +import os +import logging + +import flask from flask import Flask, abort, flash, request, redirect, url_for, \ render_template, g, session from flask_openid import OpenID @@ -24,11 +28,20 @@ from wtforms import Form, BooleanField, TextField, \ PasswordField, validators from flask_mail import Mail -from refstack.app import app +from refstack import app as base_app +from refstack import utils from refstack.models import * -mail = Mail(app) +# TODO(termie): temporary hack for first-run experience +utils.make_dir(utils.INSTANCE_FOLDER_PATH) + +# TODO(termie): transition all the routes below to blueprints +# TODO(termie): use extensions setup from the create_app() call + +app = base_app.create_app() + +mail = Mail(app) # setup flask-openid oid = OpenID(app) admin = Admin(app, base_template='admin/master.html') @@ -270,6 +283,3 @@ def logout(): session.pop('openid', None) flash(u'You have been signed out') return redirect(oid.get_next_url()) - - - From ca4b81866a64e6a2c98e0b172166b8cbf46b82ea Mon Sep 17 00:00:00 2001 From: termie Date: Tue, 26 Nov 2013 11:01:24 -0800 Subject: [PATCH 2/8] use local dir for config by default --- README.rst | 10 ++++++++++ alembic.ini | 2 +- refstack/app.py | 25 +++++++++++++++++++++---- refstack/config.py | 9 ++++----- refstack/utils.py | 22 ++++++++++++++++------ refstack/web.py | 3 --- 6 files changed, 52 insertions(+), 19 deletions(-) diff --git a/README.rst b/README.rst index 87f262dc..83fb8c86 100755 --- a/README.rst +++ b/README.rst @@ -32,4 +32,14 @@ This is our documentation for how we get this set up:: # gunicorn: gunicorn refstack.web:app + # To actually configure this winner, check out the config section and + # crack open refstack.cfg in vim. + # `vim refstack.cfg` + # Now browse to http://localhost:8000 + + +Configuration +------------- + +Coming soon! diff --git a/alembic.ini b/alembic.ini index d26c4925..80fbbdee 100755 --- a/alembic.ini +++ b/alembic.ini @@ -18,7 +18,7 @@ sqlalchemy.url = driver://user:pass@localhost/dbname # path to migration scripts script_location = alembic -sqlalchemy.url = sqlite:///refstack.db +sqlalchemy.url = sqlite:///db.sqlite # Logging configuration diff --git a/refstack/app.py b/refstack/app.py index fbdcfb3d..7d36e6d1 100644 --- a/refstack/app.py +++ b/refstack/app.py @@ -15,7 +15,11 @@ from .config import DefaultConfig #from .admin import admin #from .extensions import db, mail, cache, login_manager, oid from .extensions import db, mail, login_manager, oid -from .utils import INSTANCE_FOLDER_PATH + +from refstack import utils + + +INSTANCE_FOLDER_PATH = utils.INSTANCE_FOLDER_PATH # For import * @@ -38,9 +42,17 @@ def create_app(config=None, app_name=None, blueprints=None): if blueprints is None: blueprints = DEFAULT_BLUEPRINTS + # NOTE(termie): Flask has this new instance_path stuff that allows + # you to keep config and such in different places, but I don't really + # see how that is going to be very helpful, so we're going to stick + # to using config relative to the root unless we explicitly set such + # a path in the INSTANCE_FOLDER_PATH environment variable. app = Flask(app_name, instance_path=INSTANCE_FOLDER_PATH, instance_relative_config=True) + + + configure_app(app, config) configure_hook(app) configure_blueprints(app, blueprints) @@ -50,6 +62,9 @@ def create_app(config=None, app_name=None, blueprints=None): configure_template_filters(app) configure_error_handlers(app) + if app.debug: + print utils.dump_config(app) + return app @@ -59,8 +74,10 @@ def configure_app(app, config=None): # http://flask.pocoo.org/docs/api/#configuration app.config.from_object(DefaultConfig) - # http://flask.pocoo.org/docs/config/#instance-folders - app.config.from_pyfile('production.cfg', silent=True) + # If we've set the INSTANCE_FOLDER_PATH environment var, this may be + # loaded from an instance folder, otherwise relative to flask.root_path. + # http://flask.pocoo.org/docs/config/#instance-folders + app.config.from_pyfile('refstack.cfg', silent=True) if config: app.config.from_object(config) @@ -128,7 +145,7 @@ def configure_logging(app): import logging from logging.handlers import SMTPHandler - # Set info level on logger, which might be overwritten by handers. + # Set info level on logger, which might be overwritten by handlers. # Suppress DEBUG messages. app.logger.setLevel(logging.INFO) diff --git a/refstack/config.py b/refstack/config.py index 168efb46..b4487eb9 100644 --- a/refstack/config.py +++ b/refstack/config.py @@ -4,16 +4,15 @@ import os -from utils import make_dir, INSTANCE_FOLDER_PATH +from utils import make_dir, INSTANCE_FOLDER_PATH, PROJECT_ROOT class BaseConfig(object): - PROJECT = "fbone" + PROJECT = "refstack" - # Get app root path, also can use flask.root_path. - # ../../config.py - PROJECT_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) + # The app root path, also can use flask.root_path. + PROJECT_ROOT = PROJECT_ROOT DEBUG = False TESTING = False diff --git a/refstack/utils.py b/refstack/utils.py index 5def7aa9..45be994d 100644 --- a/refstack/utils.py +++ b/refstack/utils.py @@ -6,15 +6,17 @@ Utils has nothing to do with models and views. """ -import string -import random -import os - from datetime import datetime +import logging +import os +import pprint +import random +import string -# Instance folder path, make it independent. -INSTANCE_FOLDER_PATH = os.path.join('/tmp', 'instance') +# Instance folder path, if set, otherwise project root. +PROJECT_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) +INSTANCE_FOLDER_PATH = os.environ.get('INSTANCE_FOLDER_PATH', PROJECT_ROOT) ALLOWED_AVATAR_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif']) @@ -104,3 +106,11 @@ def make_dir(dir_path): os.mkdir(dir_path) except Exception, e: raise e + + +### Begin Non-Fbone stuff + + +def dump_config(app): + """Useful to dump app config for debug purposes.""" + return pprint.pformat(dict(app.config.iteritems())) diff --git a/refstack/web.py b/refstack/web.py index 675ab692..2472b9cc 100755 --- a/refstack/web.py +++ b/refstack/web.py @@ -33,9 +33,6 @@ from refstack import utils from refstack.models import * -# TODO(termie): temporary hack for first-run experience -utils.make_dir(utils.INSTANCE_FOLDER_PATH) - # TODO(termie): transition all the routes below to blueprints # TODO(termie): use extensions setup from the create_app() call From b267be88a68abdf4ed8db0b1f0ed3a9ee4d03485 Mon Sep 17 00:00:00 2001 From: termie Date: Tue, 26 Nov 2013 11:26:36 -0800 Subject: [PATCH 3/8] move db back to flask-sqlalchemy --- refstack/app.py | 41 ++++++++------- refstack/models.py | 122 ++++++++++++++++++-------------------------- refstack/schema.sql | 3 -- 3 files changed, 71 insertions(+), 95 deletions(-) delete mode 100755 refstack/schema.sql diff --git a/refstack/app.py b/refstack/app.py index 7d36e6d1..1ce25609 100644 --- a/refstack/app.py +++ b/refstack/app.py @@ -56,8 +56,7 @@ def create_app(config=None, app_name=None, blueprints=None): configure_app(app, config) configure_hook(app) configure_blueprints(app, blueprints) - # NOTE(termie): commented out until we switch the web config to this - #configure_extensions(app) + configure_extensions(app) configure_logging(app) configure_template_filters(app) configure_error_handlers(app) @@ -90,31 +89,31 @@ def configure_extensions(app): # flask-sqlalchemy db.init_app(app) - # flask-mail - mail.init_app(app) + ## flask-mail + #mail.init_app(app) - # flask-cache - cache.init_app(app) + ## flask-cache + #cache.init_app(app) - # flask-babel - babel = Babel(app) + ## flask-babel + #babel = Babel(app) - @babel.localeselector - def get_locale(): - accept_languages = app.config.get('ACCEPT_LANGUAGES') - return request.accept_languages.best_match(accept_languages) + #@babel.localeselector + #def get_locale(): + # accept_languages = app.config.get('ACCEPT_LANGUAGES') + # return request.accept_languages.best_match(accept_languages) - # flask-login - login_manager.login_view = 'frontend.login' - login_manager.refresh_view = 'frontend.reauth' + ## flask-login + #login_manager.login_view = 'frontend.login' + #login_manager.refresh_view = 'frontend.reauth' - @login_manager.user_loader - def load_user(id): - return User.query.get(id) - login_manager.setup_app(app) + #@login_manager.user_loader + #def load_user(id): + # return User.query.get(id) + #login_manager.setup_app(app) - # flask-openid - oid.init_app(app) + ## flask-openid + #oid.init_app(app) def configure_blueprints(app, blueprints): diff --git a/refstack/models.py b/refstack/models.py index 3b0f20b0..79bf662d 100755 --- a/refstack/models.py +++ b/refstack/models.py @@ -13,44 +13,25 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -"""striaght up sqlalchemy declarative_base model structure. - *I created this because i was having a problem getting - the cli to use the models that were generated for the flask - webapp. The plan is to use this for both. But I have not - started my serious efforts on the web interface. dl 10.2013 - - *For now in dev I have this database in /tmp we can talk - about someplace else for it by default. -""" from datetime import datetime -from sqlalchemy import create_engine -from sqlalchemy.orm import scoped_session, sessionmaker,relationship, backref -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, Binary, Boolean -engine = create_engine('sqlite:////tmp/refstack.db', convert_unicode=True) -db = scoped_session(sessionmaker(autocommit=False, - autoflush=False, - bind=engine)) - -Base = declarative_base() -Base.query = db.query_property() +from .extensions import db -class User(Base): +class User(db.Model): __tablename__ = 'user' - id = Column(Integer, primary_key=True) - vendor_id = Column(Integer, ForeignKey('vendor.id')) - vendor = relationship('Vendor', - backref=backref('clouds', + id = db.Column(db.Integer, primary_key=True) + vendor_id = db.Column(db.Integer, db.ForeignKey('vendor.id')) + vendor = db.relationship('Vendor', + backref=db.backref('clouds', lazy='dynamic')) - name = Column(String(60)) - email = Column(String(200), unique=True) - email_verified = Column(Boolean) - openid = Column(String(200), unique=True) - authorized = Column(Boolean, default=False) - su = Column(Boolean, default=False) + name = db.Column(db.String(60)) + email = db.Column(db.String(200), unique=True) + email_verified = db.Column(db.Boolean) + openid = db.Column(db.String(200), unique=True) + authorized = db.Column(db.Boolean, default=False) + su = db.Column(db.Boolean, default=False) def __init__(self, name, email, openid): self.name = name @@ -60,64 +41,63 @@ class User(Base): def __str__(self): return self.name - """ Note: The vendor list will be pre-populated from the sponsoring company database. TODO: better define the vendor object and its relationship with user it needs the ability to facilitate a login. """ -class Vendor(Base): +class Vendor(db.Model): __tablename__ = 'vendor' - id = Column(Integer, primary_key=True) - vendor_name = Column(String(80), unique=True) - contact_email = Column(String(120), unique=True) - contact_name = Column(String(120), unique=False) + id = db.Column(db.Integer, primary_key=True) + vendor_name = db.Column(db.String(80), unique=True) + contact_email = db.Column(db.String(120), unique=True) + contact_name = db.Column(db.String(120), unique=False) def __str__(self): return self.vendor_name -class Cloud(Base): +class Cloud(db.Model): """*need to take the time to descibe this stuff in detail. """ __tablename__ = 'cloud' - id = Column(Integer, primary_key=True) + id = db.Column(db.Integer, primary_key=True) - label = Column(String(60), unique=False) - endpoint = Column(String(120), unique=True) - test_user = Column(String(80), unique=False) - test_key = Column(String(80), unique=False) - admin_endpoint = Column(String(120), unique=False) - admin_user = Column(String(80), unique=False) - admin_key = Column(String(80), unique=False) + label = db.Column(db.String(60), unique=False) + endpoint = db.Column(db.String(120), unique=True) + test_user = db.Column(db.String(80), unique=False) + test_key = db.Column(db.String(80), unique=False) + admin_endpoint = db.Column(db.String(120), unique=False) + admin_user = db.Column(db.String(80), unique=False) + admin_key = db.Column(db.String(80), unique=False) - user_id = Column(Integer, ForeignKey('user.id')) - user = relationship('User', - backref=backref('clouds',lazy='dynamic')) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + user = db.relationship('User', + backref=db.backref('clouds',lazy='dynamic')) -class Test(Base): +class Test(db.Model): __tablename__ = 'test' - id = Column(Integer, primary_key=True) - cloud_id = Column(Integer, ForeignKey('cloud.id')) - cloud = relationship('Cloud', - backref=backref('tests',lazy='dynamic')) - config = Column(String(4096)) + id = db.Column(db.Integer, primary_key=True) + cloud_id = db.Column(db.Integer, db.ForeignKey('cloud.id')) + cloud = db.relationship('Cloud', + backref=db.backref('tests',lazy='dynamic')) + config = db.Column(db.String(4096)) def __init__(self, cloud_id): self.cloud_id = cloud_id -class TestStatus(Base): +class TestStatus(db.Model): __tablename__ = 'test_status' - id = Column(Integer, primary_key=True) - test_id = Column(Integer, ForeignKey('test.id')) - test = relationship('Test', - backref=backref('status',lazy='dynamic')) - message = Column(String(1024)) - finished = Column(Boolean, default=False) - timestamp = Column(DateTime, default=datetime.now) + id = db.Column(db.Integer, primary_key=True) + test_id = db.Column(db.Integer, db.ForeignKey('test.id')) + test = db.relationship('Test', + backref=db.backref('status',lazy='dynamic')) + message = db.Column(db.String(1024)) + finished = db.Column(db.Boolean, default=False) + timestamp = db.Column(db.DateTime, default=datetime.now) def __init__(self,test_id, message, finished=False): @@ -126,14 +106,14 @@ class TestStatus(Base): self.finished = finished -class TestResults(Base): +class TestResults(db.Model): __tablename__ = 'test_results' - id = Column(Integer, primary_key=True) - test_id = Column(Integer, ForeignKey('test.id')) - test = relationship('Test', - backref=backref('results',lazy='dynamic')) - timestamp = Column(DateTime, default=datetime.now) - subunit = Column(String(8192)) - blob = Column(Binary) + id = db.Column(db.Integer, primary_key=True) + test_id = db.Column(db.Integer, db.ForeignKey('test.id')) + test = db.relationship('Test', + backref=db.backref('results',lazy='dynamic')) + timestamp = db.Column(db.DateTime, default=datetime.now) + subunit = db.Column(db.String(8192)) + blob = db.Column(db.Binary) diff --git a/refstack/schema.sql b/refstack/schema.sql deleted file mode 100755 index 4aaa01b4..00000000 --- a/refstack/schema.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE TABLE IF NOT EXISTS VENDORS - (vendor_id integer primary key asc autoincrement, - vendor_name TEXT NOT NULL); From 8e4a5b2dd8a3e0fa458d5e2cccc35f50da565b9b Mon Sep 17 00:00:00 2001 From: termie Date: Tue, 26 Nov 2013 12:24:58 -0800 Subject: [PATCH 4/8] update flask-admin and move it to a module --- refstack/admin.py | 27 +++++++++++++++++++ refstack/app.py | 13 ++++++--- refstack/extensions.py | 3 +++ .../admin/{master.html => master_legacy.html} | 0 refstack/web.py | 20 +++++--------- requirements.txt | 8 +++--- 6 files changed, 50 insertions(+), 21 deletions(-) create mode 100644 refstack/admin.py rename refstack/templates/admin/{master.html => master_legacy.html} (100%) diff --git a/refstack/admin.py b/refstack/admin.py new file mode 100644 index 00000000..79955e89 --- /dev/null +++ b/refstack/admin.py @@ -0,0 +1,27 @@ + +import flask +from flask.ext.admin.contrib import sqla + +from refstack import models + +# Global admin object +from .extensions import admin +from .extensions import db + + +class SecureView(sqla.ModelView): + def is_accessible(self): + # let us look at the admin if we're in debug mode + if flask.current_app.debug: + return True + return flask.g.user.su is not False + + +def init_app(app): + admin.init_app(app) + + +def configure_admin(): + admin.add_view(SecureView(models.Cloud, db.session)) + admin.add_view(SecureView(models.User, db.session)) + admin.add_view(SecureView(models.Vendor, db.session)) diff --git a/refstack/app.py b/refstack/app.py index 1ce25609..f15a7615 100644 --- a/refstack/app.py +++ b/refstack/app.py @@ -14,7 +14,10 @@ from .config import DefaultConfig #from .api import api #from .admin import admin #from .extensions import db, mail, cache, login_manager, oid -from .extensions import db, mail, login_manager, oid +from refstack import admin +from .extensions import db +from .extensions import oid + from refstack import utils @@ -89,6 +92,10 @@ def configure_extensions(app): # flask-sqlalchemy db.init_app(app) + # flask-admin + admin.init_app(app) + admin.configure_admin() + ## flask-mail #mail.init_app(app) @@ -112,8 +119,8 @@ def configure_extensions(app): # return User.query.get(id) #login_manager.setup_app(app) - ## flask-openid - #oid.init_app(app) + # flask-openid + oid.init_app(app) def configure_blueprints(app, blueprints): diff --git a/refstack/extensions.py b/refstack/extensions.py index e83656c8..6b36240f 100644 --- a/refstack/extensions.py +++ b/refstack/extensions.py @@ -2,6 +2,9 @@ # This file based on MIT licensed code at: https://github.com/imwilsonxu/fbone +from flask.ext.admin import Admin +admin = Admin() + from flask.ext.sqlalchemy import SQLAlchemy db = SQLAlchemy() diff --git a/refstack/templates/admin/master.html b/refstack/templates/admin/master_legacy.html similarity index 100% rename from refstack/templates/admin/master.html rename to refstack/templates/admin/master_legacy.html diff --git a/refstack/web.py b/refstack/web.py index 2472b9cc..09dff0d5 100755 --- a/refstack/web.py +++ b/refstack/web.py @@ -30,7 +30,12 @@ from flask_mail import Mail from refstack import app as base_app from refstack import utils -from refstack.models import * +from refstack.extensions import oid +from refstack.models import Cloud +from refstack.models import Test +from refstack.models import TestResults +from refstack.models import TestStatus +from refstack.models import Vendor # TODO(termie): transition all the routes below to blueprints @@ -39,19 +44,6 @@ from refstack.models import * app = base_app.create_app() mail = Mail(app) -# setup flask-openid -oid = OpenID(app) -admin = Admin(app, base_template='admin/master.html') - - -class SecureView(ModelView): - def is_accessible(self): - return g.user.su is not False - - -admin.add_view(SecureView(Vendor, db)) -admin.add_view(SecureView(Cloud, db)) -admin.add_view(SecureView(User, db)) @app.before_request diff --git a/requirements.txt b/requirements.txt index 5c640a53..e7a97700 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ -Flask==0.9 -Flask-Admin==1.0.6 +Flask==0.10.1 +Flask-Admin==1.0.7 Flask-Login==0.1.3 Flask-Mail==0.8.2 Flask-OpenID==1.1.1 Flask-Principal==0.3.5 -Flask-SQLAlchemy==0.16 +Flask-SQLAlchemy==1.0 Flask-Security==1.6.3 Flask-WTF==0.8.3 -SQLAlchemy==0.8.1 +SQLAlchemy==0.8.3 WTForms==1.0.4 Werkzeug==0.8.3 alembic==0.5.0 From 822272be09f7bbc178a98bfe174a66d2a1ad7eb7 Mon Sep 17 00:00:00 2001 From: termie Date: Tue, 26 Nov 2013 14:48:35 -0800 Subject: [PATCH 5/8] update db calls to use session --- refstack/templates/layout.html | 14 +++++++------- refstack/web.py | 23 ++++++++++++----------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/refstack/templates/layout.html b/refstack/templates/layout.html index 566df66b..c4fe3f0c 100755 --- a/refstack/templates/layout.html +++ b/refstack/templates/layout.html @@ -5,19 +5,19 @@ - + - + {% block head_css %}{% endblock %} - +
-
+
  • Users
  • - +
  • @@ -96,4 +96,4 @@ {% block tail_js %}{% endblock %} - \ No newline at end of file + diff --git a/refstack/web.py b/refstack/web.py index 09dff0d5..ed0bb46e 100755 --- a/refstack/web.py +++ b/refstack/web.py @@ -29,12 +29,13 @@ from wtforms import Form, BooleanField, TextField, \ from flask_mail import Mail from refstack import app as base_app -from refstack import utils +from refstack.extensions import db from refstack.extensions import oid from refstack.models import Cloud from refstack.models import Test from refstack.models import TestResults from refstack.models import TestStatus +from refstack.models import User from refstack.models import Vendor @@ -114,8 +115,8 @@ def create_profile(): flash(u'Error: you have to enter a valid email address') else: flash(u'Profile successfully created') - db.add(User(name, email, session['openid'])) - db.commit() + db.session.add(User(name, email, session['openid'])) + db.session.commit() return redirect(oid.get_next_url()) return render_template( 'create_profile.html', next_url=oid.get_next_url()) @@ -131,8 +132,8 @@ def delete_cloud(cloud_id): elif not c.user_id == g.user.id: flash(u"This isn't your cloud!") else: - db.delete(c) - db.commit() + db.session.delete(c) + db.session.commit() return redirect('/') @@ -172,7 +173,7 @@ def edit_cloud(cloud_id): c.admin_user = request.form['admin_user'] c.admin_key = request.form['admin_key'] - db.commit() + db.session.commit() flash(u'Cloud Saved!') return redirect('/') @@ -222,8 +223,8 @@ def create_cloud(): c.admin_user = request.form['admin_user'] c.admin_key = request.form['admin_key'] - db.add(c) - db.commit() + db.session.add(c) + db.session.commit() return redirect('/') return render_template('create_cloud.html', next_url='/') @@ -237,8 +238,8 @@ def edit_profile(): form = dict(name=g.user.name, email=g.user.email) if request.method == 'POST': if 'delete' in request.form: - db.delete(g.user) - db.commit() + db.session.delete(g.user) + db.session.commit() session['openid'] = None flash(u'Profile deleted') return redirect(url_for('index')) @@ -252,7 +253,7 @@ def edit_profile(): flash(u'Profile successfully created') g.user.name = form['name'] g.user.email = form['email'] - db.commit() + db.session.commit() return redirect(url_for('edit_profile')) return render_template('edit_profile.html', form=form) From c8284b2bdc846d9769cdf6865ff40ca37ab9661e Mon Sep 17 00:00:00 2001 From: termie Date: Tue, 26 Nov 2013 16:54:44 -0800 Subject: [PATCH 6/8] add a basic api based on flask-restless not really a final solution, but it can probably get pretty far --- refstack/admin.py | 2 +- refstack/api.py | 68 ++++++++++++++++++++++++++++++++++++++++++ refstack/app.py | 7 ++++- refstack/extensions.py | 3 ++ refstack/models.py | 14 +++++++++ refstack/web.py | 6 ++-- requirements.txt | 1 + 7 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 refstack/api.py diff --git a/refstack/admin.py b/refstack/admin.py index 79955e89..d756a65e 100644 --- a/refstack/admin.py +++ b/refstack/admin.py @@ -21,7 +21,7 @@ def init_app(app): admin.init_app(app) -def configure_admin(): +def configure_admin(app): admin.add_view(SecureView(models.Cloud, db.session)) admin.add_view(SecureView(models.User, db.session)) admin.add_view(SecureView(models.Vendor, db.session)) diff --git a/refstack/api.py b/refstack/api.py new file mode 100644 index 00000000..14e675f4 --- /dev/null +++ b/refstack/api.py @@ -0,0 +1,68 @@ +"""Basic API code. + +This is using Flask-Restless at the moment because it is super simple, +but probably should be moved to a more versatile framework like +Flask-Restful later on. +""" + +import flask +from flask.ext import restless + +from refstack import models +from refstack.extensions import api + + +def init_app(app, *args, **kw): + api.init_app(app, *args, **kw) + + +def access_control(**kw): + if not flask.g.user: + raise _not_authorized() + + if not flask.g.user.su: + return _not_authorized() + + # That's it, we're defaulting to superuser only access + # until we flesh this out further + + +ALL_METHODS = {'GET_SINGLE': [access_control], + 'GET_MANY': [access_control], + 'PUT_SINGLE': [access_control], + 'PUT_MANY': [access_control], + 'POST': [access_control], + 'DELETE': [access_control]} + + +def configure_api(app): + cloud_api = api.create_api_blueprint(models.Cloud, + preprocessors=ALL_METHODS) + cloud_api.before_request(authenticate) + app.register_blueprint(cloud_api) + + +def _not_authorized(): + return restless.ProcessingException(message='Not Authorized', + status_code=401) + + + + +def authenticate(): + # If we're already authenticated, we can ignore this + if flask.g.user: + return + + # Otherwise check headers + openid = flask.request.headers.get('X-AUTH-OPENID') + if openid: + # In debug mode accept anything + if flask.current_app.debug and False: + flask.g.user = models.User.query.filter_by(openid=openid).first() + return + + apikey = flask.request.headers.get('X-AUTH-APIKEY') + apikey_ref = models.ApiKey.query.filter_by(key=apikey) + if apikey_ref['openid'] == openid: + flask.g.user = apikey_ref.user diff --git a/refstack/app.py b/refstack/app.py index f15a7615..b1570ae7 100644 --- a/refstack/app.py +++ b/refstack/app.py @@ -15,6 +15,7 @@ from .config import DefaultConfig #from .admin import admin #from .extensions import db, mail, cache, login_manager, oid from refstack import admin +from refstack import api from .extensions import db from .extensions import oid @@ -94,7 +95,11 @@ def configure_extensions(app): # flask-admin admin.init_app(app) - admin.configure_admin() + admin.configure_admin(app) + + # flask-restless + api.init_app(app, flask_sqlalchemy_db=db) + api.configure_api(app) ## flask-mail #mail.init_app(app) diff --git a/refstack/extensions.py b/refstack/extensions.py index 6b36240f..5d528c23 100644 --- a/refstack/extensions.py +++ b/refstack/extensions.py @@ -5,6 +5,9 @@ from flask.ext.admin import Admin admin = Admin() +from flask.ext.restless import APIManager +api = APIManager() + from flask.ext.sqlalchemy import SQLAlchemy db = SQLAlchemy() diff --git a/refstack/models.py b/refstack/models.py index 79bf662d..4a83fe66 100755 --- a/refstack/models.py +++ b/refstack/models.py @@ -41,6 +41,20 @@ class User(db.Model): def __str__(self): return self.name + +class ApiKey(db.Model): + __tablename__ = 'apikey' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(60)) + key = db.Column(db.String(200)) + openid = db.Column(db.String(200)) + timestamp = db.Column(db.DateTime, default=datetime.now) + + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + user = db.relationship('User', + backref=db.backref('apikeys', lazy='dynamic')) + + """ Note: The vendor list will be pre-populated from the sponsoring company database. TODO: better define the vendor object and its relationship with user diff --git a/refstack/web.py b/refstack/web.py index ed0bb46e..a7ae49c1 100755 --- a/refstack/web.py +++ b/refstack/web.py @@ -31,6 +31,8 @@ from flask_mail import Mail from refstack import app as base_app from refstack.extensions import db from refstack.extensions import oid +from refstack import api +from refstack.models import ApiKey from refstack.models import Cloud from refstack.models import Test from refstack.models import TestResults @@ -50,9 +52,9 @@ mail = Mail(app) @app.before_request def before_request(): """Runs before the request itself.""" - g.user = None + flask.g.user = None if 'openid' in session: - g.user = User.query.filter_by(openid=session['openid']).first() + flask.g.user = User.query.filter_by(openid=session['openid']).first() @app.route('/', methods=['POST', 'GET']) diff --git a/requirements.txt b/requirements.txt index e7a97700..cd32b05b 100755 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ Flask-Principal==0.3.5 Flask-SQLAlchemy==1.0 Flask-Security==1.6.3 Flask-WTF==0.8.3 +Flask-Restless=0.12.0 SQLAlchemy==0.8.3 WTForms==1.0.4 Werkzeug==0.8.3 From bd3585ffcd4bf6800b3b02060ee28f47341e8d12 Mon Sep 17 00:00:00 2001 From: termie Date: Tue, 26 Nov 2013 17:11:50 -0800 Subject: [PATCH 7/8] add admin for apikey and migration --- alembic/env.py | 4 +-- alembic/versions/449461dbc725_add_apikey.py | 31 +++++++++++++++++++++ refstack/admin.py | 1 + 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 alembic/versions/449461dbc725_add_apikey.py diff --git a/alembic/env.py b/alembic/env.py index 6449b1da..b28f97ae 100755 --- a/alembic/env.py +++ b/alembic/env.py @@ -38,8 +38,8 @@ fileConfig(config.config_file_name) # from myapp import mymodel # target_metadata = mymodel.Base.metadata # target_metadata = None -from refstack.models import * -target_metadata = Base.metadata +from refstack.models import db +target_metadata = db.metadata # other values from the config, defined by the needs of env.py, # can be acquired: diff --git a/alembic/versions/449461dbc725_add_apikey.py b/alembic/versions/449461dbc725_add_apikey.py new file mode 100644 index 00000000..9becd096 --- /dev/null +++ b/alembic/versions/449461dbc725_add_apikey.py @@ -0,0 +1,31 @@ +"""empty message + +Revision ID: 449461dbc725 +Revises: 59e15d864941 +Create Date: 2013-11-26 16:57:16.062788 + +""" + +# revision identifiers, used by Alembic. +revision = '449461dbc725' +down_revision = '59e15d864941' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table('apikey', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=60), nullable=True), + sa.Column('key', sa.String(length=200), nullable=True), + sa.Column('openid', sa.String(length=200), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('timestamp', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id'), + ) + + +def downgrade(): + op.drop_Table('apikey') diff --git a/refstack/admin.py b/refstack/admin.py index d756a65e..39074dbf 100644 --- a/refstack/admin.py +++ b/refstack/admin.py @@ -22,6 +22,7 @@ def init_app(app): def configure_admin(app): + admin.add_view(SecureView(models.ApiKey, db.session)) admin.add_view(SecureView(models.Cloud, db.session)) admin.add_view(SecureView(models.User, db.session)) admin.add_view(SecureView(models.Vendor, db.session)) From ea9473c427919d052ca99caa837b2a0357e891a9 Mon Sep 17 00:00:00 2001 From: termie Date: Tue, 26 Nov 2013 17:23:34 -0800 Subject: [PATCH 8/8] get rid of this useless fabfile --- fabfile.py | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100755 fabfile.py diff --git a/fabfile.py b/fabfile.py deleted file mode 100755 index 350b9bfb..00000000 --- a/fabfile.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (c) 2013 Piston Cloud Computing, Inc. -# All Rights Reserved. -# -# 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. -from fabric.api import env, roles, run, cd - -# Define sets of servers as roles -env.roledefs = { - 'web': ['refstack.org'], -} - -# Set the user to use for ssh -env.user = 'refstack' - -@roles('web') -def get_version(): - run('cat /etc/issue') - - -@roles('web') -def deploy(): - with cd('/var/www/refstack'): - run('git checkout master') - run('git pull') - run('sudo pip install -r requirements.txt') - run('alembic upgrade head') - run('sudo uwsgi --reload /tmp/project-master_refstack.pid') \ No newline at end of file