Commit 3d6ea658 by Heechul Kim

porch initial release

parents
[loggers]
keys=root,porch
[handlers]
keys=console
[formatters]
keys=simple
[logger_root]
level=DEBUG
handlers=console
[logger_porch]
level=DEBUG
handlers=console
qualname=porch
propagate=0
[handler_console]
class=StreamHandler
level=DEBUG
formatter=simple
args=(sys.stdout,)
[formatter_simple]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=
File mode changed
File mode changed
import ast
import uuid
import etcd
import bcrypt
import logging
import logging.config
from datetime import datetime
from porch.database import etcdc
logging.config.fileConfig('logging.conf')
log = logging.getLogger('porch')
def get_account(name=None):
"""Get account list.
"""
l_account = list()
if name is None: # get all account.
s_rsc = '{}/account'.format(etcdc.prefix)
else:
s_rsc = '{}/account/{}'.format(etcdc.prefix, name)
try:
r = etcdc.read(s_rsc, recursive=True, sorted=True)
except etcd.EtcdKeyNotFound as e:
log.error(e)
else:
for child in r.children:
if child.value is not None:
# need to use ast to convert str to dict.
d = ast.literal_eval(child.value)
l_account.append(d)
finally:
return l_account
def create_account(data):
"""Create account.
etcd_key: <ETCD_PREFIX>/account/<name>
"""
t_ret = (False, '')
# Create uid(uuid) for account
s_uuid = str(uuid.uuid4())
# Get iso 8601 format datetime for created timestamp
s_created = datetime.utcnow().isoformat() + 'Z'
# Put s_created into data dict.
data['createdAt'] = s_created
data['uid'] = s_uuid
bytes_salt = bcrypt.gensalt()
data['pass'] = bcrypt.hashpw(str.encode(data['pass']),
bytes_salt).decode()
data['salt'] = bytes_salt.decode()
s_rsc = '{}/account/{}'.format(etcdc.prefix, data['name'])
try:
etcdc.write(s_rsc, data, prevExist=False)
except etcd.EtcdKeyAlreadyExist as e:
log.error(e)
t_ret = (False, e)
else:
t_ret = (True, 'user {} is created.'.format(data['name']))
finally:
return t_ret
def auth_account(data):
"""Auth account.
data = {'name':, 'pass'}
"""
s_rsc = '{}/account/{}'.format(etcdc.prefix, data['name'])
try:
r = etcdc.read(s_rsc)
except etcd.EtcdKeyNotFound as e:
log.error(e)
return (False, 'EtcdKeyNotFound')
d = ast.literal_eval(r.value)
# check state.
if d.get('state') and d.get('state') == 'Disabled':
return (False, 'Disabled')
elif d.get('state') and d.get('state') == 'Locked':
return (False, 'Locked')
elif 'state' not in d or d.get('state') != 'Enabled':
return (False, 'UnknownError')
# check expireAt
if datetime.utcnow().isoformat() > d.get('expireAt'):
return (False, 'Expired')
s_pass = bcrypt.hashpw(data['pass'].encode(),
d['salt'].encode()).decode()
if s_pass != d['pass']:
return (False, 'PasswordMismatch')
return (True, 'Authenticated')
def update_account(name, data):
"""Update account.
"""
data = dict((k, v) for k, v in data.items() if v)
t_ret = (False, '')
if not name: # <name> should be specified.
return t_ret
s_rsc = '{}/account/{}'.format(etcdc.prefix, name)
try:
r = etcdc.read(s_rsc)
except etcd.EtcdKeyNotFound as e:
log.error(e)
t_ret = (False, e)
return t_ret
d = ast.literal_eval(r.value)
# Get iso 8601 format datetime for modified timestamp
s_modified = datetime.utcnow().isoformat() + 'Z'
# Put s_modified into data dict.
data['modifiedAt'] = s_modified
d.update(data.items())
try:
etcdc.write(s_rsc, d, prevExist=True)
except etcd.EtcdKeyNotFound as e:
log.error(e)
t_ret = (False, e)
else:
t_ret = (True, 'user {} is updated.'.format(name))
finally:
return t_ret
def delete_account(name):
"""Delete account.
"""
t_ret = (False, '')
if not name: # <name> should be specified.
return t_ret
s_rsc = '{}/account/{}'.format(etcdc.prefix, name)
try:
r = etcdc.delete(s_rsc)
except etcd.EtcdKeyNotFound as e:
log.error(e)
t_ret = (False, e)
else:
t_ret = (True, 'user {} is deleted.'.format(name))
finally:
return t_ret
def password_account(data):
"""Modify account password.
etcd_key: <ETCD_PREFIX>/account/<name>
data: {'name': , 'pass': , 'pass2': }
"""
t_ret = (False, '')
s_rsc = '{}/account/{}'.format(etcdc.prefix, data['name'])
try:
r = etcdc.read(s_rsc)
except etcd.EtcdKeyNotFound as e:
log.error(e)
return (False, 'EtcdKeyNotFound')
d = ast.literal_eval(r.value)
# check data['pass'] is valid.
(b_ret, s_msg) = _pass_validate(data)
if not b_ret:
log.debug((b_ret, s_msg))
return (b_ret, s_msg)
# password is okay. go head.
new_data = dict()
s_modified = datetime.utcnow().isoformat() + 'Z'
data['modifiedAt'] = s_modified
# Put d['pass'] to oldpass entry.
if 'oldpass' in d:
new_data['oldpass'].append(d['pass'])
else:
new_data['oldpass'] = [d['pass']]
# Create new hashed password.
bytes_salt = bytes(d['salt'], 'utf-8')
new_data['pass'] = bcrypt.hashpw(str.encode(data['pass']),
bytes_salt).decode()
d.update(new_data.items())
s_rsc = '{}/account/{}'.format(etcdc.prefix, data['name'])
try:
etcdc.write(s_rsc, d, prevExist=True)
except etcd.EtcdKeyNotFound as e:
log.error(e)
t_ret = (False, e)
else:
t_ret = (True, 'user {} password is modified.'.format(data['name']))
finally:
return t_ret
def _pass_validate(data):
"""password validation
* check if pass is not in old pass entry.
"""
s_rsc = '{}/account/{}'.format(etcdc.prefix, data['name'])
try:
r = etcdc.read(s_rsc)
except etcd.EtcdKeyNotFound as e:
log.error(e)
return (False, 'EtcdKeyNotFound')
d = ast.literal_eval(r.value)
s_pass = bcrypt.hashpw(data['pass'].encode(),
d['salt'].encode()).decode()
if s_pass == d['pass']:
return (False, 'NewPasswordSameAsCurrentPassword')
if 'oldpass' in d:
for s_oldpass in d['oldpass']:
if s_oldpass == s_pass:
return (False, 'PasswordPreviouslyUsed')
return (True, 'PasswordMatched')
import datetime
import logging
import logging.config
from flask import request
from flask import abort
from flask import jsonify
from flask import make_response
from flask_restplus import Resource
from flask_jwt_extended import JWTManager
from flask_jwt_extended import jwt_required
from flask_jwt_extended import jwt_refresh_token_required
from flask_jwt_extended import create_access_token
from flask_jwt_extended import create_refresh_token
from flask_jwt_extended import get_jwt_identity
from flask_jwt_extended import get_jwt_claims
from porch.api.restplus import api
from porch.decorators import authz_required
from porch.api.account.serializers import accountSerializer
from porch.api.account.serializers import accountPostSerializer
from porch.api.account.serializers import accountAuthSerializer
from porch.api.account.serializers import accountPatchSerializer
from porch.api.account.serializers import accountPasswordSerializer
from porch.api.account.bizlogic import get_account
from porch.api.account.bizlogic import create_account
from porch.api.account.bizlogic import auth_account
from porch.api.account.bizlogic import update_account
from porch.api.account.bizlogic import delete_account
from porch.api.account.bizlogic import password_account
log = logging.getLogger('porch')
ns = api.namespace('account', description="Operations for account")
@ns.route('/')
class AccountCollection(Resource):
@ns.marshal_list_with(accountSerializer)
@authz_required('admin')
def get(self):
"""Returns list of account."""
claims = get_jwt_claims()
#log.debug(claims)
l_account = get_account()
return l_account
@ns.response(201, 'Account successfully created.')
@ns.expect(accountPostSerializer)
@authz_required('admin')
def post(self):
"""Creates a new account.
"""
data = request.json
(b_ret, s_msg) = create_account(data)
if not b_ret:
d_msg = {'Error': 'Account creation is failed.'}
return d_msg, 409
# get a new account.
l_account = get_account(data['name'])
return l_account[0], 201
@ns.route('/login')
class AccountAuth(Resource):
@ns.response(200, 'Account successfully Authenticated.')
@ns.expect(accountAuthSerializer)
def post(self):
"""Authenticate an account.
"""
data = request.json
(b_ret, s_ret) = auth_account(data)
if not b_ret:
s_msg = 'Authentication is failed. '
if s_ret == 'Disabled':
d_msg = { 'error': s_msg + 'Account is disabled.' }
elif s_ret == 'Locked':
d_msg = { 'error': s_msg + 'Account is locked.' }
elif s_ret == 'Expired':
d_msg = { 'error': s_msg + 'Account is expired.' }
else:
d_msg = { 'error': s_msg }
return d_msg, 401
# create token
s_role = 'user' if data['name'] != 'admin' else 'admin'
d_resp = {
'token': create_access_token(identity=data['name']),
'refresh_token': create_refresh_token(identity=data['name']),
'role': s_role
}
# Set cookie
#resp = make_response(jsonify(d_resp))
#resp.set_cookie('token', d_resp['token'], httponly=True)
#resp.set_cookie('role', s_role, httponly=True)
#return resp
return d_resp, 200
@ns.route('/refresh')
class AccountRefresh(Resource):
@jwt_refresh_token_required
def post(self):
"""Refresh an account token.
"""
uid = get_jwt_identity()
d_resp = {
'token': create_access_token(identity=uid)
}
return d_resp, 200
@ns.route('/<string:name>')
@api.response(404, 'Account not found.')
class AccountItem(Resource):
@ns.marshal_with(accountSerializer)
@ns.doc('get_something')
@jwt_required
def get(self, name):
"""Returns the account information."""
d_claim = get_jwt_claims()
# check name == d_claim['id']
if d_claim['id'] != 'admin' and name != d_claim['id']:
abort(401, 'Not authorized.')
l_account = get_account(name)
if not len(l_account):
return {'Error': 'name {} not found.'.format(name)}, 404
return l_account[0]
@ns.expect(accountPatchSerializer)
@ns.response(204, "The account is successfully updated.")
@jwt_required
def patch(self, name):
"""Updates the account information."""
d_claim = get_jwt_claims()
# check authz
if d_claim['id'] != 'admin' and name != d_claim['id']:
abort(401, 'Not authorized.')
data = request.json
(b_ret, s_msg) = update_account(name, data)
if not b_ret:
abort(404)
l_account = get_account(name)
return l_account[0], 200
@ns.response(204, "The account is successfully deleted.")
@authz_required('admin')
def delete(self, name):
"""Deletes the account."""
(b_ret, s_msg) = delete_account(name)
if not b_ret:
abort(404)
return None, 204
@ns.route('/<string:name>/pass')
@api.response(404, 'Account not found.')
class AccountPassword(Resource):
#@ns.expect(accountPasswordSerializer)
@ns.response(204, "The account is successfully updated.")
@jwt_required
def patch(self, name):
"""Updates the account password."""
d_claim = get_jwt_claims()
# check authz
if d_claim['id'] != 'admin' and name != d_claim['id']:
abort(401, 'Not authorized.')
data = request.json
(b_ret, s_msg) = password_account(data)
if not b_ret:
d_msg = {'error': s_msg}
return d_msg, 400
l_account = get_account(name)
return l_account[0], 200
import datetime
from flask_restplus import fields
from porch.api.restplus import api
accountSerializer = api.model('ListAccount', {
'name': fields.String(required=True, description='account name'),
'uid': fields.String(required=True, description='account uuid'),
'cn': fields.String(required=True, description='account common name'),
'desc': fields.String(required=False, description='account description'),
#'salt': fields.String(required=True, description='bcrypt salt'),
#'pass': fields.String(required=True, description='bcrypt hash'),
'state': fields.String(required=False, description='user state'),
'expireAt': fields.DateTime(required=False, description='pass expire date'),
'createdAt': fields.DateTime(required=True, description='account created'),
'modifiedAt': fields.DateTime(required=False, description='mod time'),
})
accountPostSerializer = api.model('RegisterAccount', {
'name': fields.String(required=True, description='account name'),
'pass': fields.String(required=True, description='plaintext password'),
'cn': fields.String(required=True, description='account common name'),
'state': fields.String(
required=True,
default="Enabled", # Enabled, Disabled, Locked
),
'desc': fields.String(required=False, description='account description'),
'expireAt': fields.DateTime(
required=True,
default=datetime.datetime.now() +
datetime.timedelta(days=90),
description='pass expire date'
),
}),
accountAuthSerializer = api.model('AuthAccount', {
'name': fields.String(required=True, description='account name'),
'pass': fields.String(required=True, description='plaintext password'),
})
accountPatchSerializer = api.model('ModifyAccount', {
'cn': fields.String(required=False, description='account common name'),
'state': fields.String(
required=False,
default="Enabled", # Enabled, Disabled, Locked
),
'desc': fields.String(required=False, description='account description'),
'expireAt': fields.DateTime(
required=True,
default=datetime.datetime.now() +
datetime.timedelta(days=90),
description='pass expire date'
),
})
accountPasswordSerializer = api.model('PasswordAccount', {
'name': fields.String(required=True, description='account name'),
'pass': fields.String(required=True, description='plaintext password'),
'pass2': fields.String(required=True, description='plaintext password'),
})
import ast
import time
import etcd
import uuid
import shutil
import tempfile
import logging
import logging.config
from datetime import datetime
from git import Repo
from flask import render_template
from porch.utils import cmd
from porch.utils import run_command
from porch.database import etcdc
from porch.config import GIT_DIR
from porch.config import PLAY_DIR
from porch.config import PRIKEY_DIR
logging.config.fileConfig('logging.conf')
log = logging.getLogger('porch')
def get_app(name=None):
"""Get app list.
"""
l_app = list()
s_rsc = ''
if name is None: # get all app.
s_rsc = '{}/app'.format(etcdc.prefix)
else:
s_rsc = '{}/app/{}'.format(etcdc.prefix, name)
try:
r = etcdc.read(s_rsc, recursive=True, sorted=True)
except etcd.EtcdKeyNotFound as e:
log.error(e)
else:
for child in r.children:
if child.value is not None:
d = ast.literal_eval(child.value)
# get tags for each app.
l_tags = list()
try:
o_repo = Repo(GIT_DIR + '/' + d['repo'])
for o_tag in o_repo.tags:
l_tags.append(o_tag.name)
d['tag'] = l_tags
except:
log.error('Cannot get git repo.')
l_app.append(d)
finally:
return l_app
def create_app(data):
"""Create app.
etcd_key: <ETCD_PREFIX>/app/<name>
"""
t_ret = (False, '')
s_rsc = ''
# Create uid(uuid) for app
s_uuid = str(uuid.uuid4())
# Get iso 8601 format datetime for created timestamp
s_created = datetime.utcnow().isoformat() + 'Z'
# Put s_created into data dict.
data['createdAt'] = s_created
data['uid'] = s_uuid
s_rsc = '{}/app/{}'.format(etcdc.prefix, data['name'])
try:
etcdc.write(s_rsc, data, prevExist=False)
except etcd.EtcdKeyAlreadyExist as e:
log.error(e)
t_ret = (False, e)
else:
t_ret = (True, 'app {} is created.'.format(data['name']))
# Create git repo for the app.
o_repo = Repo.init(GIT_DIR + '/' + data['repo'], mkdir=True, bare=True)
return t_ret
def update_app(name, data):
"""Update app.
"""
if not name: # <name> should be specified.
s_msg = 'name should be specified.'
return (False, s_msg)
# Delete the keys whose values are empty.
data = dict((k, v) for k, v in data.items() if v != '')
s_rsc = '{}/app/{}'.format(etcdc.prefix, name)
try:
r = etcdc.read(s_rsc)
except etcd.EtcdKeyNotFound as e:
log.error(e)
return (False, e)
d = ast.literal_eval(r.value)
# Get iso 8601 format datetime for modified timestamp
s_modified = datetime.utcnow().isoformat() + 'Z'
# Put s_modified into data dict.
data['modifiedAt'] = s_modified
d.update(data.items())
try:
etcdc.write(s_rsc, d, prevExist=True)
except:
s_msg = 'app {} update is failed.'.format(name)
log.error(s_msg)
return (False, s_msg)
else:
s_msg = 'app {} is updated.'.format(name)
return (True, s_msg)
def delete_app(name):
"""Delete app.
"""
if not name: # <name> should be specified.
return (False, 'name should be specified.')
# Get app info.
s_rsc = '{}/app/{}'.format(etcdc.prefix, name)
try:
r = etcdc.read(s_rsc)
except etcd.EtcdKeyNotFound as e:
log.error(e)
return (False, e)
d = ast.literal_eval(r.value)
# Delete app repo.
try:
shutil.rmtree(GIT_DIR + '/' + d['repo'])
except FileNotFoundError as e:
log.error(e)
# Delete etcd key.
s_rsc = '{}/app/{}'.format(etcdc.prefix, name)
r = etcdc.delete(s_rsc)
return (True, 'machine {} is deleted.'.format(name))
def play_app(name, data):
"""Play app.
data['name'],
data['tag']
data['machines']
"""
if not name: # <name> should be specified.
return (False, 'name should be specified.')
# Get app info.
s_rsc = '{}/app/{}'.format(etcdc.prefix, name)
try:
r = etcdc.read(s_rsc)
except etcd.EtcdKeyNotFound as e:
log.error(e)
return (False, e)
d = ast.literal_eval(r.value)
# get repo and tag.
s_play_dir = PLAY_DIR + '/' + name + str(time.time())
o_repo = Repo.clone_from(
'file://' + GIT_DIR + '/' + d['repo'],
s_play_dir,
branch=data['tag'],
depth=1
)
# Add hosts file in o_tmpdir.name
s_out = render_template('ansible_hosts.j2', data=data['machines'])
try:
with open(s_play_dir + '/hosts', 'w') as f:
f.write(s_out)
except:
s_msg = 'Fail to create ansible hosts file.'
return (False, s_msg)
# Let's play.
s_prikey = PRIKEY_DIR + '/prikey'
s_cmd= "ansible-playbook -i {0}/hosts --private-key={1} {0}/site.yml"\
.format(s_play_dir, s_prikey)
log.debug(s_cmd)
# set log file.
s_logfile = s_play_dir + '/' + name + '.log'
b_ret = run_command(s_cmd)
if not b_ret:
s_msg = 'Play is failed.'
return (False, s_msg)
# Delete cloned app.
try:
shutil.rmtree(s_play_dir)
except FileNotFoundError as e:
log.error(e)
return (True, 'Play is succeeded.')
def get_app_log(name):
"""Get app log.
"""
l_log = []
s_play_dir = PLAY_DIR + '/' + name
s_logfile = s_play_dir + '/' + name + '.log'
log.debug(s_logfile)
try:
with open(s_logfile, 'r') as f:
s_log = f.read()
except:
return l_log
else:
l_log.append(s_log)
log.debug(l_log)
return l_log
import logging
import logging.config
from flask import request
from flask import abort
from flask_restplus import Resource
from flask_jwt_extended import jwt_required
from flask_jwt_extended import get_jwt_claims
from porch.api.restplus import api
from porch.decorators import authz_required
from porch.api.app.serializers import appSerializer
from porch.api.app.serializers import appPostSerializer
from porch.api.app.serializers import appPatchSerializer
from porch.api.app.serializers import appPlaySerializer
from porch.api.app.bizlogic import get_app
from porch.api.app.bizlogic import create_app
from porch.api.app.bizlogic import update_app
from porch.api.app.bizlogic import delete_app
from porch.api.app.bizlogic import play_app
log = logging.getLogger('porch')
ns = api.namespace('app', description="Operations for app")
@ns.route('/')
class AppCollection(Resource):
@ns.marshal_list_with(appSerializer)
@jwt_required
def get(self):
"""Returns list of app."""
l_app = get_app()
return l_app
@ns.response(201, 'App successfully created.')
@ns.expect(appPostSerializer)
@jwt_required
def post(self):
"""Creates a new app.
"""
i_ret_code = 201
data = request.json
(b_ret, s_msg) = create_app(data)
if not b_ret:
d_msg = {'error', s_msg}
i_ret_code = 400
else:
d_msg = {'msg': s_msg}
return d_msg, i_ret_code
@ns.route('/<string:name>')
@ns.response(404, 'App not found.')
class AppItem(Resource):
@ns.marshal_with(appSerializer)
@ns.doc('get_something')
@jwt_required
def get(self, name):
"""Returns the app information."""
i_ret_code = 200
l_app = get_app(name)
if not len(l_app):
d_msg = {'error': 'name {} not found.'.format(name)}
i_ret_code = 404
return d_msg, i_ret_code
else:
return l_app[0], i_ret_code
@ns.expect(appPatchSerializer)
@ns.response(204, "The app is successfully updated.")
@jwt_required
def patch(self, name):
"""Updates the app information."""
data = request.json
log.debug(data)
(b_ret, s_msg) = update_app(name, data)
log.debug(b_ret)
if not b_ret:
d_msg = {'error': s_msg}
return d_msg, 404
else:
l_app = get_app(name)
return l_app[0], 200
@ns.response(204, "The app is successfully deleted.")
@authz_required('admin')
def delete(self, name):
"""Deletes the app."""
(b_ret, s_msg) = delete_app(name)
if not b_ret:
d_msg = {'error': s_msg}
return d_msg, 404
return None, 204
@ns.route('/<string:name>/play')
@ns.response(404, 'App not found.')
class AppPlay(Resource):
@ns.response(200, 'App is successfully played.')
@ns.expect(appPlaySerializer)
def post(self, name):
"""Play app.
"""
i_ret_code = 200
data = request.json
(b_ret, s_msg) = play_app(name, data)
if not b_ret:
d_msg = {'error', s_msg}
i_ret_code = 400
else:
d_msg = {'msg': s_msg}
return d_msg, i_ret_code
def get(self, name):
"""Returns the app play log."""
i_ret_code = 200
l_app = get_app_log(name)
if not len(l_app):
d_msg = {'error': 'name {} not found.'.format(name)}
i_ret_code = 404
return d_msg, i_ret_code
else:
return l_app, i_ret_code
from flask_restplus import fields
from porch.api.restplus import api
appSerializer = api.model('ListApp', {
'name': fields.String(required=True, description='app name'),
'repo': fields.String(required=True, description='app git repo'),
'tag': fields.List(fields.String),
'uid': fields.String(required=True, description='app uuid'),
'desc': fields.String(required=False, description='app description'),
'createdAt': fields.DateTime(required=True, description='app created'),
'modifiedAt': fields.DateTime(required=False, description='app mod time'),
})
appPostSerializer = api.model('RegisterApp', {
'name': fields.String(required=True, description='app name'),
'repo': fields.String(required=True, description='app git repo'),
'desc': fields.String(required=False, description='app description'),
})
appPatchSerializer = api.model('ModifyApp', {
'repo': fields.String(required=False, description='app git repo'),
'desc': fields.String(required=False, description='app description'),
})
appPlaySerializer = api.model('PlayApp', {
'name': fields.String(required=True, description='app name'),
'tag': fields.String(required=True, description='app name'),
'machines': fields.List(fields.String),
})
import uuid
import ast
import logging
import logging.config
from datetime import datetime
from porch.database import etcdc
logging.config.fileConfig('logging.conf')
log = logging.getLogger('porch')
def get_arrangement(name=None):
"""Get arrangement list.
etcd_key: <ETCD_PREFIX>/arrangement
"""
l_arrangement = list()
s_rsc = ''
if name is None: # get all arrangement.
s_rsc = '{}/arrangement'.format(etcdc.prefix)
else:
s_rsc = '{}/arrangement/{}'.format(etcdc.prefix, name)
r = etcdc.read(s_rsc, recursive=True, sorted=True)
for child in r.children:
if child.value is not None:
# need to use ast to convert str to dict.
l_arrangement.append(ast.literal_eval(child.value))
return l_arrangement
def create_arrangement(data):
"""Create arrangement.
etcd_key: <ETCD_PREFIX>/arrangement/<name>
"""
s_rsc = ''
# Create uid(uuid) for arrangement
s_uuid = str(uuid.uuid4())
# Get iso 8601 format datetime for created timestamp
s_created = datetime.utcnow().isoformat() + 'Z'
# Put s_created into data dict.
data['createdAt'] = s_created
data['uid'] = s_uuid
s_rsc = '{}/arrangement/{}'.format(etcdc.prefix, data['name'])
etcdc.write(s_rsc, data, prevExist=False)
def update_arrangement(name, data):
"""Update arrangement.
"""
b_ret = False
if not name: # <name> should be specified.
return b_ret
s_rsc = '{}/arrangement/{}'.format(etcdc.prefix, name)
r = etcdc.read(s_rsc)
d = ast.literal_eval(r.value)
# Get iso 8601 format datetime for modified timestamp
s_modified = datetime.utcnow().isoformat() + 'Z'
# Put s_modified into data dict.
data['modifiedAt'] = s_modified
d.update(data.items())
etcdc.write(s_rsc, d, prevExist=True)
return True
def delete_arrangement(name):
"""Delete arrangement.
"""
b_ret = False
if not name: # <name> should be specified.
return b_ret
s_rsc = '{}/arrangement/{}'.format(etcdc.prefix, name)
r = etcdc.delete(s_rsc)
return True
import logging
import logging.config
from flask import request
from flask import abort
from flask_restplus import Resource
from porch.api.restplus import api
from porch.api.arrangement.serializers import arrangementSerializer
from porch.api.arrangement.serializers import arrangementPostSerializer
from porch.api.arrangement.serializers import arrangementPatchSerializer
from porch.api.arrangement.bizlogic import get_arrangement
from porch.api.arrangement.bizlogic import create_arrangement
from porch.api.arrangement.bizlogic import update_arrangement
from porch.api.arrangement.bizlogic import delete_arrangement
log = logging.getLogger('porch')
ns = api.namespace('arrangement', description="Operations for arrangement")
@ns.route('/')
class ArrangementCollection(Resource):
@api.marshal_list_with(arrangementSerializer)
def get(self):
"""Returns list of arrangement."""
l_arrangement = get_arrangement()
return l_arrangement
@api.response(201, 'Arrangement successfully created.')
@api.expect(arrangementPostSerializer)
def post(self, arrangement=None):
"""Creates a new arrangement.
"""
data = request.json
create_arrangement(data)
return None, 201
@ns.route('/<string:name>')
@api.response(404, 'Arrangement not found.')
class ArrangementItem(Resource):
@api.marshal_with(arrangementSerializer)
@api.doc('get_something')
def get(self, name):
"""Returns the arrangement information."""
l_arrangement = get_arrangement(name)
return l_arrangement[0]
@api.expect(arrangementPatchSerializer)
@api.response(204, "The arrangement is successfully updated.")
def patch(self, name):
"""Updates the arrangement information."""
data = request.json
b_ret = update_arrangement(name, data)
if not b_ret:
abort(404)
return None, 204
@api.response(204, "The arrangement is successfully deleted.")
def delete(self, name):
"""Deletes the arrangement."""
b_ret = delete_arrangement(name)
if not b_ret:
abort(404)
return None, 204
from flask_restplus import fields
from porch.api.restplus import api
arrangementSerializer = api.model('ListArrangement', {
'name': fields.String(required=True, description='arrangement name'),
'uid': fields.String(required=True, description='arrangement uuid'),
'os': fields.String(required=True),
'preseed': fields.String(required=True),
'desc': fields.String(required=False),
'createdAt': fields.DateTime(required=True),
'modifiedAt': fields.DateTime(required=False),
})
arrangementPostSerializer = api.model('RegisterArrangement', {
'name': fields.String(required=True, description='arrangement name'),
'os': fields.String(required=True),
'preseed': fields.String(required=True),
'desc': fields.String(required=False),
})
arrangementPatchSerializer = api.model('ModifyArrangement', {
'os': fields.String(required=True),
'preseed': fields.String(required=True),
'desc': fields.String(required=False),
})
import uuid
import ast
import logging
import logging.config
from datetime import datetime
from porch.database import etcdc
logging.config.fileConfig('logging.conf')
log = logging.getLogger('porch')
def get_machine_info(mac=None, name=None):
"""Get machine information
```
:param: mac: string, mac address
```
Return: dict, {'os':, 'is_installed'}
"""
d_machine = dict()
# check to see if one of mac or name exists.
if mac is None and name is None:
return d_machine
if mac is not None: # get machine info from mac.
print(mac)
s_rsc = '{}/macaddr/{}'.format(etcdc.prefix, mac)
r = etcdc.read(s_rsc)
d = ast.literal_eval(r.value)
name = d['name']
print(name)
# Get is_installed from machine key
s_rsc = '{}/machine/{}'.format(etcdc.prefix, name)
r = etcdc.read(s_rsc)
d_machine = ast.literal_eval(r.value)
return d_machine
def set_is_installed(name):
"""Set machine's is_installed flag to True
"""
s_rsc = '{}/machine/{}'.format(etcdc.prefix, name)
r = etcdc.read(s_rsc)
d_machine = ast.literal_eval(r.value)
d_machine['is_installed'] = True
etcdc.write(s_rsc, d_machine, prevExist=True)
import re
import logging
import logging.config
from flask import request, Response, send_from_directory
from flask import abort
from flask_restplus import Resource
from porch.api.restplus import api
from porch.config import BOOT, WINBOOT, LINBOOT, TFTP_FAI_DIR
from porch.api.fai.bizlogic import get_machine_info
from porch.api.fai.bizlogic import set_is_installed
log = logging.getLogger('porch')
ns = api.namespace('fai', description="Operations for fai")
@ns.route('/boot/')
class Boot(Resource):
def get(self):
"""Returns initial boot ipxe script."""
with open(BOOT, 'r') as o_file:
s_content = o_file.read()
return Response(s_content, mimetype='text/plain')
@ns.route('/pxe/<string:mac>')
class Pxe(Resource):
def get(self, mac):
"""Returns pxe binary(linux)or ipxe script(windows)."""
if not mac:
abort(404)
# get machine info from macaddr key
d_machine = get_machine_info(mac) # {'os':, 'is_installed':, 'name':}
if not d_machine: # check if d_machine is empty.
abort(404)
# get pxe binary(linux) or ipxe script(windows)
if re.match("^WIN", d_machine['os']): # windows os
s_src = WINBOOT
s_type = 'text/plain'
else: # linux os
s_src = LINBOOT
s_type = 'application/octet-stream'
s_content = ''
# check if it is installed.
if not d_machine['is_installed']:
set_is_installed(d_machine['name'])
with open(s_src, 'rb') as o_src:
s_content = o_src.read()
return Response(s_content, mimetype=s_type)
@ns.route('/class/<string:hostname>')
class FaiClass(Resource):
def get(self, hostname):
"""Returns hostname's os."""
if not hostname:
abort(404)
# get os info from hostname
d_machine = get_machine_info(mac=None, name=hostname)
print(d_machine)
if not d_machine:
abort(404)
if 'os' not in d_machine:
abort(404)
return Response(d_machine['os'], mimetype='text/plain')
@ns.route('/boot/<path:path>')
class SendFiles(Resource):
def get(self, path):
return send_from_directory(TFTP_FAI_DIR, path)
import os
import ast
import uuid
import etcd
import stat
import shutil
import tarfile
import logging
import logging.config
from datetime import datetime
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
from flask import render_template
from porch.database import etcdc
from porch.config import TFTP_FAI_DIR
from porch.config import PXELINUXCFG
from porch.config import PRIKEY_DIR
from porch.config import PUBKEY_DIR
from porch.config import PORCH_URL
from porch.config import FAI_KERNEL_URL
from porch.config import FAI_INITRD_URL
from porch.config import FAI_SQUASH_URL
from porch.config import DHCPEDIT
from porch.utils import cmd
logging.config.fileConfig('logging.conf')
log = logging.getLogger('porch')
def get_machine(name=None):
"""Get machine list.
"""
l_machine = list()
s_rsc = ''
if name is None: # get all machine.
s_rsc = '{}/machine'.format(etcdc.prefix)
else: # name is given
s_rsc = '{}/machine/{}'.format(etcdc.prefix, name)
try:
r = etcdc.read(s_rsc, recursive=True, sorted=True)
except etcd.EtcdKeyNotFound as e:
log.error(e)
else:
for child in r.children:
if child.value is not None:
# need to use ast to convert str to dict.
l_machine.append(ast.literal_eval(child.value))
finally:
return l_machine
def create_machine(data):
"""Create machine.
etcd_key: /<ETCD_PREFIX>/machine/<name>
etcd_key: /<ETCD_PREFIX>/macaddr/<mac>
"""
t_ret = (False, '')
s_rsc = ''
# Create uid(uuid) for machine
s_uuid = str(uuid.uuid4())
# Get iso 8601 format datetime for created timestamp
s_created = datetime.utcnow().isoformat() + 'Z'
# Put s_created into data dict.
data['createdAt'] = s_created
data['uid'] = s_uuid
if 'section' not in data:
data['section'] = 'default'
data['state'] = 'Created'
data['is_installed'] = False
# create machine key:value
s_rsc = '{}/machine/{}'.format(etcdc.prefix, data['name'])
try:
etcdc.write(s_rsc, data, prevExist=False)
except etcd.EtcdKeyAlreadyExist as e:
log.error(e)
t_ret = (False, e)
else:
# Succeed to create a machine.
# Now create macaddr key:value
# if key is already there, just update the new values.
mac_data = {'name': data['name'], 'os': data['os']}
s_rsc = '{}/macaddr/{}'.format(etcdc.prefix, data['mac'])
etcdc.write(s_rsc, mac_data)
t_ret = (True, 'machine {} is created.'.format(data['name']))
return t_ret
def provision_machine(name):
"""Provision a machine."""
if not name:
s_msg = 'name should be specified.'
return (False, s_msg)
s_rsc = '{}/machine/{}'.format(etcdc.prefix, name)
try:
r = etcdc.read(s_rsc)
except etcd.EtcdKeyNotFound as e:
log.error(e)
return (False, e)
d = ast.literal_eval(r.value)
if 'mac' not in d:
s_msg = 'mac should exist.'
return (False, s_msg)
if 'ip' not in d:
s_msg = 'ip should exist.'
return (False, s_msg)
# Add dhcp entry of host.
s_cmd = "{} {} {} {}".format(DHCPEDIT, name, d['mac'], d['ip'])
log.debug(s_cmd)
cmd(s_cmd, b_sudo=True)
# Create pxelinuxcfg file.
s_mac = '01-' + d['mac'].replace(':', '-')
data = dict()
data['fai_kernel_url'] = FAI_KERNEL_URL
data['fai_initrd_url'] = FAI_INITRD_URL
data['fai_squash_url'] = FAI_SQUASH_URL
data['fai_config_url'] = PORCH_URL + '/boot/{}.tar'.format(d['name'])
s_out = render_template('pxelinuxcfg.j2', data=data)
s_pxelinuxcfg = "{}/{}".format(PXELINUXCFG, s_mac)
try:
with open(s_pxelinuxcfg, 'w') as f:
f.write(s_out)
except:
s_msg = 'Fail to create pxelinuxcfg file: {}.'.format(s_pxelinuxcfg)
return (False, s_msg)
# Create ssh private/public keys
key = rsa.generate_private_key(backend=default_backend(), \
public_exponent=65537, key_size=2048)
pubkey = key.public_key().public_bytes(serialization.Encoding.OpenSSH, \
serialization.PublicFormat.OpenSSH)
# private key pem format
pem = key.private_bytes(encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption())
# decode to printable strings
s_prikey = pem.decode('utf-8')
s_pubkey = pubkey.decode('utf-8')
# last, write files.
s_prikey_file = PRIKEY_DIR + '/' + d['ip']
s_pubkey_file = PUBKEY_DIR + '/' + d['ip']
try:
with open(s_prikey_file, 'w') as f:
os.chmod(s_prikey_file, stat.S_IRUSR|stat.S_IWUSR)
f.write(s_prikey)
except:
s_msg = 'Fail to create ssh private key.'
return (False, s_msg)
try:
with open(s_pubkey_file, 'w') as f:
os.chmod(s_pubkey_file, stat.S_IRUSR|stat.S_IWUSR)
f.write(s_pubkey)
except:
s_msg = 'Fail to create ssh public key.'
return (False, s_msg)
# Create fai config tar file on TFTP_FAI_DIR
# copy base tar file to d['name'].tar
s_src = TFTP_FAI_DIR + '/fai-{}-config.tar'.format(d['os'])
s_dst = TFTP_FAI_DIR + '/{}.tar'.format(d['name'])
try:
shutil.copy(s_src, s_dst)
except:
s_msg = 'Fail to create fai config tar file.'
return (False, s_msg)
# add pubkeys/<ip> into tarfile.
with tarfile.open(s_dst, 'a') as o_tar:
o_tar.add(s_pubkey_file, arcname='pubkeys/{}'.format(d['ip']))
# finally update machine state = provisioned
update_machine(name, {'state': 'Provisioned'})
return (True, 'machine {} provisioning is done.'.format(name))
def update_machine(name, data):
"""Update machine.
"""
if not name:
s_msg = 'name should be specified.'
return (False, s_msg)
# Delete the keys whose values are empty.
data = dict((k, v) for k, v in data.items() if v != '')
s_rsc = '{}/machine/{}'.format(etcdc.prefix, name)
try:
r = etcdc.read(s_rsc)
except etcd.EtcdKeyNotFound as e:
log.error(e)
return (False, e)
d = ast.literal_eval(r.value)
# Get mac
s_old_mac = d['mac']
# Get iso 8601 format datetime for modified timestamp
s_modified = datetime.utcnow().isoformat() + 'Z'
data['modifiedAt'] = s_modified
if not data.get('state'):
data['state'] = 'Updated'
d.update(data.items())
log.debug(d)
s_new_mac = d['mac']
# update machine key's value
try:
etcdc.write(s_rsc, d, prevExist=True)
except:
s_msg = 'machine {} update is failed.'.format(name)
log.error(s_msg)
return (False, s_msg)
# update macaddr key's value
b_mac_exist = True if s_old_mac == s_new_mac else False
s_rsc = '{}/macaddr/{}'.format(etcdc.prefix, s_new_mac)
mac_value = {'name': name, 'os': d['os']}
etcdc.write(s_rsc, mac_value)
if not b_mac_exist:
# delete the old macaddr key
s_rsc = '{}/macaddr/{}'.format(etcdc.prefix, s_old_mac)
etcdc.delete(s_rsc)
s_msg = 'machine {} is updated.'.format(name)
return (True, s_msg)
def delete_machine(name):
"""Delete machine.
"""
if not name:
return (False, 'name should be specified.')
# get machine's macaddr
s_rsc = '{}/machine/{}'.format(etcdc.prefix, name)
try:
r = etcdc.read(s_rsc)
except etcd.EtcdKeyNotFound as e:
log.error(e)
return (False, e)
d = ast.literal_eval(r.value)
s_mac = d['mac']
# delete machine key
s_rsc = '{}/machine/{}'.format(etcdc.prefix, name)
etcdc.delete(s_rsc)
# delete macaddr key
s_rsc = '{}/macaddr/{}'.format(etcdc.prefix, s_mac)
etcdc.delete(s_rsc)
return (True, 'machine {} is deleted.'.format(name))
import logging
import logging.config
from flask import request
from flask import abort
from flask_restplus import Resource
from flask_jwt_extended import JWTManager
from flask_jwt_extended import jwt_required
from flask_jwt_extended import create_access_token
from flask_jwt_extended import get_jwt_identity
from porch.api.restplus import api
from porch.decorators import authz_required
from porch.api.machine.serializers import machineSerializer
from porch.api.machine.serializers import machinePostSerializer
from porch.api.machine.serializers import machinePatchSerializer
from porch.api.machine.bizlogic import get_machine
from porch.api.machine.bizlogic import create_machine
from porch.api.machine.bizlogic import update_machine
from porch.api.machine.bizlogic import delete_machine
from porch.api.machine.bizlogic import provision_machine
log = logging.getLogger('porch')
ns = api.namespace('machine', description="Operations for machine")
@ns.route('/')
class MachineCollection(Resource):
@ns.marshal_list_with(machineSerializer)
@jwt_required
def get(self):
"""Returns list of machine."""
l_machine = get_machine()
return l_machine
@ns.response(201, 'Machine successfully created.')
@ns.expect(machinePostSerializer)
@jwt_required
def post(self):
"""Creates a new machine.
"""
i_ret_code = 201
data = request.json
(b_ret, s_msg) = create_machine(data)
if not b_ret:
d_msg = {'error': s_msg}
i_ret_code = 400
else:
d_msg = {'msg': s_msg}
return d_msg, i_ret_code
@ns.route('/<string:name>')
class MachineItem(Resource):
@ns.marshal_with(machineSerializer)
@ns.doc('get_something')
@jwt_required
def get(self, name):
"""Returns the machine information."""
i_ret_code = 200
l_machine = get_machine(name)
if not len(l_machine):
d_msg = {'error': 'name {} not found.'.format(name)}
i_ret_code = 404
return d_msg, i_ret_code
else:
return l_machine[0], i_ret_code
@ns.expect(machinePatchSerializer)
@jwt_required
def patch(self, name):
"""Updates the machine information."""
data = request.json
(b_ret, s_msg) = update_machine(name, data)
if not b_ret:
d_msg = {'error': s_msg}
return d_msg, 404
else:
l_machine = get_machine(name)
return l_machine[0], 200
@ns.response(204, "The machine is successfully deleted.")
@authz_required('admin')
def delete(self, name):
"""Deletes the machine."""
(b_ret, s_msg) = delete_machine(name)
if not b_ret:
d_msg = {'error': s_msg}
return d_msg, 404
return None, 204
@ns.route('/<string:name>/provision')
class MachineProvision(Resource):
@ns.response(200, 'Machine successfully provisioned.')
@jwt_required
def post(self, name):
"""Provision a new machine.
"""
i_ret_code = 200
(b_ret, s_msg) = provision_machine(name)
if not b_ret:
d_msg = {'error': s_msg}
i_ret_code = 400
else:
d_msg = {'msg': s_msg}
return d_msg, i_ret_code
from flask_restplus import fields
from porch.api.restplus import api
machineSerializer = api.model('ListMachine', {
'name': fields.String(required=True, description='machine hostname'),
'type': fields.String(required=True, description='type: PM or VM'),
'uid': fields.String(required=True, description='machine uuid'),
'desc': fields.String(required=False, description='machine description'),
'section': fields.String(required=True, default='default',
description='the section machine belongs to'),
'os': fields.String(required=True, description='machine os'),
'preseed': fields.String(required=True,
default='', description='machine preseed'),
'mac': fields.String(required=True, description='machine mac'),
'ip': fields.String(required=True, description='machine ip address'),
'state': fields.String(required=True, default='Created',
description='state: Created, Provisioned'),
'is_installed': fields.Boolean(required=True, default=False,
description='machine install flag'),
'createdAt': fields.DateTime(required=True, description='machine created'),
'modifiedAt': fields.DateTime(required=False, description='machine mod time'),
})
machinePostSerializer = api.model('RegisterMachine', {
'name': fields.String(required=True, description='machine hostname'),
'type': fields.String(required=True, description='type: PM or VM'),
'desc': fields.String(required=False, description='machine description'),
'os': fields.String(required=True, description='machine os'),
'preseed': fields.String(required=True, description='machine preseed'),
'mac': fields.String(required=True, description='machine mac'),
'ip': fields.String(required=True, description='machine ip address'),
})
machinePatchSerializer = api.model('ModifyMachine', {
'desc': fields.String(required=False, description='machine description'),
'section': fields.String(required=False,
description='the section machine belongs to'),
'os': fields.String(required=False, description='machine os'),
'preseed': fields.String(required=False, description='machine preseed'),
'mac': fields.String(required=False, description='machine mac'),
'ip': fields.String(required=False, description='machine ip address'),
'state': fields.String(required=False, default='Created',
description='state: Created, Provisioned'),
'is_installed': fields.Boolean(required=False,
description='machine install flag'),
})
import logging
import traceback
from flask_restplus import Api
from porch import settings
log = logging.getLogger(__name__)
api = Api(version='1.0',
title='PORCH API',
description='PORCH delivers machine and app instances at your porch.',
contact='jijisa@iorchard.net',
license='My own license',
license_url='http://wherever_license_is_on/',
endpoint='api',
validate=True,
)
@api.errorhandler
def default_error_handler(e):
message = 'An unhandled exception occurred.'
log.exception(message)
if not settings.FLASK_DEBUG:
return {'message': message}, 500
import uuid
import ast
import logging
import logging.config
from datetime import datetime
from porch.database import etcdc
logging.config.fileConfig('logging.conf')
log = logging.getLogger('porch')
def get_section(name=None):
"""Get section list.
"""
l_section = list()
s_rsc = ''
if name is None: # get all section.
s_rsc = '{}/section'.format(etcdc.prefix)
else:
s_rsc = '{}/section/{}'.format(etcdc.prefix, name)
try:
r = etcdc.read(s_rsc, recursive=True, sorted=True)
except etcd.EtcdKeyNotFound as e:
log.error(e)
else:
for child in r.children:
if child.value is not None:
# need to use ast to convert str to dict.
l_section.append(ast.literal_eval(child.value))
finally:
return l_section
def create_section(data):
"""Create section.
etcd_key: <ETCD_PREFIX>/section/<name>
"""
t_ret = (False, '')
s_rsc = ''
# Create uid(uuid) for section
s_uuid = str(uuid.uuid4())
# Get iso 8601 format datetime for created timestamp
s_created = datetime.utcnow().isoformat() + 'Z'
# Put s_created into data dict.
data['createdAt'] = s_created
data['uid'] = s_uuid
s_rsc = '{}/section/{}'.format(etcdc.prefix, data['name'])
try:
etcdc.write(s_rsc, data, prevExist=False)
except etcd.EtcdKeyAlreadyExist as e:
log.error(e)
t_ret = (False, e)
else:
t_ret = (True, 'section {} is created.'.format(data['name']))
return t_ret
def update_section(name, data):
"""Update section.
"""
if not name: # <name> should be specified.
s_msg = 'name should be specified.'
return (False, s_msg)
# Delete the keys whose values are empty.
data = dict((k, v) for k, v in data.items() if v != '')
s_rsc = '{}/section/{}'.format(etcdc.prefix, name)
try:
r = etcdc.read(s_rsc)
except etcd.EtcdKeyNotFound as e:
log.error(e)
return (False, e)
d = ast.literal_eval(r.value)
# Get iso 8601 format datetime for modified timestamp
s_modified = datetime.utcnow().isoformat() + 'Z'
# Put s_modified into data dict.
data['modifiedAt'] = s_modified
d.update(data.items())
try:
etcdc.write(s_rsc, d, prevExist=True)
except:
s_msg = 'section {} update is failed.'.format(name)
log.error(s_msg)
return (False, s_msg)
else:
s_msg = 'section {} is updated.'.format(name)
return (True, s_msg)
def delete_section(name):
"""Delete section.
"""
if not name: # <name> should be specified.
return (False, 'name should be specified.')
s_rsc = '{}/section/{}'.format(etcdc.prefix, name)
r = etcdc.delete(s_rsc)
return (True, 'machine {} is deleted.'.format(name))
import logging
import logging.config
from flask import request
from flask import abort
from flask_restplus import Resource
from porch.api.restplus import api
from porch.decorators import authz_required
from porch.api.section.serializers import sectionSerializer
from porch.api.section.serializers import sectionPostSerializer
from porch.api.section.serializers import sectionPatchSerializer
from porch.api.section.bizlogic import get_section
from porch.api.section.bizlogic import create_section
from porch.api.section.bizlogic import update_section
from porch.api.section.bizlogic import delete_section
log = logging.getLogger('porch')
ns = api.namespace('section', description="Operations for section")
@ns.route('/')
class SectionCollection(Resource):
@ns.marshal_list_with(sectionSerializer)
@authz_required('user')
def get(self):
"""Returns list of section."""
l_section = get_section()
return l_section
@ns.response(201, 'Section successfully created.')
@ns.expect(sectionPostSerializer)
@authz_required('user')
def post(self):
"""Creates a new section.
"""
i_ret_code = 201
data = request.json
(b_ret, s_msg) = create_section(data)
if not b_ret:
d_msg = {'error', s_msg}
i_ret_code = 400
else:
d_msg = {'msg': s_msg}
return d_msg, i_ret_code
@ns.route('/<string:name>')
@ns.response(404, 'Section not found.')
class SectionItem(Resource):
@ns.marshal_with(sectionSerializer)
@ns.doc('get_something')
@authz_required('user')
def get(self, name):
"""Returns the section information."""
i_ret_code = 200
l_section = get_section(name)
if not len(l_section):
d_msg = {'error': 'name {} not found.'.format(name)}
i_ret_code = 404
return d_msg, i_ret_code
else:
return l_section[0], i_ret_code
@ns.expect(sectionPatchSerializer)
@ns.response(204, "The section is successfully updated.")
@authz_required('user')
def patch(self, name):
"""Updates the section information."""
data = request.json
(b_ret, s_msg) = update_section(name, data)
if not b_ret:
d_msg = {'error': s_msg}
return d_msg, 404
else:
l_section = get_section(name)
return l_section[0], 200
@ns.response(204, "The section is successfully deleted.")
@authz_required('admin')
def delete(self, name):
"""Deletes the section."""
(b_ret, s_msg) = delete_section(name)
if not b_ret:
d_msg = {'error': s_msg}
return d_msg, 404
return None, 204
from flask_restplus import fields
from porch.api.restplus import api
sectionSerializer = api.model('ListSection', {
'name': fields.String(required=True, description='section name'),
'cn': fields.String(required=True, description='section common name'),
'uid': fields.String(required=True, description='section uuid'),
'desc': fields.String(required=False, description='section description'),
'createdAt': fields.DateTime(required=True, description='section created'),
'modifiedAt': fields.DateTime(required=False, description='section mod time'),
})
sectionPostSerializer = api.model('RegisterSection', {
'name': fields.String(required=True, description='section name'),
'cn': fields.String(required=True, description='section common name'),
'desc': fields.String(required=False, description='section description'),
})
sectionPatchSerializer = api.model('ModifySection', {
'cn': fields.String(required=True, description='section common name'),
'desc': fields.String(required=False, description='section description'),
})
import time
import datetime
import logging.config
from flask import Flask
from flask import Blueprint
from flask_jwt_extended import JWTManager
from flask_cors import CORS, cross_origin
from porch import settings
from porch.config import JWT_SECRET_KEY
from porch.config import JWT_ACCESS_TOKEN_EXPIRES
from porch.api.restplus import api
from porch.api.arrangement.endpoints.route import ns as arrangement_ns
from porch.api.section.endpoints.route import ns as section_ns
from porch.api.machine.endpoints.route import ns as machine_ns
from porch.api.fai.endpoints.route import ns as fai_ns
from porch.api.account.endpoints.route import ns as account_ns
from porch.api.app.endpoints.route import ns as app_ns
app = Flask(__name__)
app.secret_key = JWT_SECRET_KEY
jwt = JWTManager(app)
@jwt.user_claims_loader
def add_claims_to_access_token(identity):
s_role = 'admin' if identity == 'admin' else 'user'
return {
'id': identity,
'role': s_role
}
CORS(app)
logging.config.fileConfig('logging.conf')
log = logging.getLogger(__name__)
def configure_app(flask_app):
flask_app.config['SWAGGER_UI_DOC_EXPANSION'] = \
settings.RESTPLUS_SWAGGER_UI_DOC_EXPANSION
flask_app.config['RESTPLUS_VALIDATE'] = settings.RESTPLUS_VALIDATE
flask_app.config['RESTPLUS_MASK_SWAGGER'] = settings.RESTPLUS_MASK_SWAGGER
flask_app.config['ERROR_404_HELP'] = settings.RESTPLUS_ERROR_404_HELP
# JWT config
flask_app.config['JWT_ACCESS_TOKEN_EXPIRES'] = JWT_ACCESS_TOKEN_EXPIRES
# disable strict slashes
flask_app.url_map.strict_slashes = False
def initialize_app(flask_app):
configure_app(flask_app)
blueprint = Blueprint('api', __name__, url_prefix='/api')
api.init_app(blueprint)
api.add_namespace(arrangement_ns)
api.add_namespace(section_ns)
api.add_namespace(machine_ns)
api.add_namespace(fai_ns)
api.add_namespace(account_ns)
api.add_namespace(app_ns)
flask_app.register_blueprint(blueprint)
def main():
initialize_app(app)
app.run(
debug=settings.FLASK_DEBUG,
host=settings.FLASK_HOST,
port=settings.FLASK_PORT,
)
if __name__ == "__main__":
main()
import os
from datetime import timedelta
APP_ROOT = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(APP_ROOT)
# pxe
TFTP_FAI_DIR = "/srv/tftp/fai"
BOOT = TFTP_FAI_DIR + "/boot.ipxe"
WINBOOT = TFTP_FAI_DIR + "/winpe/boot.ipxe"
LINBOOT = TFTP_FAI_DIR + "/lpxelinux.0"
PXELINUXCFG = TFTP_FAI_DIR + '/pxelinux.cfg'
## URL
PORCH_URL = 'http://192.168.24.50/porch' # Put PORCH's pxe network ip address.
FAI_KERNEL_URL = PORCH_URL + '/boot/vmlinuz-3.16.0-4-amd64'
FAI_INITRD_URL = PORCH_URL + '/boot/initrd.img-3.16.0-4-amd64'
FAI_SQUASH_URL = PORCH_URL + '/boot/squash.img'
## FAI related
FAI_DIR = "/srv/fai"
PRIKEY_DIR = FAI_DIR + "/prikeys"
PUBKEY_DIR = FAI_DIR + "/pubkeys"
WINANS_DIR = "/answer"
# JWT secret key
# Create random key: binascii.hexlify(os.urandom(24)).decode()
JWT_SECRET_KEY = '911b05b71a7cbf24eecd03b82fdc4d4f934282cbaca98bf3'
JWT_ACCESS_TOKEN_EXPIRES = timedelta(minutes=60)
#JWT_ACCESS_TOKEN_EXPIRES is 15 minutes default.
#JWT_REFRESH_TOKEN_EXPIRES is 1 month default.
# dhcp-edit
DHCPEDIT = '/usr/sbin/dhcp-edit'
# git
GIT_DIR = APP_ROOT + '/git'
PLAY_DIR = APP_ROOT + '/play'
from etcd import Client
from porch.settings import ETCD_HOST, ETCD_PORT, ETCD_PROTO, ETCD_PREFIX
etcdc = Client(
host=ETCD_HOST,
port=ETCD_PORT,
allow_reconnect=True,
protocol=ETCD_PROTO
)
etcdc.prefix = ETCD_PREFIX
from functools import wraps
from flask_restplus import abort
from flask_jwt_extended import jwt_required
from flask_jwt_extended import get_jwt_claims
import logging
import logging.config
log = logging.getLogger('porch')
def authz_required(role='admin'):
def authz_deco(f):
@wraps(f)
@jwt_required
def decorated_function(*args, **kwargs):
# Get role.
d_claim = get_jwt_claims()
log.debug(d_claim)
log.debug(role)
if d_claim['role'] != 'admin':
if 'role' not in d_claim or d_claim['role'] != role:
d_msg = {'error': 'Authorization failed.'}
log.debug(d_msg)
abort(401)
return f(*args, **kwargs)
return decorated_function
return authz_deco
# Flask settings
#FLASK_SERVER_NAME = '121.254.203.198:5000'
FLASK_HOST = '0.0.0.0'
FLASK_PORT = 8000
FLASK_DEBUG = True # Do not use debug mode in production
# Flask-Restplus settings
RESTPLUS_SWAGGER_UI_DOC_EXPANSION = 'list'
RESTPLUS_VALIDATE = True
RESTPLUS_MASK_SWAGGER = False
RESTPLUS_ERROR_404_HELP = False
# ETCD settings
ETCD_HOST = '192.168.24.31'
ETCD_PORT = 2379
ETCD_PROTO = 'http'
ETCD_PREFIX = '/porch'
{% for machine in data %}
{{ machine }} ansible_connection=ssh ansible_user=root
{% endfor %}
default porch-generated
label porch-generated
kernel {{ data['fai_kernel_url'] }}
append initrd={{ data['fai_initrd_url'] }} ip=dhcp root=live:{{ data['fai_squash_url'] }} aufs FAI_FLAGS=verbose,sshd,reboot FAI_CONFIG_SRC={{ data['fai_config_url'] }} FAI_ACTION=install
import logging
import logging.config
import subprocess
import shlex
import time
from subprocess import Popen
from subprocess import PIPE
from subprocess import STDOUT
from subprocess import TimeoutExpired
from porch.database import etcdc
logging.config.fileConfig('logging.conf')
log = logging.getLogger('porch')
def cmd(s_cmd, i_timeout=30, b_sudo=False):
"""Execute s_cmd using Popen.i
Return:
t_result: tuple = (
b_ret: boolen, cmd return code. 0=True or False
s_output: string, json string
)
"""
t_result = (False, {})
if b_sudo:
s_cmd = "LANG=C sudo %s" % (s_cmd)
o_proc = Popen(s_cmd, shell=True, stdout=PIPE, stderr=STDOUT)
try:
(B_outs, B_errs) = o_proc.communicate(timeout=i_timeout)
except TimeoutExpired:
o_proc.kill()
(B_outs, B_errs) = o_proc.communicate()
i_retcode = int(o_proc.returncode)
b_ret = i_retcode == 0
s_output = B_outs.decode('utf-8').strip()
t_result = (b_ret, s_output)
return t_result
def run_command(command, name):
process = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
s_rsc = '{}/log/{}'.format(etcdc.prefix, name)
while True:
output = process.stdout.readline().decode()
if output == '' and process.poll() is not None:
break
if output:
s_created = datetime.utcnow().isoformat() + 'Z'
data[s_created] = output.strip()
rc = process.poll()
return rc
export PYTHONPATH=".:$PYTHONPATH"
python porch/app.py
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment