Adding container support

selenium-screenshot-testing
Kevin Chung 2016-05-07 16:38:10 -04:00
parent a479b3a7aa
commit d2778c30ad
7 changed files with 236 additions and 32 deletions

View File

@ -1,5 +1,5 @@
from flask import render_template, request, redirect, abort, jsonify, url_for, session, Blueprint
from CTFd.utils import sha512, is_safe_url, authed, admins_only, is_admin, unix_time, unix_time_millis, get_config, set_config, sendmail, rmdir, create_container
from CTFd.utils import sha512, is_safe_url, authed, admins_only, is_admin, unix_time, unix_time_millis, get_config, set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, container_stop, container_start
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from itsdangerous import TimedSerializer, BadTimeSignature
from sqlalchemy.sql import and_, or_, not_
@ -227,19 +227,58 @@ def delete_page(pageroute):
@admins_only
def list_container():
containers = Containers.query.all()
for c in containers:
c.status = container_status(c.name)
c.ports = ", ".join(container_ports(c.name, verbose=True))
return render_template('admin/containers.html', containers=containers)
@admin.route('/admin/containers/<container_id>/stop', methods=['POST'])
@admins_only
def stop_container(container_id):
container = Containers.query.filter_by(id=container_id).first_or_404()
if container_stop(container.name):
return '1'
else:
return '0'
@admin.route('/admin/containers/<container_id>/start', methods=['POST'])
@admins_only
def run_container(container_id):
container = Containers.query.filter_by(id=container_id).first_or_404()
if container_status(container.name) == 'missing':
if run_image(container.name):
return '1'
else:
return '0'
else:
if container_start(container.name):
return '1'
else:
return '0'
@admin.route('/admin/containers/<container_id>/delete', methods=['POST'])
@admins_only
def delete_container(container_id):
container = Containers.query.filter_by(id=container_id).first_or_404()
if delete_image(container.name):
db.session.delete(container)
db.session.commit()
db.session.close()
return '1'
@admin.route('/admin/containers/new', methods=['POST'])
@admins_only
def new_container():
name = request.form.get('name')
buildfile = request.form.get('buildfile')
files = request.files.getlist('files[]')
print name
print buildfile
print files
create_container(name=name, buildfile=buildfile, files=files)
create_image(name=name, buildfile=buildfile, files=files)
run_image(name)
return redirect('/admin/containers')

View File

@ -67,6 +67,8 @@ def chals_per_solves():
@challenges.route('/solves')
@challenges.route('/solves/<teamid>')
def solves(teamid=None):
solves = None
awards = None
if teamid is None:
if is_admin():
solves = Solves.query.filter_by(teamid=session['id']).all()

View File

@ -35,7 +35,9 @@
<li><a href="/admin/pages">Pages</a></li>
<li><a href="/admin/teams">Teams</a></li>
<li><a href="/admin/scoreboard">Scoreboard</a></li>
{% if can_create_container() %}
<li><a href="/admin/containers">Containers</a></li>
{% endif %}
<li><a href="/admin/chals">Challenges</a></li>
<li><a href="/admin/statistics">Statistics</a></li>
<li><a href="/admin/config">Config</a></li>

View File

@ -34,6 +34,32 @@
</div>
</div>
<div id="confirm" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h2 class="text-center"><span id="confirm-container-title"></span> Container</h2>
</div>
<div class="modal-body" style="height:110px">
<div class="row-fluid">
<div class="col-md-12">
<form method="POST">
<input id="nonce" type="hidden" name="nonce" value="{{ nonce }}">
<div class="small-6 small-centered text-center columns">
<p>Are you sure you want to <span id="confirm-container-method"></span> <strong id="confirm-container-name"></strong>?</p>
<button type="button" data-dismiss="modal" class="btn btn-theme btn-outlined">No</button>
<button type="button" id="confirm-container" class="btn btn-theme btn-outlined">Yes</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<br>
<div style="text-align:center">
@ -47,31 +73,29 @@
<table id="teamsboard">
<thead>
<tr>
<td><b>Status</b>
<td class="text-center"><strong>Status</strong>
</td>
<td><b>Name</b>
<td class="text-center"><strong>Name</strong>
</td>
<td><b>IP Address</b>
<td class="text-center"><strong>Ports</strong>
</td>
<td><b>Memory</b>
</td>
<td><b>Disk</b>
</td>
<td><b>Settings</b>
<td class="text-center"><strong>Settings</strong>
</td>
</tr>
</thead>
<tbody>
{% for c in containers %}
<tr>
<td>{{ c.name }}</td>
<td>{{ c.status }}</td>
<td>{{ c.ip_address }}</td>
<td>{{ c.memory }} <sub>MB</sub></td>
<td>{{ c.disk }} <sub>GB</sub></td>
<td>
<td class="text-center">{{ c.status }}</td>
<td class="text-center container_item" id="{{ c.id }}">{{ c.name }}</td>
<td class="text-center">{{ c.ports }}</td>
<td class="text-center">
<span>
<i class="fa fa-pencil-square-o"></i>
{% if c.status != 'running' %}
<i class="fa fa-play"></i>
{% else %}
<i class="fa fa-stop"></i>
{% endif %}
<i class="fa fa-times"></i>
</span>
</td>
@ -84,4 +108,50 @@
{% endblock %}
{% block scripts %}
<script>
function load_confirm_modal(title, url, container_name){
var modal = $('#confirm')
modal.find('#confirm-container-name').text(container_name)
modal.find('#confirm-container-title').text(title)
modal.find('#confirm-container-method').text(title.toLowerCase())
$('#confirm form').attr('action', url);
$('#confirm').modal('show');
}
$('#confirm-container').click(function(e){
e.preventDefault();
var id = $('#confirm input[name="id"]').val()
var user_data = $('#confirm form').serializeArray()
$.post($('#confirm form').attr('action'), $('#confirm form').serialize(), function(data){
var data = $.parseJSON(JSON.stringify(data))
if (data == "1"){
location.reload()
}
})
});
$('.fa-times').click(function(){
var elem = $(this).parent().parent().parent().find('.container_item');
var container = elem.attr('id');
var container_name = elem.text().trim();
load_confirm_modal('Delete', '/admin/containers/'+container+'/delete', container_name)
});
$('.fa-play').click(function(){
var elem = $(this).parent().parent().parent().find('.container_item');
var container = elem.attr('id');
var container_name = elem.text().trim();
load_confirm_modal('Start', '/admin/containers/'+container+'/start', container_name)
});
$('.fa-stop').click(function(){
var elem = $(this).parent().parent().parent().find('.container_item');
var container = elem.attr('id');
var container_name = elem.text().trim();
load_confirm_modal('Stop', '/admin/containers/'+container+'/stop', container_name)
});
</script>
{% endblock %}

View File

@ -231,7 +231,7 @@ $('#send-user-email').click(function(e){
});
});
$('#delete-user').click(function(e){
$('#delete-container').click(function(e){
e.preventDefault();
var id = $('#confirm input[name="id"]').val()
var user_data = $('#confirm form').serializeArray()

View File

@ -5,7 +5,7 @@ from werkzeug.utils import secure_filename
from functools import wraps
from flask import current_app as app, g, request, redirect, url_for, session, render_template, abort
from itsdangerous import Signer, BadSignature
from socket import inet_aton, inet_ntoa
from socket import inet_aton, inet_ntoa, socket
from struct import unpack, pack
from sqlalchemy.engine.url import make_url
from sqlalchemy import create_engine
@ -24,6 +24,7 @@ import smtplib
import email
import tempfile
import subprocess
import json
def init_logs(app):
logger_keys = logging.getLogger('keys')
@ -92,6 +93,7 @@ def init_utils(app):
app.jinja_env.globals.update(can_register=can_register)
app.jinja_env.globals.update(mailserver=mailserver)
app.jinja_env.globals.update(ctf_name=ctf_name)
app.jinja_env.globals.update(can_create_container=can_create_container)
@app.context_processor
def inject_user():
@ -393,7 +395,16 @@ def can_create_container():
return False
def create_container(name, buildfile, files):
def is_port_free(port):
s = socket()
result = s.connect_ex(('127.0.0.1', port))
if result == 0:
s.close()
return False
return True
def create_image(name, buildfile, files):
if not can_create_container():
return False
folder = tempfile.mkdtemp(prefix='ctfd')
@ -404,10 +415,12 @@ def create_container(name, buildfile, files):
for f in files:
filename = os.path.basename(f.filename)
f.save(os.path.join(folder, filename))
# repository name component must match "[a-z0-9](?:-*[a-z0-9])*(?:[._][a-z0-9](?:-*[a-z0-9])*)*"
# docker build -f tmpfile.name -t name
try:
subprocess.call(['docker', 'build', '-f', tmpfile.name, '-t', name])
cmd = ['docker', 'build', '-f', tmpfile.name, '-t', name, folder]
print cmd
subprocess.call(cmd)
container = Containers(name, buildfile)
db.session.add(container)
db.session.commit()
@ -415,3 +428,83 @@ def create_container(name, buildfile, files):
return True
except subprocess.CalledProcessError:
return False
def delete_image(name):
try:
subprocess.call(['docker', 'rm', name])
subprocess.call(['docker', 'rmi', name])
return True
except subprocess.CalledProcessError:
return False
def run_image(name):
try:
info = json.loads(subprocess.check_output(['docker', 'inspect', '--type=image', name]))
ports_asked = info[0]['Config']['ExposedPorts'].keys()
ports_asked = [int(re.sub('[A-Za-z/]+', '', port)) for port in ports_asked]
cmd = ['docker', 'run', '-d']
for port in ports_asked:
if is_port_free(port):
cmd.append('-p')
cmd.append('{}:{}'.format(port, port))
else:
cmd.append('-p')
ports_used.append('{}'.format(port))
cmd += ['--name', name, name]
print cmd
subprocess.call(cmd)
return True
except subprocess.CalledProcessError:
return False
def container_start(name):
try:
cmd = ['docker', 'start', name]
subprocess.call(cmd)
return True
except subprocess.CalledProcessError:
return False
def container_stop(name):
try:
cmd = ['docker', 'stop', name]
subprocess.call(cmd)
return True
except subprocess.CalledProcessError:
return False
def container_status(name):
try:
data = json.loads(subprocess.check_output(['docker', 'inspect', '--type=container', name]))
status = data[0]["State"]["Status"]
return status
except subprocess.CalledProcessError:
return 'missing'
def container_ports(name, verbose=False):
try:
info = json.loads(subprocess.check_output(['docker', 'inspect', '--type=container', name]))
if verbose:
ports = info[0]["NetworkSettings"]["Ports"]
if not ports:
return []
final = []
for port in ports.keys():
final.append("".join([ports[port][0]["HostPort"], '->', port]))
return final
else:
ports = info[0]['Config']['ExposedPorts'].keys()
if not ports:
return []
ports = [int(re.sub('[A-Za-z/]+', '', port)) for port in ports_asked]
return ports
except subprocess.CalledProcessError:
return []

View File

@ -135,9 +135,7 @@ def teams(page):
def team(teamid):
if get_config('view_scoreboard_if_authed') and not authed():
return redirect(url_for('auth.login', next=request.path))
user = Teams.query.filter_by(id=teamid).first()
if not user:
abort(404)
user = Teams.query.filter_by(id=teamid).first_or_404()
solves = Solves.query.filter_by(teamid=teamid)
awards = Awards.query.filter_by(teamid=teamid).all()
score = user.score()