Merge branch 'master' of https://github.com/dlenwell/refstack
Conflicts: README.md
This commit is contained in:
commit
12c2639281
21
README.md
21
README.md
@ -1,21 +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
|
||||
|
||||
TODO:
|
||||
=====
|
||||
|
||||
Metadata gathering:
|
||||
- Number of nodes
|
||||
- vCPUs
|
||||
- Distro
|
||||
- Deployment approach
|
||||
- RAM
|
||||
|
||||
Plugins:
|
||||
- Cinder
|
||||
- Neutron
|
59
README.rst
Executable file
59
README.rst
Executable file
@ -0,0 +1,59 @@
|
||||
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
|
||||
|
||||
# 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!
|
||||
|
||||
TODO:
|
||||
=====
|
||||
|
||||
Metadata gathering:
|
||||
- Number of nodes
|
||||
- vCPUs
|
||||
- Distro
|
||||
- Deployment approach
|
||||
- RAM
|
||||
|
||||
Plugins:
|
||||
- Cinder
|
||||
- Neutron
|
@ -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:///db.sqlite
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
@ -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.
|
||||
@ -42,9 +38,13 @@ 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:
|
||||
# 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()
|
||||
|
||||
|
31
alembic/versions/449461dbc725_add_apikey.py
Normal file
31
alembic/versions/449461dbc725_add_apikey.py
Normal file
@ -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')
|
39
fabfile.py
vendored
39
fabfile.py
vendored
@ -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')
|
28
refstack/admin.py
Normal file
28
refstack/admin.py
Normal file
@ -0,0 +1,28 @@
|
||||
|
||||
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(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))
|
68
refstack/api.py
Normal file
68
refstack/api.py
Normal file
@ -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
|
239
refstack/app.py
Executable file → Normal file
239
refstack/app.py
Executable file → Normal file
@ -1,50 +1,209 @@
|
||||
#!/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 refstack import admin
|
||||
from refstack import api
|
||||
from .extensions import db
|
||||
from .extensions import oid
|
||||
|
||||
|
||||
#db_path = os.path.abspath(
|
||||
# os.path.join(os.path.basename(__file__), "../"))
|
||||
|
||||
db_path = 'tmp'
|
||||
from refstack import utils
|
||||
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
INSTANCE_FOLDER_PATH = utils.INSTANCE_FOLDER_PATH
|
||||
|
||||
|
||||
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'
|
||||
# For import *
|
||||
__all__ = ['create_app']
|
||||
|
||||
DEFAULT_BLUEPRINTS = tuple()
|
||||
# frontend,
|
||||
# user,
|
||||
# settings,
|
||||
# api,
|
||||
# admin,
|
||||
#)
|
||||
|
||||
|
||||
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
|
||||
|
||||
# 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)
|
||||
configure_extensions(app)
|
||||
configure_logging(app)
|
||||
configure_template_filters(app)
|
||||
configure_error_handlers(app)
|
||||
|
||||
if app.debug:
|
||||
print utils.dump_config(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def configure_app(app, config=None):
|
||||
"""Different ways of configurations."""
|
||||
|
||||
# http://flask.pocoo.org/docs/api/#configuration
|
||||
app.config.from_object(DefaultConfig)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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-admin
|
||||
admin.init_app(app)
|
||||
admin.configure_admin(app)
|
||||
|
||||
# flask-restless
|
||||
api.init_app(app, flask_sqlalchemy_db=db)
|
||||
api.configure_api(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 handlers.
|
||||
# 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
|
||||
|
77
refstack/config.py
Normal file
77
refstack/config.py
Normal file
@ -0,0 +1,77 @@
|
||||
# -*- 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, PROJECT_ROOT
|
||||
|
||||
|
||||
class BaseConfig(object):
|
||||
|
||||
PROJECT = "refstack"
|
||||
|
||||
# The app root path, also can use flask.root_path.
|
||||
PROJECT_ROOT = PROJECT_ROOT
|
||||
|
||||
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://'
|
17
refstack/decorators.py
Normal file
17
refstack/decorators.py
Normal file
@ -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
|
47
refstack/default_settings.py
Normal file
47
refstack/default_settings.py
Normal file
@ -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'
|
25
refstack/extensions.py
Normal file
25
refstack/extensions.py
Normal file
@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file based on MIT licensed code at: https://github.com/imwilsonxu/fbone
|
||||
|
||||
from flask.ext.admin import Admin
|
||||
admin = Admin()
|
||||
|
||||
from flask.ext.restless import APIManager
|
||||
api = APIManager()
|
||||
|
||||
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()
|
@ -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
|
||||
@ -61,63 +42,76 @@ class User(Base):
|
||||
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
|
||||
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 +120,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)
|
||||
|
||||
|
||||
|
@ -1,3 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS VENDORS
|
||||
(vendor_id integer primary key asc autoincrement,
|
||||
vendor_name TEXT NOT NULL);
|
116
refstack/utils.py
Normal file
116
refstack/utils.py
Normal file
@ -0,0 +1,116 @@
|
||||
# -*- 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.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import os
|
||||
import pprint
|
||||
import random
|
||||
import string
|
||||
|
||||
|
||||
# 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'])
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
### Begin Non-Fbone stuff
|
||||
|
||||
|
||||
def dump_config(app):
|
||||
"""Useful to dump app config for debug purposes."""
|
||||
return pprint.pformat(dict(app.config.iteritems()))
|
@ -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,32 +28,33 @@ from wtforms import Form, BooleanField, TextField, \
|
||||
PasswordField, validators
|
||||
from flask_mail import Mail
|
||||
|
||||
from refstack.app import app
|
||||
from refstack.models import *
|
||||
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
|
||||
from refstack.models import TestStatus
|
||||
from refstack.models import User
|
||||
from refstack.models import Vendor
|
||||
|
||||
|
||||
# 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')
|
||||
|
||||
|
||||
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
|
||||
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'])
|
||||
@ -112,8 +117,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())
|
||||
@ -129,8 +134,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('/')
|
||||
|
||||
@ -170,7 +175,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('/')
|
||||
@ -220,8 +225,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='/')
|
||||
@ -235,8 +240,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'))
|
||||
@ -250,7 +255,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)
|
||||
|
||||
@ -270,6 +275,3 @@ def logout():
|
||||
session.pop('openid', None)
|
||||
flash(u'You have been signed out')
|
||||
return redirect(oid.get_next_url())
|
||||
|
||||
|
||||
|
||||
|
@ -1,13 +1,14 @@
|
||||
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
|
||||
Flask-Restless=0.12.0
|
||||
SQLAlchemy==0.8.3
|
||||
WTForms==1.0.4
|
||||
Werkzeug==0.8.3
|
||||
alembic==0.5.0
|
||||
|
Loading…
Reference in New Issue
Block a user