Commit f655fbfe by Heechul Kim

updated

parent c80c0ac4
......@@ -6,9 +6,12 @@ import logging
import logging.config
from datetime import datetime
from datetime import timedelta
from porch.database import etcdc
from porch.config import ADMIN_ID
from porch.config import ADMIN_SALT
from porch.config import ADMIN_PW
logging.config.fileConfig('logging.conf')
log = logging.getLogger('porch')
......@@ -71,11 +74,24 @@ def admin_auth_account(data):
try:
r = etcdc.read(s_rsc)
except etcd.EtcdKeyNotFound as e:
log.error(e)
# auth against config file.
s_pass = bcrypt.hashpw(data['pass'].encode(),
ADMIN_SALT.encode()).decode()
if data['name'] == ADMIN_ID and s_pass == ADMIN_PW:
# Add admin entry in etcd and return True.
d = dict()
d['name'] = ADMIN_ID
d['pass'] = ADMIN_PW
d['cn'] = 'Admin'
d['state'] = 'Enabled'
s_expire = (datetime.now() + timedelta(days=90)).isoformat() + 'Z'
d['expireAt'] = s_expire
(b_ret, s_msg) = create_account(d)
return b_ret
else:
return False
else:
d = ast.literal_eval(r.value)
s_pass = bcrypt.hashpw(data['pass'].encode(),
d['salt'].encode()).decode()
return data['name'] == ADMIN_ID and s_pass == d['pass']
......@@ -190,15 +206,14 @@ def password_account(data):
data['modifiedAt'] = s_modified
# Put d['pass'] to oldpass entry.
if 'oldpass' in d:
new_data['oldpass'].append(d['pass'])
d['oldpass'].append(d['pass'])
else:
new_data['oldpass'] = [d['pass']]
d['oldpass'] = [d['pass']]
# Create new hashed password.
bytes_salt = bytes(d['salt'], 'utf-8')
new_data['pass'] = bcrypt.hashpw(str.encode(data['pass']),
d['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)
......@@ -235,3 +250,58 @@ def _pass_validate(data):
return (False, 'PasswordPreviouslyUsed')
return (True, 'PasswordMatched')
def user_password_account(data):
"""Modify user account password.
etcd_key: <ETCD_PREFIX>/account/<name>
data: {'name': , 'pass': , 'pass1': , '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 if data['pass'] == d['pass']
s_pass = bcrypt.hashpw(data['pass'].encode(),
d['salt'].encode()).decode()
if s_pass != d['pass']: # current password mismatch
return (False, 'Current password is not matched.')
# since data['pass'] is matched, overwrite data['pass1'] to data['pass']
# to validate new password.
data['pass'] = data['pass1']
(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:
d['oldpass'].append(d['pass'])
else:
d['oldpass'] = [d['pass']]
# Create new hashed password.
bytes_salt = bytes(d['salt'], 'utf-8')
d['pass'] = bcrypt.hashpw(str.encode(data['pass']),
bytes_salt).decode()
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
......@@ -18,11 +18,14 @@ from flask_jwt_extended import get_jwt_claims
from porch.api.restplus import api
from porch.decorators import authz_required
from porch.config import ADMIN_ID
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.serializers import accountUserPasswordSerializer
from porch.api.account.bizlogic import get_account
from porch.api.account.bizlogic import create_account
......@@ -31,6 +34,7 @@ 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
from porch.api.account.bizlogic import user_password_account
log = logging.getLogger('porch')
......@@ -74,6 +78,10 @@ class AdminAccountAuth(Resource):
"""Authenticate an account.
"""
data = request.json
if data['name'] != ADMIN_ID:
d_msg = { 'error': 'Authentication is failed.' }
return d_msg, 401
b_ret = admin_auth_account(data)
if not b_ret:
d_msg = {'error': 'Admin Authentication is failed.'}
......@@ -97,6 +105,10 @@ class AccountAuth(Resource):
"""Authenticate an account.
"""
data = request.json
if data['name'] == ADMIN_ID:
d_msg = { 'error': 'Authentication is failed.' }
return d_msg, 401
(b_ret, s_ret) = auth_account(data)
if not b_ret:
s_msg = 'Authentication is failed. '
......@@ -137,6 +149,7 @@ class AccountRefresh(Resource):
d_resp = {
'token': create_access_token(identity=uid)
}
log.debug(d_resp)
return d_resp, 200
@ns.route('/<string:name>')
......@@ -196,7 +209,7 @@ class AccountPassword(Resource):
"""Updates the account password."""
d_claim = get_jwt_claims()
# check authz
if d_claim['id'] != 'admin' and name != d_claim['id']:
if d_claim['id'] != 'admin':
abort(401, 'Not authorized.')
data = request.json
(b_ret, s_msg) = password_account(data)
......@@ -206,3 +219,26 @@ class AccountPassword(Resource):
l_account = get_account(name)
return l_account[0], 200
@ns.route('/<string:name>/userpass')
@api.response(404, 'Account not found.')
class AccountUserPassword(Resource):
@ns.expect(accountUserPasswordSerializer)
@ns.response(204, "The account is successfully updated.")
@jwt_required
def patch(self, name):
"""Updates the account password as user role."""
d_claim = get_jwt_claims()
# check authz: only user itself.
if name != d_claim['id']:
abort(401, 'Not authorized.')
data = request.json
(b_ret, s_msg) = user_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
......@@ -58,3 +58,9 @@ accountPasswordSerializer = api.model('PasswordAccount', {
'pass': fields.String(required=True, description='plaintext password'),
'pass2': fields.String(required=True, description='plaintext password'),
})
accountUserPasswordSerializer = api.model('UserPasswordAccount', {
'name': fields.String(required=True, description='account name'),
'pass': fields.String(required=True, description='plaintext password'),
'pass1': fields.String(required=True, description='plaintext password'),
'pass2': fields.String(required=True, description='plaintext password'),
})
import os
import ast
import time
import etcd
import uuid
import shutil
import signal
import tempfile
import logging
import logging.config
from subprocess import Popen
from concurrent.futures import ProcessPoolExecutor
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.utils import get_port
from porch.database import etcdc
from porch.config import GIT_DIR
from porch.config import PLAY_DIR
from porch.config import PRIKEY_DIR
from porch.config import APP_LOG
from porch.config import FILE_LOG
from porch.config import APP_ROOT
from porch.config import PORCH_USER
from porch.config import PORCHSHELL
logging.config.fileConfig('logging.conf')
log = logging.getLogger('porch')
......@@ -164,7 +172,7 @@ def play_app(name, data):
d = ast.literal_eval(r.value)
# get repo and tag.
s_play_dir = PLAY_DIR + '/' + name + str(time.time())
s_play_dir = PLAY_DIR + '/' + name + datetime.now().isoformat()
o_repo = Repo.clone_from(
'file://' + GIT_DIR + '/' + d['repo'],
......@@ -183,39 +191,162 @@ def play_app(name, data):
# 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)
s_ssh_options = "--ssh-common-args='-o UserKnownHostsFile=/dev/null'"
s_cmd= "ansible-playbook {0} -i {1}/hosts --private-key={2} {1}/site.yml"\
.format(s_ssh_options, 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.'
# set logfile name
s_date = datetime.now().isoformat()
s_logfile = APP_LOG +'/' + s_date
# write to etcd /porch/log/app/<s_date>/{log: s_logfile}
s_rsc = '{}/log/app/{}'.format(etcdc.prefix, s_date)
data['log'] = s_logfile
try:
etcdc.write(s_rsc, data)
except:
s_msg = 'Cannot create log record.'
log.error(s_msg)
return (False, s_msg)
pool = ProcessPoolExecutor(3)
future = pool.submit(_bgprocess, s_cmd, s_logfile)
# Delete cloned app.
try:
shutil.rmtree(s_play_dir)
except FileNotFoundError as e:
log.error(e)
#try:
# shutil.rmtree(s_play_dir)
#except FileNotFoundError as e:
# log.error(e)
return (True, s_date)
return (True, 'Play is succeeded.')
def _bgprocess(s_cmd, s_logfile):
log.debug(s_logfile)
with open(s_logfile, 'w+') as f:
Popen(s_cmd, shell=True, stdout=f, stderr=f)
def get_app_log(name):
def get_app_log(s_logfile):
"""Get app log.
"""
l_log = []
s_play_dir = PLAY_DIR + '/' + name
s_logfile = s_play_dir + '/' + name + '.log'
s_log = ''
log.debug(s_logfile)
try:
with open(s_logfile, 'r') as f:
s_log = f.read()
except:
return l_log
pass
return s_log
def file_container_app(name):
"""Run container for app file management."""
# get port for container connection
i_port = get_port()
# check if container is already running.
s_cmd = "docker inspect {}".format(name)
log.debug(s_cmd)
(b_ret, l_out) = cmd(s_cmd)
if not b_ret:
# run docker container for file management.
s_cmd = "docker run -it --rm -d " + \
"--name {0} -v /porch_files/{0}:/NOTADIR {1}"\
.format(name, PORCHSHELL)
log.debug(s_cmd)
(b_ret, l_out) = cmd(s_cmd)
if not b_ret:
log.error(l_out)
s_msg = 'Fail to run a container for file management.'
return (b_ret, s_msg)
s_pidfile = '/tmp/{}.pid'.format(name)
s_cmd = "shellinaboxd --background={} ".format(s_pidfile) + \
"--service='/:{0}:{0}:/:".format(PORCH_USER) + \
"docker attach {}' -p {} --disable-ssl -q ".format(name, i_port) + \
"--css {}/static/white-on-black.css".format(APP_ROOT)
log.debug(s_cmd)
Popen(s_cmd, shell=True)
return (True, i_port)
def close_file_container(name):
"""close container console."""
# check if achilterm is running.
s_pidfile = '/tmp/{}.pid'.format(name)
if os.path.isfile(s_pidfile):
with open(s_pidfile, 'r') as f:
i_pid = f.read()
try:
os.kill(int(i_pid), signal.SIGTERM)
except:
log.info('pid {} not found. skip it'.format(i_pid))
else:
l_log.append(s_log)
log.info('pid {} is killed.'.format(i_pid))
s_cmd = "docker stop {}".format(name)
log.debug(s_cmd)
(b_ret, l_out) = cmd(s_cmd)
if not b_ret:
log.error(l_out)
return True
def file_distribute_app(data):
"""Distribute files of app
data = {'name':, 'machines': []}
"""
# chown /porch_files/<app-name> to PORCH_USER
s_cmd = "chown -R {0}:{0} /porch_files/{1}".format(PORCH_USER, data['name'])
log.debug(s_cmd)
(b_ret, l_out) = cmd(s_cmd, b_sudo=True)
if not b_ret:
log.error(l_out)
return False
# Let's distribute.
# set logfile name
s_date = datetime.now().isoformat()
s_logfile = FILE_LOG + '/' + s_date
# write to etcd /porch/log/file/<s_date>/<data>
data['log'] = s_logfile
log.debug(data)
s_rsc = '{}/log/file/{}'.format(etcdc.prefix, s_date)
try:
etcdc.write(s_rsc, data)
except:
s_msg = 'Cannot create log record.'
log.error(s_msg)
return (False, s_msg)
pool = ProcessPoolExecutor(3)
future = pool.submit(_bg_file_distribute, data, s_logfile)
log.debug(l_log)
return l_log
return (True, s_date)
def _bg_file_distribute(data, s_logfile):
sshoptions = "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
try:
with open(s_logfile, 'w+') as f:
f.write('BEGIN: ' + datetime.now().isoformat() + "\n")
for machine in data['machines']:
s_cmd = "rsync -av -e 'ssh {}' ".format(sshoptions) + \
"/porch_files/{}/ root@{}:/"\
.format(data['name'], machine)
f.write(s_cmd + "\n")
Popen(s_cmd, shell=True, stdout=f, stderr=f)
except Exception as e:
log.error(e)
return False
return True
def create_tag(data):
"""Create tag
```
{
"name": string, app name
"tag": string, app tag
}
```
"""
t_ret = (False, '')
......@@ -14,12 +14,19 @@ 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.serializers import appFileDistributeSerializer
from porch.config import MY_IP
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
from porch.api.app.bizlogic import get_app_log
from porch.api.app.bizlogic import file_container_app
from porch.api.app.bizlogic import close_file_container
from porch.api.app.bizlogic import file_distribute_app
log = logging.getLogger('porch')
......@@ -112,20 +119,64 @@ class AppPlay(Resource):
data = request.json
(b_ret, s_msg) = play_app(name, data)
if not b_ret:
d_msg = {'error', s_msg}
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):
return s_msg, i_ret_code
@ns.route('/<string:name>/play/log')
@ns.response(404, 'App not found.')
class AppPlayLog(Resource):
def post(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
data = request.json
s_log = get_app_log(data['log'])
return s_log
@ns.route('/<string:name>/file_container')
@ns.response(404, 'App not found.')
class AppFileContainer(Resource):
@ns.response(200, 'App File is successfully delivered.')
@jwt_required
def post(self, name):
"""Run a console for app file management.
"""
data = request.json
(b_ret, i_port) = file_container_app(name)
if not b_ret:
d_msg = {'error', s_msg}
return d_msg, 400
s_url = 'http://' + MY_IP + ':{}'.format(i_port)
return s_url
@ns.route('/<string:name>/file_container/close')
@ns.response(404, 'App not found.')
class AppFileContainerClose(Resource):
@jwt_required
def post(self, name):
"""close the catalog file container."""
close_file_container(name)
return {}
@ns.route('/<string:name>/file_distribute')
@ns.response(404, 'App not found.')
class AppFileDistribute(Resource):
@ns.expect(appFileDistributeSerializer)
@jwt_required
def post(self, name):
"""the catalog file distributer."""
data = request.json
(b_ret, s_msg) = file_distribute_app(data)
if not b_ret:
d_msg = {'error': s_msg}
return d_msg, 400
return s_msg, 200
......@@ -27,4 +27,8 @@ appPlaySerializer = api.model('PlayApp', {
'tag': fields.String(required=True, description='app name'),
'machines': fields.List(fields.String),
})
appFileDistributeSerializer = api.model('FileDistributeApp', {
'name': fields.String(required=True, description='app name'),
'machines': fields.List(fields.String),
})
......@@ -49,7 +49,7 @@ class Pxe(Resource):
s_content = ''
# check if it is installed.
if not d_machine['is_installed']:
set_is_installed(d_machine['name'])
# set_is_installed(d_machine['name'])
with open(s_src, 'rb') as o_src:
s_content = o_src.read()
......
import os
import ast
import uuid
import time
import etcd
import stat
import shutil
import signal
import tarfile
import logging
import logging.config
from subprocess import Popen
from concurrent.futures import ProcessPoolExecutor
from datetime import datetime
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
......@@ -15,15 +19,22 @@ from cryptography.hazmat.backends import default_backend
from flask import render_template
from porch.database import etcdc
from porch.utils import get_port
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.config import APP_ROOT
from porch.config import PORCH_URL
from porch.config import PORCH_USER
from porch.config import IPMITOOL, IPMIPOWER, IPMICONSOLE, IPMIUSER, IPMIPASS
from porch.config import OS_LOG
from porch.config import FAI_MONITOR
from porch.utils import cmd
......@@ -259,4 +270,163 @@ def delete_machine(name):
s_rsc = '{}/macaddr/{}'.format(etcdc.prefix, s_mac)
etcdc.delete(s_rsc)
# delete dhcp entry
s_cmd = "{} -r {}".format(DHCPEDIT, d['mac'])
log.debug(s_cmd)
cmd(s_cmd, b_sudo=True)
return (True, 'machine {} is deleted.'.format(name))
def install_machine(data):
"""Install OS on selected machines.
data: {'machines': [], 'os': ''}
"""
# ipmi bios pxe boot setting
# ipmi chassis power reset
l_ipmi = list()
s_tmp = '/tmp/pass'
with open(s_tmp, 'w') as f:
f.write(IPMIPASS)
# get monitor port
i_port = get_port()
for machine in data['machines']:
# Get IPMI IP of machine
l_machine = get_machine(machine)
d = l_machine[0]
# Append FAI_MONITOR_PORT=i_port at the end of pxelinux.
s_mac = '01-' + d['mac'].replace(':', '-')
s_pxelinuxcfg = "{}/{}".format(PXELINUXCFG, s_mac)
s_monitor = " FAI_MONITOR_PORT={}".format(i_port)
try:
with open(s_pxelinuxcfg, 'a') as f:
f.write(s_monitor)
except:
log.debug('Fail to append pxelinuxcfg file: {}'\
.format(s_pxelinuxcfg))
continue
# skip if there is no ipmi_ip.
if 'ipmi_ip' in d:
# set pxeboot
s_cmd = "{} -I lanplus -H {} -U {} -f {} chassis bootdev pxe"\
.format(IPMITOOL, d['ipmi_ip'], IPMIUSER, s_tmp)
log.debug(s_cmd)
Popen(s_cmd, shell=True)
# update machine state to 'Installing'
d['state'] = 'Installing'
(b_ret, s_msg) = update_machine(d['name'], d)
if not b_ret:
continue
l_ipmi.append(d['ipmi_ip'])
# delete s_tmp
os.unlink(s_tmp)
#if not len(l_ipmi):
# return (False, 'No IPMI IP is found.')
# set logfile name
s_date = datetime.now().isoformat()
s_logfile = OS_LOG + '/' + s_date
# write to etcd /porch/log/os/<s_date>/{log: s_logfile}
s_rsc = '{}/log/os/{}'.format(etcdc.prefix, s_date)
try:
etcdc.write(s_rsc, {'log': s_logfile})
except:
s_msg = 'Cannot create log record.'
log.error(s_msg)
return (False, s_msg)
# fai-monitor log
pool = ProcessPoolExecutor(2)
future = pool.submit(_bgmonitorprocess, i_port, s_logfile)
# power on the physical machines if any.
if len(l_ipmi):
s_hosts = ",".join(l_ipmi)
# /usr/sbin/ipmipower -h 192.168.24.18 -u root -p xxxx --cycle
s_cmd = "{} -h {} -u {} -p '{}' --cycle"\
.format(IPMIPOWER, s_hosts, IPMIUSER, IPMIPASS)
(b_ret, l_out) = cmd(s_cmd)
if not b_ret:
log.error('IPMI POWER is failed.')
return (True, s_date)
def _bgmonitorprocess(i_port, s_logfile):
s_cmd = "sudo {} -p {} -l {} -T".format(FAI_MONITOR, i_port, s_logfile)
log.debug(s_cmd)
with open(s_logfile, 'w+') as f:
proc = Popen(s_cmd, shell=True, stdout=f, stderr=f)
try:
proc.communicate(timeout=30*60)
except TimeoutExpired:
proc.kill()
def show_console_machine(name):
"""Show machine ipmi console."""
i_port = get_port()
# kill previous console if exists.
close_console_machine(name)
# Get ipmi_ip of machine
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)
ipmi_ip = d['ipmi_ip']
#s_tmp = '/tmp/pass'
#with open(s_tmp, 'w') as f:
# f.write(IPMIPASS)
# deactivate first.
#s_cmd = '{} -I lanplus -H {} -U {} -f {} sol deactivate'\
# .format(IPMITOOL, ipmi_ip, IPMIUSER, s_tmp)
s_cmd = '{} -h {} -u {} -p "{}" --deactivate'\
.format(IPMICONSOLE, ipmi_ip, IPMIUSER, IPMIPASS)
log.debug(s_cmd)
cmd(s_cmd)
s_pidfile = '/tmp/{}.pid'.format(name)
# run terminal to connect to a machine
#ipmi_cmd = '{} -I lanplus -H {} -U {} -f {} sol activate'\
# .format(IPMITOOL, ipmi_ip, IPMIUSER, s_tmp)
ipmi_cmd = '{} -h {} -u {} -p "{}"'\
.format(IPMICONSOLE, ipmi_ip, IPMIUSER, IPMIPASS)
s_cmd = "shellinaboxd --background={} ".format(s_pidfile) + \
"--service='/:{0}:{0}:/:{1}' ".format(PORCH_USER, ipmi_cmd) + \
"-p {} --disable-ssl -q ".format(i_port) + \
"--css {}/static/white-on-black.css".format(APP_ROOT)
log.debug(s_cmd)
Popen(s_cmd, shell=True)
# delete s_tmp after 3 seconds.
#os.unlink(s_tmp)
return (True, i_port)
def close_console_machine(name):
"""close console."""
# check if terminal is running.
s_pidfile = '/tmp/{}.pid'.format(name)
if os.path.isfile(s_pidfile):
with open(s_pidfile, 'r') as f:
i_pid = f.read()
try:
os.kill(int(i_pid), signal.SIGTERM)
except:
log.info('pid {} not found. skip it'.format(i_pid))
else:
log.info('pid {} is killed.'.format(i_pid))
return True
......@@ -12,6 +12,8 @@ from flask_jwt_extended import get_jwt_identity
from porch.api.restplus import api
from porch.decorators import authz_required
from porch.config import MY_IP
from porch.api.machine.serializers import machineSerializer
from porch.api.machine.serializers import machinePostSerializer
from porch.api.machine.serializers import machinePatchSerializer
......@@ -21,6 +23,9 @@ 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
from porch.api.machine.bizlogic import install_machine
from porch.api.machine.bizlogic import show_console_machine
from porch.api.machine.bizlogic import close_console_machine
log = logging.getLogger('porch')
......@@ -112,3 +117,71 @@ class MachineProvision(Resource):
return d_msg, i_ret_code
@ns.route('/os')
class MachineOS(Resource):
@ns.response(200, 'Machine OS successfully installed.')
@jwt_required
def post(self):
"""Install OS into selected machines.
"""
i_ret_code = 200
data = request.json
log.debug(data)
(b_ret, s_msg) = install_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>/console')
@api.response(404, 'App not found.')
class MachineConsole(Resource):
@jwt_required
def get(self, name):
"""Returns machine ipmi console url.
name: string, machine name
"""
(b_ret, i_port) = show_console_machine(name)
if not b_ret:
d_msg = {'error': 'Fail to run console.'}
return d_msg, 404
s_url = 'http://' + MY_IP + ':{}'.format(i_port)
return s_url
@ns.route('/<string:name>/console/close')
@api.response(404, 'Pod not found.')
class MachineConsoleClose(Resource):
@jwt_required
def post(self, name):
"""close pod console."""
close_console_machine(name)
return {}
@ns.route('/<string:name>/installed')
class MachineInstalled(Resource):
@ns.response(200, 'Machine successfully installed.')
def post(self, name):
"""Set installed flag for the machine.
"""
i_ret_code = 200
data = {
'state': 'Installed',
'is_installed': True
}
(b_ret, s_msg) = update_machine(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
......@@ -13,6 +13,7 @@ machineSerializer = api.model('ListMachine', {
default='', description='machine preseed'),
'mac': fields.String(required=True, description='machine mac'),
'ip': fields.String(required=True, description='machine ip address'),
'ipmi_ip': fields.String(required=False, description='machine ipmi ip'),
'state': fields.String(required=True, default='Created',
description='state: Created, Provisioned'),
'is_installed': fields.Boolean(required=True, default=False,
......@@ -30,6 +31,7 @@ machinePostSerializer = api.model('RegisterMachine', {
description='machine preseed'),
'mac': fields.String(required=True, description='machine mac'),
'ip': fields.String(required=True, description='machine ip address'),
'ipmi_ip': fields.String(required=False, description='machine ipmi ip'),
})
machinePatchSerializer = api.model('ModifyMachine', {
......@@ -38,6 +40,7 @@ machinePatchSerializer = api.model('ModifyMachine', {
'section': fields.String(required=False,
description='the section machine belongs to'),
'os': fields.String(required=False, description='machine os'),
'ipmi_ip': fields.String(required=False, description='machine ipmi ip'),
'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'),
......
......@@ -19,6 +19,7 @@ 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
from porch.api.log.endpoints.route import ns as log_ns
app = Flask(__name__)
app.secret_key = JWT_SECRET_KEY
......@@ -62,6 +63,7 @@ def initialize_app(flask_app):
api.add_namespace(fai_ns)
api.add_namespace(account_ns)
api.add_namespace(app_ns)
api.add_namespace(log_ns)
flask_app.register_blueprint(blueprint)
......
......@@ -2,38 +2,75 @@ import os
from datetime import timedelta
## URL
PORCH_URL = 'http://192.168.24.50:8000/api/fai' # Put PORCH's pxe network ip address.
# ADMIN_* is used when there is no admin entry in etcd.
# ADMIN_SALT = bcrypt.gensalt()
# ADMIN_PW = bcrypt.hashpw(b"plaintext_password", ADMIN_SALT)
ADMIN_ID = 'admin'
ADMIN_SALT = '$2b$12$kffJppSYRy9IHlrxFNNWEu'
ADMIN_PW = '$2b$12$kffJppSYRy9IHlrxFNNWEu.JsCmlAVuwIgamsHHloP8Q3UZQSbEZ6'
# 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.
# ipmi
IPMIUSER = 'root'
IPMIPASS = '1Tlzbdj@%*)'
# Terminal
PORCH_USER = 'jijisa'
PORCHSHELL = 'kubicshell'
TERM_PORT_BEGIN = 10000
TERM_PORT_END = 11000
MY_IP = '121.254.203.198'
#
## Do not edit below!!!
#
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_MONITOR = "/usr/sbin/fai-monitor"
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'
# ipmi
IPMITOOL = '/usr/bin/ipmitool'
IPMIPOWER = '/usr/sbin/ipmipower'
IPMICONSOLE = '/usr/sbin/ipmiconsole'
# Log
OS_LOG = APP_ROOT + '/log/os'
APP_LOG = APP_ROOT + '/log/app'
FILE_LOG = APP_ROOT + '/log/file'
......@@ -15,8 +15,6 @@ def authz_required(role='admin'):
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.'}
......
......@@ -2,4 +2,4 @@ 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
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 {% if data['type'] == 'PM' %} console=ttyS0,115200n8 {% endif %} net.ifnames=0
......@@ -3,6 +3,8 @@ import logging.config
import subprocess
import shlex
import time
import random
import socket
from subprocess import Popen
from subprocess import PIPE
......@@ -11,6 +13,8 @@ from subprocess import TimeoutExpired
from porch.database import etcdc
from porch.config import TERM_PORT_BEGIN
from porch.config import TERM_PORT_END
logging.config.fileConfig('logging.conf')
log = logging.getLogger('porch')
......@@ -43,15 +47,23 @@ def cmd(s_cmd, i_timeout=30, b_sudo=False):
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:
def get_port():
"""Get terminal port"""
i_port = 0
b_gotit = False
s_address = 'localhost'
s = socket.socket()
for i in range(1, 10):
i_port = random.randrange(TERM_PORT_BEGIN, TERM_PORT_END)
try:
s.connect((s_address, i_port))
except Exception as e:
# port is availabe so use it.
b_gotit = True
break
if output:
s_created = datetime.utcnow().isoformat() + 'Z'
data[s_created] = output.strip()
rc = process.poll()
return rc
finally:
s.close()
i_port = i_port if b_gotit else 0
return i_port
export const API_URL = 'http://121.254.203.198:8000/api'
export const POLLING_INTERVAL = 10 * 1000
export const REFRESH_INTERVAL = 25 * 60 * 1000
/*
export const selectOS = [
{label: 'CENTOS7', value: 'CENTOS7'},
{label: 'UXENOS', value: 'UXENOS'},
{label: 'JESSIE', value: 'JESSIE'}
]
*/
export const selectOS = [
{label: 'CENTOS7', value: 'CENTOS7'},
{label: 'JESSIE', value: 'JESSIE'}
]
export function checkStrength (pw) {
// initialize
var iStrength = 0
......
......@@ -7,6 +7,15 @@
<q-tabs slot="navigation" class="purple">
<q-tab
v-if="this.$store.state.login"
icon="toc"
route="/log"
exact
replace
@click="trigger()"
>로그
</q-tab>
<q-tab
v-if="this.$store.state.login"
icon="cast"
route="/os"
exact
......@@ -61,6 +70,15 @@
</q-tab>
<q-tab
v-if="this.$store.state.login"
icon="vpn_key"
v-bind:route="computedPassUrl"
exact
replace
@click="trigger()"
>비밀번호
</q-tab>
<q-tab
v-if="this.$store.state.login"
icon="exit_to_app"
route="/logout"
exact
......@@ -99,24 +117,32 @@
</template>
<script>
import { API_URL, REFRESH_INTERVAL } from 'config'
export default {
name: 'app',
data () {
return {
porchVersion: '0.0.1',
intervalRefresh: [],
position: 'bottom',
reverse: false,
size: 12,
size: 8,
color: '#e21b0c'
}
},
computed: {
computedSize () {
return this.size + 'px'
},
computedPassUrl () {
return '/account/' + this.$store.state.name + '/userpass'
}
},
methods: {
updateMenu: function () {
/*
console.log(this.$cookie.get('token'))
if (this.$cookie.get('token')) {
this.$store.commit('loggedIn', true)
this.$store.commit('setName', this.$cookie.get('name'))
......@@ -124,6 +150,7 @@ export default {
this.$store.commit('setToken', this.$cookie.get('token'))
this.$store.commit('setRefreshToken', this.$cookie.get('refreshToken'))
}
*/
},
trigger () {
this.$refs.bar.start()
......@@ -133,6 +160,26 @@ export default {
this.$refs.bar.stop()
}
}, 5000)
},
refresh: function () {
// check if logged in session.
if (!this.$store.state.login) {
return
}
const refreshToken = this.$store.state.refreshToken
const headers = {
headers: {
'Authorization': `Bearer ${refreshToken}`
}
}
const url = API_URL + '/account/refresh'
console.log(url)
this.$http.post(url, {}, headers).then(response => {
console.log(response.body.token)
// this.$cookie.set('token', response.body.token)
this.$store.commit('setToken', response.body.token)
}, response => {
})
}
},
updated () {
......@@ -140,6 +187,10 @@ export default {
},
mounted () {
this.updateMenu()
this.intervalRefresh = setInterval(this.refresh, REFRESH_INTERVAL)
},
destroyed: function () {
clearInterval(this.intervalRefresh)
}
}
</script>
......
......@@ -11,7 +11,7 @@ export default {
methods: {
redirect: function () {
if (this.$store.state.login) {
this.$router.push('/section')
this.$router.push('/machine')
}
else {
this.$router.push('/login')
......
......@@ -73,10 +73,12 @@ export default {
}
this.$http.post(url, dReqBody, headers).then(response => {
// save token cookie
/*
this.$cookie.set('token', response.body.token)
this.$cookie.set('refreshToken', response.body.refresh_token)
this.$cookie.set('role', response.body.role)
this.$cookie.set('name', this.name)
*/
// state update
this.$store.commit('loggedIn', true)
this.$store.commit('setName', this.name)
......@@ -98,6 +100,9 @@ export default {
}
},
mounted () {
if (this.$store.state.login) {
this.$router.push('/')
}
this.$refs.name.focus()
}
}
......
<template>
<div>로그아웃 되었습니다.</div>
<div class="logout-page bg-light column items-center">
<div class="logout-logo bg-purple flex items-center justify-center">
UDAM
</div>
<div>
<div class="logout-card card bg-white column items-center justify-center">
로그아웃 되었습니다.
</div>
</div>
<p></p>
<p></p>
<p></p>
</div>
</template>
<script>
export default {
......@@ -11,16 +23,18 @@ export default {
methods: {
logout: function () {
// delete token cookie
/*
this.$cookie.delete('token')
this.$cookie.delete('refreshToken')
this.$cookie.delete('role')
this.$cookie.delete('name')
*/
this.$store.commit('loggedIn', false)
this.$store.commit('setName', '')
this.$store.commit('setRole', '')
this.$store.commit('setToken', '')
this.$store.commit('setRefreshToken', '')
this.$router.push('/')
// this.$router.push('/')
}
},
mounted () {
......@@ -28,6 +42,19 @@ export default {
}
}
</script>
<style>
<style lang="stylus">
.logout-page
.logout-logo
height 12vh
width 60%
padding-top 1vh
font-size 5vmax
color rgba(255, 255, 255, .7)
overflow hidden
.logout-card
margin-top 40px
width 50vw
height 20vh
max-width 600px
padding 20px
</style>
......@@ -29,7 +29,7 @@
class="purple small circular"
@click="goToPassword(cell.row.name)"
>
<i>lock</i>
<i>vpn_key</i>
<q-tooltip>사용자 비밀번호 변경: {{ cell.row.name }}</q-tooltip>
</button>
<button
......@@ -93,7 +93,7 @@
</template>
<script>
import { API_URL } from 'config'
import { API_URL, POLLING_INTERVAL } from 'config'
import { Dialog, Toast } from 'quasar'
export default {
......@@ -248,20 +248,10 @@ export default {
this.$http.get(url, headers).then(response => {
this.data = response.body
}, response => {
Toast.create.negative('계정 정보 수신 실패')
})
},
refresh: function () {
const refreshToken = this.$store.state.refreshToken
const headers = {
headers: {
'Authorization': `Bearer ${refreshToken}`
}
if (response.status === 401) {
this.$router.push('/logout')
}
const url = API_URL + '/account/refresh'
this.$http.post(url, {}, headers).then(response => {
this.$store.commit('setToken', response.body.token)
}, response => {
Toast.create.negative('계정 정보 수신 실패')
})
}
},
......@@ -269,13 +259,10 @@ export default {
if (!this.$store.state.login) { this.$router.push('/') }
this.loadData()
this.intervalObj = setInterval(this.loadData, 10 * 1000)
this.refresh()
this.intervalRefresh = setInterval(this.refresh, 5 * 60 * 1000)
this.intervalObj = setInterval(this.loadData, POLLING_INTERVAL)
},
destroyed: function () {
clearInterval(this.intervalObj)
clearInterval(this.intervalRefresh)
}
}
</script>
......
......@@ -116,30 +116,13 @@ export default {
this.$http.patch(url, payload, headers).then(response => {
this.$router.push('/account')
}, response => {
Toast.create.negative('사용자 수정 실패: ' + response.body['error'])
Toast.create.negative('비밀번호 변경 실패: ' + response.body['error'])
})
},
loadData: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
let url = API_URL + '/account' + '/' + this.$route.params.name
this.$http.get(url, headers).then(response => {
let rep = response.body
this.form.cn = rep.cn ? rep.cn : ''
this.form.state = rep.state ? rep.state : ''
this.form.expireAt = rep.expireAt ? rep.expireAt : ''
this.form.desc = rep.desc ? rep.desc : ''
}, response => {
Toast.create.negative('사용자 정보 수신 실패')
})
}
},
mounted: function () {
this.loadData()
}
}
</script>
......
<template>
<div class="column">
<h6>
사용자 비밀번호 변경
</h6>
<p></p>
<form>
<div class="stacked-label">
<label>아이디</label>
<input
type="text"
v-model="form.name"
ref="name"
readonly
>
</div>
<p></p>
<div class="stacked-label">
<label>현 비밀번호</label>
<input
type="password"
size="50"
v-model="form.pass"
ref="pass"
placeholder="현 비밀번호를 입력하세요."
:class="{'has-error': $v.form.pass.$error}"
>
</div>
<span
class="text-negative"
v-if="!$v.form.pass.required"
> 현 비밀번호는 필수 입력 항목입니다.
</span>
<p></p>
<div class="stacked-label">
<label>새 비밀번호 (알파벳, 숫자, 특수문자 조합. 8자 이상)</label>
<input
type="password"
size="50"
v-model="form.pass1"
ref="pass1"
placeholder="새 비밀번호를 입력하세요."
:class="{'has-error': $v.form.pass1.$error}"
>
</div>
<span
class="text-negative"
v-if="!$v.form.pass1.required"
> 새 비밀번호는 필수 입력 항목입니다.
</span>
<span
class="text-negative"
v-if="!$v.form.pass1.minLength"
> 비밀번호는 8자 이상이어야 합니다.
</span>
<p></p>
<div class="stacked-label">
<label>새 비밀번호 재입력</label>
<input
type="password"
size="50"
v-model="form.pass2"
ref="pass2"
placeholder="새 비밀번호를 재입력하세요."
:class="{'has-error': $v.form.pass2.$error}"
>
</div>
<span
class="text-negative"
v-if="!$v.form.pass2.sameAsPass"
> 비밀번호가 일치하지 않습니다.
</span>
<p></p>
<div>
<p class="text-italic">
비밀번호 변경이 성공하면, 자동 로그아웃됩니다.
새 비밀번호로 다시 로그인 하세요.
</p>
</div>
<p></p>
<div>
<button class="purple" @click="submit">
비밀번호 변경
</button>
</div>
</form>
</div>
</template>
<script>
import { API_URL, checkStrength } from 'config'
import { Toast } from 'quasar'
import { required, minLength, sameAs } from 'vuelidate/lib/validators'
export default {
name: 'userCreate',
data () {
return {
form: {
name: this.$route.params.name,
pass: '',
pass1: '',
pass2: ''
}
}
},
validations: {
form: {
pass: {
required
},
pass1: {
required,
minLength: minLength(8)
},
pass2: {
required,
sameAsPass: sameAs('pass1')
}
}
},
methods: {
submit: function () {
if (this.$v.form.$invalid) {
Toast.create.negative('양식 제출 오류')
return
}
if (checkStrength(this.form.pass1) < 3) {
Toast.create.negative('비밀번호가 보안규약에 위배됩니다.')
return
}
const payload = {
name: this.form.name,
pass: this.form.pass,
pass1: this.form.pass1,
pass2: this.form.pass2
}
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
const url = API_URL + '/account' + '/' + this.form.name + '/userpass'
this.$http.patch(url, payload, headers).then(response => {
this.$router.push('/logout')
}, response => {
Toast.create.negative('비밀번호 변경 실패: ' + response.body['error'])
})
},
loadData: function () {
}
},
mounted: function () {
}
}
</script>
<style>
</style>
......@@ -13,9 +13,9 @@
type="text"
v-model="name"
ref="name"
placeholder="사용자 ID를 입력하세요."
placeholder="관리자 ID를 입력하세요."
>
<label>사용자 ID</label>
<label>관리자 ID</label>
</div>
<div class="stacked-label">
<input
......@@ -61,10 +61,12 @@ export default {
}
}
this.$http.post(url, dReqBody, headers).then(response => {
/*
this.$cookie.set('token', response.body.token)
this.$cookie.set('refreshToken', response.body.refresh_token)
this.$cookie.set('role', response.body.role)
this.$cookie.set('name', this.name)
*/
this.$store.commit('loggedIn', true)
this.$store.commit('setName', this.name)
......@@ -85,6 +87,9 @@ export default {
}
},
mounted () {
if (this.$store.state.login) {
this.$router.push('/')
}
this.$refs.name.focus()
}
}
......
<template>
<div>
<div>
<q-tabs
:refs="$refs"
v-model="qtabs"
class="toolbar justified"
>
<q-tab name="console-tab" :disable=true>
1. 파일 관리 콘솔
</q-tab>
<q-tab name="machine-tab" :disable=true>
2. 서버 선택
</q-tab>
<q-tab name="install-tab" :disable=true>
3. 파일 배포
</q-tab>
</q-tabs>
</div>
<br>
<div ref="console-tab">
<div class="card">
<div class="card-title">
파일 관리 콘솔
</div>
<div class="card-content">
<button class="purple"
@click="invokeConsole()"
>
<i>folder</i> 콘솔 열기
</button>
</div>
</div>
<button
class="primary"
@click="moveTab('machine-tab')"
>
다음
</button>
</div>
<div ref="machine-tab">
<div class="card">
<div class="card-title">
선택 애플리케이션: {{ this.form.name }}
</div>
<div class="card-content">
<div class="card" v-for="(machines, section) in this.sections">
<div class="card-title">
그룹 {{ section }}
</div>
<div class="card-content">
<span v-for="machine in machines">
<input
ref="machineCheckbox"
type="checkbox"
v-model="form.machines"
v-bind:value="machine.ip"
>
{{ machine.name }} ({{ machine.ip }})
</span>
</div>
</div>
</div>
</div>
<button
class="primary"
@click="moveTab('console-tab')"
>
이전
</button>
<button
class="primary"
@click="moveTab('install-tab')"
>
다음
</button>
</div>
<div ref="install-tab">
<div class="card">
<div class="card-title">
파일 배포 요약
</div>
<div class="card-content">
<div>선택 애플리케이션: {{ this.form.name }}</div>
<div>선택 서버:
<span v-for="machine in this.form.machines">
{{ machine }}
</span>
</div>
</div>
</div>
<button
class="primary"
@click="moveTab('machine-tab')"
>
이전
</button>
<button
class="primary"
@click="submit()"
>
배포
</button>
</div>
<q-modal
ref="consoleModal" class="maximized"
@escape-key="notifyConsoleClose()"
@close="notifyConsoleClose()"
>
<q-layout>
<div slot="header" class="toolbar purple">
<button @click="$refs.consoleModal.close()">
<i>keyboard_arrow_left</i>
</button>
<q-toolbar-title>
애플리케이션 파일관리 콘솔: {{ this.form.name }}
</q-toolbar-title>
</div>
<div class="layout-view">
<div class="layout-padding">
<iframe v-bind:src="consoleUrl"
width="100%" height="500" frameborder="0"></iframe>
</div>
</div>
</q-layout>
</q-modal>
</div>
</template>
<script>
import { API_URL } from 'config'
import { Toast } from 'quasar'
export default {
name: 'AppFileList',
data: function () {
return {
qtabs: 'console-tab',
consoleUrl: '',
sections: {'b': []},
machines: [],
allSelected: false,
form: {
name: this.$route.params.name,
machines: []
}
}
},
methods: {
notifyConsoleClose: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
let url = API_URL + `/app/${this.form.name}/file_container/close`
this.$http.post(url, {}, headers).then(response => {
}, response => {
})
},
invokeConsole: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
let url = API_URL + `/app/${this.form.name}/file_container`
this.$http.post(url, {}, headers).then(response => {
this.consoleUrl = response.body
setTimeout(() => {
this.$refs.consoleModal.open()
}, 500)
}, response => {
let err = `파일 관리 콘솔 생성 실패`
Toast.create.negative({
html: err,
timeout: 3000
})
})
},
submit: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
const url = API_URL + '/app/' + this.form.name + '/file_distribute'
this.$http.post(url, this.form, headers).then(response => {
this.$router.push('/log/file/' + response.body)
}, response => {
Toast.create.negative('서버 파일 배포 실패')
return
})
},
toggle: function () {
let selected = []
if (this.allSelected) {
this.machines.forEach(function (machine) {
selected.push(machine)
})
}
this.form.machines = selected
},
moveTab: function (tab) {
if (tab === 'install-tab') {
if (!this.form.machines.length) {
Toast.create.negative('서버는 필수 항목입니다.')
return
}
}
this.qtabs = tab
},
getMachineListBySection: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
this.sections = {}
const url = API_URL + '/machine'
this.$http.get(url, headers).then(response => {
let self = this
let result = response.body
result.forEach(function (element) {
self.sections[element.section] = []
})
result.forEach(function (element) {
self.sections[element.section].push({
name: element.name,
ip: element.ip
})
})
}, response => {
Toast.create.negative('서버 정보 수신 실패')
})
}
},
mounted: function () {
if (!this.$store.state.login) { this.$router.push('/') }
this.getMachineListBySection()
},
destroyed: function () {
}
}
</script>
<style lang="stylus">
view-flexbox
margin-top 30px
max-width 200vw
</style>
......@@ -14,10 +14,6 @@
<i>edit</i>
<q-tooltip>애플리케이션 수정: {{ cell.row.name }}</q-tooltip>
</button>
<button class="purple small circular" @click="play(cell.row)">
<i>play_arrow</i>
<q-tooltip>애플리케이션 배포: {{ cell.row.name }}</q-tooltip>
</button>
<button
class="purple small circular"
@click="confirmDelete(cell.row.name)"
......@@ -26,68 +22,27 @@
<i>delete</i>
<q-tooltip>애플리케이션 삭제: {{ cell.row.name }}</q-tooltip>
</button>
</template>
</q-data-table>
<q-modal
ref="playModal" :content-css="{minWidth: '80vw', minHeight: '80vh'}"
<button
class="purple small circular"
@click="goToFile(cell.row.name)"
>
<q-layout>
<div slot="header" class="toolbar purple">
<button @click="$refs.playModal.close()">
<i>keyboard_arrow_left</i>
<i>folder</i>
<q-tooltip>애플리케이션 파일 배포: {{ cell.row.name }}</q-tooltip>
</button>
<q-toolbar-title :padding="1">
애플리케이션 배포: {{ form.name }}
</q-toolbar-title>
</div>
<div class="layout-view">
<div class="layout-padding">
<div class="stacked-label">
<label>Tag 선택</label>
<p></p>
<q-select
type="list"
v-model="form.tag"
:options="tagOptions"
>
</div>
<div><label>서버 목록</label></div>
<div class="card" v-for="(machines, section) in this.sections">
<div class="card-title bg-primary text-white">
<a @click="toggle(section)"><i>check_box</i></a>
{{ section }}
</div>
<div class="card-content card-force-top-padding">
<span v-for="machine in machines">
<input type="checkbox" v-model="form.machines" v-bind:value="machine">
{{ machine }}
</span>
</div>
</div>
<div>
<p><label>배포 로그</label></p>
<textarea
v-model="playlog"
rows="12"
class="full-width"
readonly
<button
class="purple small circular"
@click="processTag(cell.row.name)"
>
</textarea>
</div>
<div>
<button class="purple small" @click="playApp()">
배포
<i>bookmark</i>
<q-tooltip>애플리케이션 태그 자동 생성: {{ cell.row.name }}</q-tooltip>
</button>
</div>
</div>
</div>
</q-layout>
</q-modal>
</template>
</q-data-table>
</div>
</template>
<script>
import { API_URL } from 'config'
import { API_URL, POLLING_INTERVAL } from 'config'
import { Dialog, Toast } from 'quasar'
export default {
......@@ -164,87 +119,59 @@ export default {
goToCreate: function () {
this.$router.push('/app/create')
},
toggle: function (section) {
for (let i in this.sections) {
console.log(i)
}
goToFile: function (name) {
this.$router.push('/app/' + name + '/file')
},
play: function (row) {
this.getMachineListBySection()
this.form.name = row.name
// Get tag names
this.tagOptions = []
for (let i in row.tag) {
this.tagOptions.push({
label: row.tag[i],
value: row.tag[i]
})
processTag: function (name) {
let self = this
Dialog.create({
title: `애플리케이션: ${name}`,
message: '태그 생성',
form: {
tag: {
type: 'textbox',
label: '태그'
}
this.$refs.playModal.open()
},
playApp: function () {
console.log(this.form.name)
console.log(this.form.tag)
console.log(this.form.machines)
const payload = {
name: this.form.name,
tag: this.form.tag,
machines: this.form.machines
buttons: [
'취소',
{
label: '확인',
preventClose: true,
handler (data, close) {
// Validate data
if (!data.tag) {
Toast.create.negative('태그는 필수 항목입니다.')
return
}
console.log(payload)
const token = this.$store.state.token
const url = API_URL + `/app/${name}/tag`
const token = self.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
let url = API_URL + `/app/${this.form.name}/play`
this.intervalLogObj = setInterval(this.getLog, 3000)
this.$http.post(url, payload, headers).then(response => {
Toast.create.positive(`애플리케이션 ${this.form.name} 배포 성공`)
this.$refs.playModal.close()
}, response => {
Toast.create.negative(`애플리케이션 ${this.form.name} 배포 실패`)
self.$http.patch(url, data, headers).then(response => {
// close dialog
close(() => {
Toast.create.positive({
html: `애플리케이션 ${name} 태그 생성 완료`,
timeout: 5000
})
},
getLog: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
const url = API_URL + `/app/${this.form.name}/playlog`
console.log(url)
this.$http.get(url, headers).then(response => {
this.playlog = response.body
console.log(this.playlog)
})
self.loadData()
}, response => {
let err = `애플리케이션 ${name} 태그 실패`
Toast.create.negative({
html: err,
timeout: 3000
})
})
clearInterval(this.intervalLogObj)
},
getMachineListBySection: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
this.sections = {}
const url = API_URL + '/machine'
this.$http.get(url, headers).then(response => {
let self = this
let result = response.body
result.forEach(function (element) {
self.sections[element.section] = []
})
result.forEach(function (element) {
self.sections[element.section].push(element.name)
})
}, response => {
Toast.create.negative('서버 정보 수신 실패')
]
})
console.log(this.sections)
},
processEdit: function (row) {
let self = this
......@@ -357,32 +284,16 @@ export default {
}, response => {
Toast.create.negative('애플리케이션 정보 수신 실패')
})
},
refresh: function () {
const refreshToken = this.$store.state.refreshToken
const headers = {
headers: {
'Authorization': `Bearer ${refreshToken}`
}
}
const url = API_URL + '/account/refresh'
this.$http.post(url, {}, headers).then(response => {
this.$store.commit('setToken', response.body.token)
}, response => {
})
}
},
mounted: function () {
if (!this.$store.state.login) { this.$router.push('/') }
this.loadData()
this.intervalObj = setInterval(this.loadData, 10 * 1000)
this.refresh()
this.intervalRefresh = setInterval(this.refresh, 5 * 60 * 1000)
this.intervalObj = setInterval(this.loadData, POLLING_INTERVAL)
},
destroyed: function () {
clearInterval(this.intervalObj)
clearInterval(this.intervalRefresh)
}
}
</script>
......
<template>
<div>
<div>
애플리케이션 로그: {{ this.appName }}
</div>
<div class="layout-view">
<div class="layout-padding">
<textarea
ref="textarea"
rows="30"
class="full-width"
readonly
>
{{ this.appLog }}
</textarea>
</div>
</div>
</div>
</template>
<script>
import { API_URL } from 'config'
import { Toast } from 'quasar'
export default {
name: 'LogRead',
data: function () {
return {
intervalLogObj: null,
appName: this.$route.params.name,
appLog: '',
applogs: []
}
},
methods: {
getAppLogData: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
const url = API_URL + `/log/app/${this.appName}`
this.$http.get(url, headers).then(response => {
this.appLog = response.body
this.$refs.textarea.scrollTop = this.$refs.textarea.scrollHeight
}, response => {
Toast.create.negative('로그 정보 수신 실패')
})
}
},
mounted: function () {
if (!this.$store.state.login) { this.$router.push('/') }
this.getAppLogData()
this.intervalLogObj = setInterval(this.getAppLogData, 5 * 1000)
},
destroyed: function () {
clearInterval(this.intervalLogObj)
}
}
</script>
<style lang="stylus">
view-flexbox
margin-top 30px
max-width 200vw
</style>
<template>
<div>
<div>
파일 배포 로그: {{ this.fileName }}
</div>
<div class="layout-view">
<div class="layout-padding">
<textarea
ref="textarea"
rows="30"
class="full-width"
readonly
>{{ this.fileLog }}</textarea>
</div>
</div>
</div>
</template>
<script>
import { API_URL } from 'config'
import { Toast } from 'quasar'
export default {
name: 'FileLogRead',
data: function () {
return {
intervalLogObj: null,
fileName: this.$route.params.name,
fileLog: '',
filelogs: []
}
},
methods: {
getFileLogData: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
const url = API_URL + `/log/file/${this.fileName}`
this.$http.get(url, headers).then(response => {
this.fileLog = response.body
this.$refs.textarea.scrollTop = this.$refs.textarea.scrollHeight
}, response => {
Toast.create.negative('로그 정보 수신 실패')
})
}
},
mounted: function () {
if (!this.$store.state.login) { this.$router.push('/') }
this.getFileLogData()
this.intervalLogObj = setInterval(this.getFileLogData, 5 * 1000)
},
destroyed: function () {
clearInterval(this.intervalLogObj)
}
}
</script>
<style lang="stylus">
view-flexbox
margin-top 30px
max-width 200vw
</style>
......@@ -3,7 +3,7 @@
<h6>
서버 생성
</h6>
<p></p>
<br>
<form>
<div class="stacked-label">
<input
......@@ -12,26 +12,21 @@
ref="name"
class="full-width"
:class="{'has-error': $v.form.name.$error}"
placeholder="Type machine name"
placeholder="서버 이름을 입력하세요."
>
<label>이름</label>
<span
class="text-negative"
v-if="!$v.form.name.required">
Field is required
이름은 필수 입력 항목입니다.
</span>
<span
class="text-negative"
v-if="!$v.form.name.minLength">
Name must be longer than 3 letters.
</span>
<span
class="text-negative"
v-if="!$v.form.name.alphaNum">
Name must be alphanumeric or hyphen.
이름은 3문자 이상이어야 합니다.
</span>
</div>
<p></p>
<br><br>
<div class="stacked-label">
<label>종류</label><p></p>
<q-select
......@@ -41,7 +36,7 @@
:options="machineType">
</q-select>
</div>
<p></p>
<br><br>
<div class="stacked-label">
<label>OS</label><p></p>
<q-select
......@@ -51,7 +46,18 @@
:options="selectOS">
</q-select>
</div>
<p></p>
<br><br>
<div class="stacked-label">
<label>IPMI IP주소</label>
<input
type="text"
v-model="form.ipmi_ip"
ref="ip"
class="full-width"
placeholder="서버 IPMI IP 주소를 입력하세요."
>
</div>
<br><br>
<div class="stacked-label">
<label>MAC주소</label><p></p>
<input
......@@ -60,15 +66,15 @@
ref="mac"
class="full-width"
:class="{'has-error': $v.form.mac.$error}"
placeholder="Type machine Mac Address"
placeholder="서버 MAC 주소를 입력하세요."
>
<span
class="text-negative"
v-if="!$v.form.mac.required">
Field is required
MAC 주소는 필수 입력 항목입니다.
</span>
</div>
<p></p>
<br><br>
<div class="stacked-label">
<label>IP주소</label><p></p>
<input
......@@ -77,15 +83,15 @@
ref="ip"
class="full-width"
:class="{'has-error': $v.form.ip.$error}"
placeholder="Type machine IP Address"
placeholder="서버 IP주소를 입력하세요."
>
<span
class="text-negative"
v-if="!$v.form.ip.required">
Field is required
IP 주소는 필수 입력 항목입니다.
</span>
</div>
<p></p>
<br><br>
<div class="stacked-label">
<label>설명</label><p></p>
<input
......@@ -93,10 +99,10 @@
v-model="form.desc"
ref="desc"
class="full-width"
placeholder="Type machine description"
placeholder="서버 설명을 입력하세요."
>
</div>
<p></p>
<br><br>
<div>
<button class="purple" @click="submit">
생성
......@@ -109,7 +115,7 @@
<script>
import { API_URL, selectOS } from 'config'
import { Toast } from 'quasar'
import { required, minLength, alphaNum } from 'vuelidate/lib/validators'
import { required, minLength } from 'vuelidate/lib/validators'
export default {
name: 'machineCreate',
......@@ -119,6 +125,7 @@ export default {
name: '',
type: '',
os: '',
ipmi_ip: '',
mac: '',
ip: '',
desc: ''
......@@ -134,7 +141,6 @@ export default {
form: {
name: {
required,
alphaNum,
minLength: minLength(3)
},
mac: {
......@@ -147,11 +153,11 @@ export default {
},
methods: {
submit: function () {
const url = API_URL + '/machine'
const payload = {
name: this.form.name,
type: this.form.type,
os: this.form.os,
ipmi_ip: this.form.ipmi_ip,
mac: this.form.mac,
ip: this.form.ip,
desc: this.form.desc,
......@@ -162,6 +168,7 @@ export default {
return
}
const token = this.$store.state.token
const url = API_URL + '/machine'
const headers = {
headers: {
'Authorization': `Bearer ${token}`
......
<template>
<div>
<q-data-table
:data="machines"
:config="config"
:columns="columns"
>
<template slot="col-state" scope="cell">
<span
v-if="cell.data === 'Provisioned'"
class="label text-white bg-primary"
>{{ cell.data }}
</span>
<span
v-else
class="label text-white bg-info"
>{{ cell.data }}
</span>
</template>
<template slot="col-is_installed" scope="cell">
<span
v-if="cell.data === true"
class="circular label text-white bg-primary"
>{{ cell.data }}
</span>
<span
v-else
class="circular label text-white bg-red"
>{{ cell.data }}
</span>
</template>
<template slot="col-functions" scope="cell">
<button
class="purple small circular"
v-if="cell.row.ipmi_ip"
@click="showConsole(cell.row)"
>
<i>computer</i>
<q-tooltip>서버 콘솔: {{ cell.row.name }}</q-tooltip>
</button>
</template>
</q-data-table>
<q-modal
ref="consoleModal" class="maximized"
@escape-key="notifyConsoleClose()"
@close="notifyConsoleClose()"
>
<q-layout>
<div slot="header" class="toolbar purple">
<button @click="$refs.consoleModal.close()">
<i>keyboard_arrow_left</i>
</button>
<q-toolbar-title>
콘솔: {{ this.machine }}
</q-toolbar-title>
</div>
<div class="layout-view">
<div class="layout-padding">
<iframe v-bind:src="consoleUrl" scrolling="no"
width="100%" height="500" frameborder="0"></iframe>
</div>
</div>
</q-layout>
</q-modal>
</div>
</template>
<script>
import { API_URL, POLLING_INTERVAL } from 'config'
import { Toast } from 'quasar'
export default {
name: 'machineList',
data: function () {
return {
intervalObj: [],
machine: '',
consoleUrl: '',
machines: [],
config: {
rowHeight: '10px',
title: '설치 중인 서버 목록',
refresh: false,
columnPicker: true,
pagination: {
rowsPerPage: 10,
options: [5, 10, 15, 30, 50, 100]
},
messages: {
noData: '<i>warning</i> 데이터가 없습니다.',
noDataAfterFiltering: '<i>warning</i> 검색 결과가 없습니다.'
}
},
columns: [
{
label: '이름',
field: 'name',
width: '70px',
filter: true,
sort: true
},
{
label: '그룹',
field: 'section',
width: '80px',
filter: true,
sort: true
},
{
label: '운영체제',
field: 'os',
width: '100px',
filter: true,
sort: true
},
{
label: 'IP 주소',
field: 'ip',
width: '100px',
filter: true,
sort: true
},
{
label: 'OS 설정',
field: 'confdir',
width: '50px',
sort: true
},
{
label: '디스크 설정',
field: 'partition',
width: '50px',
sort: true
},
{
label: '기본 설치',
field: 'extrabase',
width: '50px',
sort: true
},
{
label: '패키지 설치',
field: 'instsoft',
width: '50px',
sort: true
},
{
label: '커스텀 설정',
field: 'configure',
width: '50px',
sort: true
},
{
label: '완료',
field: 'reboot',
width: '50px',
sort: true
},
{ label: '기능', field: 'functions', width: '150px', sort: false }
]
}
},
methods: {
notifyConsoleClose: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
let url = API_URL + `/machine/${this.machine}/console/close`
console.log(url)
this.$http.post(url, {}, headers).then(response => {
}, response => {
})
},
showConsole: function (row) {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
let url = API_URL + `/machine/${row.name}/console`
console.log(url)
this.$http.get(url, headers).then(response => {
this.machine = row.name
this.consoleUrl = response.body
setTimeout(() => {
this.$refs.consoleModal.open()
}, 2000)
}, response => {
let err = `콘솔 생성 실패`
Toast.create.negative({
html: err,
timeout: 5000
})
})
},
loadData: function () {
const url = API_URL + '/machine/install'
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
this.$http.get(url, headers).then(response => {
this.machines = response.body
}, response => {
Toast.create.negative('서버 정보 수신 실패')
})
}
},
mounted: function () {
if (!this.$store.state.login) { this.$router.push('/') }
this.loadData()
this.intervalObj = setInterval(this.loadData, POLLING_INTERVAL)
},
destroyed: function () {
clearInterval(this.intervalObj)
}
}
</script>
<style>
</style>
......@@ -51,14 +51,43 @@
<i>delete</i>
<q-tooltip>서버 삭제: {{ cell.row.name }}</q-tooltip>
</button>
<button
class="purple small circular"
v-if="cell.row.ipmi_ip"
@click="showConsole(cell.row)"
>
<i>computer</i>
<q-tooltip>서버 콘솔: {{ cell.row.name }}</q-tooltip>
</button>
</template>
</q-data-table>
<q-modal
ref="consoleModal" class="maximized"
@escape-key="notifyConsoleClose()"
@close="notifyConsoleClose()"
>
<q-layout>
<div slot="header" class="toolbar purple">
<button @click="$refs.consoleModal.close()">
<i>keyboard_arrow_left</i>
</button>
<q-toolbar-title>
콘솔: {{ this.machine }}
</q-toolbar-title>
</div>
<div class="layout-view">
<div class="layout-padding">
<iframe v-bind:src="consoleUrl" scrolling="no"
width="100%" height="500" frameborder="0"></iframe>
</div>
</div>
</q-layout>
</q-modal>
</div>
</template>
<script>
import { API_URL, selectOS } from 'config'
import { API_URL, selectOS, POLLING_INTERVAL } from 'config'
import { Loading, Dialog, Toast } from 'quasar'
function show (options) {
......@@ -76,6 +105,8 @@ export default {
data: function () {
return {
intervalObj: [],
machine: '',
consoleUrl: '',
machines: [],
config: {
rowHeight: '10px',
......@@ -114,6 +145,13 @@ export default {
sort: true
},
{
label: 'IPMI 주소',
field: 'ipmi_ip',
width: '100px',
filter: true,
sort: false
},
{
label: 'MAC 주소',
field: 'mac',
width: '100px',
......@@ -131,22 +169,16 @@ export default {
label: '상태',
field: 'state',
width: '100px',
filter: true,
sort: true
},
{
label: '설치유무',
field: 'is_installed',
width: '70px',
filter: true,
sort: true
},
{
label: '생성일',
field: 'createdAt',
width: '150px',
filter: 'date',
sort: true,
format (value) { return new Date(value).toLocaleString() }
},
{ label: '기능', field: 'functions', width: '150px', sort: false }
]
}
......@@ -170,11 +202,48 @@ export default {
show()
this.$http.post(url, {}, headers).then(response => {
Loading.hide()
this.loadData()
Toast.create.positive(`서버 ${row.name} 준비 완료`)
}, response => {
Toast.create.negative(`서버 ${row.name} 준비 실패`)
})
},
notifyConsoleClose: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
let url = API_URL + `/machine/${this.machine}/console/close`
console.log(url)
this.$http.post(url, {}, headers).then(response => {
}, response => {
})
},
showConsole: function (row) {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
let url = API_URL + `/machine/${row.name}/console`
console.log(url)
this.$http.get(url, headers).then(response => {
this.machine = row.name
this.consoleUrl = response.body
setTimeout(() => {
this.$refs.consoleModal.open()
}, 500)
}, response => {
let err = `콘솔 생성 실패`
Toast.create.negative({
html: err,
timeout: 5000
})
})
},
processEdit: function (row) {
let self = this
Dialog.create({
......@@ -215,6 +284,11 @@ export default {
type: 'heading',
label: '입력 항목'
},
ipmi_ip: {
type: 'textbox',
label: 'IPMI IP 주소',
model: row.ipmi_ip ? row.ipmi_ip : ''
},
mac: {
type: 'textbox',
label: 'MAC 주소',
......@@ -351,32 +425,16 @@ export default {
}, response => {
Toast.create.negative('서버 정보 수신 실패')
})
},
refresh: function () {
const refreshToken = this.$store.state.refreshToken
const headers = {
headers: {
'Authorization': `Bearer ${refreshToken}`
}
}
const url = API_URL + '/account/refresh'
this.$http.post(url, {}, headers).then(response => {
this.$state.commit('setToken', response.body.token)
}, response => {
})
}
},
mounted: function () {
if (!this.$store.state.login) { this.$router.push('/') }
this.loadData()
this.intervalObj = setInterval(this.loadData, 10 * 1000)
this.refresh()
this.intervalRefresh = setInterval(this.refresh, 5 * 60 * 1000)
this.intervalObj = setInterval(this.loadData, POLLING_INTERVAL)
},
destroyed: function () {
clearInterval(this.intervalObj)
clearInterval(this.intervalRefresh)
}
}
</script>
......
<template>
<div>
<h6>
서버 생성
</h6>
<p></p>
<form>
<div class="stacked-label">
<input
type="text"
v-model="form.name"
ref="name"
class="full-width"
:class="{'has-error': $v.form.name.$error}"
placeholder="Type machine name"
>
<label>이름</label>
<span
class="text-negative"
v-if="!$v.form.name.required">
Field is required
</span>
<span
class="text-negative"
v-if="!$v.form.name.minLength">
Name must be longer than 3 letters.
</span>
<span
class="text-negative"
v-if="!$v.form.name.alphaNum">
Name must be alphanumeric or hyphen.
</span>
</div>
<p></p>
<div class="stacked-label">
<label>종류</label><p></p>
<q-select
type="radio"
v-model="form.type"
ref="type"
:options="machineType">
</q-select>
</div>
<p></p>
<div class="stacked-label">
<label>OS</label><p></p>
<q-select
type="radio"
v-model="form.os"
ref="os"
:options="selectOS">
</q-select>
</div>
<p></p>
<div class="stacked-label">
<label>MAC주소</label><p></p>
<input
type="text"
v-model="form.mac"
ref="mac"
class="full-width"
:class="{'has-error': $v.form.mac.$error}"
placeholder="Type machine Mac Address"
>
<span
class="text-negative"
v-if="!$v.form.mac.required">
Field is required
</span>
</div>
<p></p>
<div class="stacked-label">
<label>IP주소</label><p></p>
<input
type="text"
v-model="form.ip"
ref="ip"
class="full-width"
:class="{'has-error': $v.form.ip.$error}"
placeholder="Type machine IP Address"
>
<span
class="text-negative"
v-if="!$v.form.ip.required">
Field is required
</span>
</div>
<p></p>
<div class="stacked-label">
<label>설명</label><p></p>
<input
type="text"
v-model="form.desc"
ref="desc"
class="full-width"
placeholder="Type machine description"
>
</div>
<p></p>
<div>
<button class="purple" @click="submit">
생성
</button>
</div>
</form>
</div>
</template>
<script>
import { API_URL, selectOS } from 'config'
import { Toast } from 'quasar'
import { required, minLength, alphaNum } from 'vuelidate/lib/validators'
export default {
name: 'machineCreate',
data () {
return {
form: {
name: '',
type: '',
os: '',
mac: '',
ip: '',
desc: ''
},
machineType: [
{'label': 'PM', value: 'PM'},
{'label': 'VM', value: 'VM'}
],
selectOS: selectOS
}
},
validations: {
form: {
name: {
required,
alphaNum,
minLength: minLength(3)
},
mac: {
required
},
ip: {
required
}
}
},
methods: {
submit: function () {
const url = API_URL + '/machine'
const payload = {
name: this.form.name,
type: this.form.type,
os: this.form.os,
mac: this.form.mac,
ip: this.form.ip,
desc: this.form.desc,
preseed: ''
}
if (!payload.os) {
console.log(payload)
return
}
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
this.$http.post(url, payload, headers).then(response => {
this.$router.push('/machine')
}, response => {
Toast.create.negative(response.statusText)
this.$refs.name.focus()
})
}
},
mounted () {
if (!this.$store.state.login) { this.$router.push('/') }
this.$refs.name.focus()
}
}
</script>
<style>
</style>
<template>
<div>
<div>
<q-tabs
:refs="$refs"
v-model="qtabs"
class="toolbar text-white justified"
>
<q-tab name="os-tab" :disable=true>
1. OS 선택
</q-tab>
<q-tab name="machine-tab" :disable=true>
2. 서버 선택
</q-tab>
<q-tab name="install-tab" :disable=true>
3. OS 배포
</q-tab>
</q-tabs>
</div>
<br>
<div ref="os-tab">
<div class="card">
<div class="card-title">
OS 선택
</div>
<div class="card-content">
<label v-for="os in selectOS">
<q-radio
v-model="form.os"
v-bind:val="os.value"
@input="radioVal()"
>
</q-radio>
{{ os.value }}
</label>
</div>
</div>
<button
class="primary"
@click="moveTab('machine-tab')"
>
다음
</button>
</div>
<div ref="machine-tab">
<div class="card">
<div class="card-title">
<input type="checkbox" @click="toggle" v-model="allSelected">
선택 OS: {{ this.form.os }}
</div>
<div class="card-content">
<span v-for="machine in this.machines">
<input
ref="machineCheckbox"
type="checkbox"
v-model="form.machines"
v-bind:value="machine"
>
{{ machine }}
</span>
</div>
</div>
<button
class="primary"
@click="moveTab('os-tab')"
>
이전
</button>
<button
class="primary"
@click="moveTab('install-tab')"
>
다음
</button>
</div>
<div ref="install-tab">
<div class="card">
<div class="card-title">
배포 요약
</div>
<div class="card-content">
<div>선택 OS: {{ this.form.os }}</div>
<div>선택 서버</div>
<ul v-for="machine in this.form.machines">
<li>{{ machine }}</li>
</ul>
</div>
</div>
<button
class="primary"
@click="moveTab('machine-tab')"
>
이전
</button>
<button
class="primary"
@click="submit()"
>
배포
</button>
</div>
</div>
</template>
<script>
import { API_URL, selectOS } from 'config'
import { Toast } from 'quasar'
export default {
name: 'OSList',
data: function () {
return {
qtabs: 'os-tab',
selectOS: selectOS,
machines: [],
allSelected: false,
oses: {
'JESSIE': [],
'CENTOS7': []
},
form: {
os: '',
machines: []
}
}
},
methods: {
submit: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
const url = API_URL + '/machine/os'
this.$http.post(url, this.form, headers).then(response => {
console.log(response.body)
this.$router.push('/log')
}, response => {
Toast.create.negative('서버 OS 배포 실패')
return
})
},
toggle: function () {
let selected = []
if (this.allSelected) {
this.machines.forEach(function (machine) {
selected.push(machine)
})
}
this.form.machines = selected
},
radioVal: function () {
// changed so set allSelected to false.
this.allSelected = false
},
moveTab: function (tab) {
if (tab === 'machine-tab') {
if (!this.form.os) {
Toast.create.negative('OS는 필수 항목입니다.')
return
}
this.getMachineListByOS()
}
if (tab === 'install-tab') {
if (!this.form.machines.length) {
Toast.create.negative('서버는 필수 항목입니다.')
return
}
}
this.qtabs = tab
},
getMachineListByOS: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
const url = API_URL + '/machine'
this.machines = []
this.$http.get(url, headers).then(response => {
let self = this
let result = response.body
result.forEach(function (el) {
if (!el.is_installed &&
el.state === 'Provisioned' &&
el.os === self.form.os) {
self.machines.push(el.name)
}
})
}, response => {
Toast.create.negative('서버 정보 수신 실패')
return
})
}
},
mounted: function () {
if (!this.$store.state.login) { this.$router.push('/') }
},
destroyed: function () {
}
}
</script>
<style lang="stylus">
view-flexbox
margin-top 30px
max-width 200vw
</style>
<template>
<div>
<div>
<q-tabs
:refs="$refs"
v-model="qtabs"
class="toolbar justified"
>
<q-tab name="app-tab" :disable=true>
1. 애플리케이션 선택
</q-tab>
<q-tab name="machine-tab" :disable=true>
2. 서버 선택
</q-tab>
<q-tab name="install-tab" :disable=true>
3. 애플리케이션 배포
</q-tab>
</q-tabs>
</div>
<br>
<div ref="app-tab">
<div class="card">
<div class="card-title">
애플리케이션 선택
</div>
<div class="card-content">
<label v-for="app in selectApp">
<q-radio
v-model="form.name"
v-bind:val="app.value"
@input="radioVal()"
>
</q-radio>
{{ app.value }}
</label>
</div>
</div>
<div class="card">
<div class="card-title">
태그 선택
</div>
<div class="card-content">
<label v-for="tag in tags">
<q-radio
v-model="form.tag"
v-bind:val="tag.value"
>
</q-radio>
{{ tag.value }}
</label>
</div>
</div>
<button
class="primary"
@click="moveTab('machine-tab')"
>
다음
</button>
</div>
<div ref="machine-tab">
<div class="card">
<div class="card-title">
선택 애플리케이션: {{ this.form.name }}
</div>
<div class="card-content">
<div class="card" v-for="(machines, section) in this.sections">
<div class="card-title">
그룹 {{ section }}
</div>
<div class="card-content">
<span v-for="machine in machines">
<input
ref="machineCheckbox"
type="checkbox"
v-model="form.machines"
v-bind:value="machine.ip"
>
{{ machine.name }} ({{ machine.ip }})
</span>
</div>
</div>
</div>
</div>
<button
class="primary"
@click="moveTab('app-tab')"
>
이전
</button>
<button
class="primary"
@click="moveTab('install-tab')"
>
다음
</button>
</div>
<div ref="install-tab">
<div class="card">
<div class="card-title">
배포 요약
</div>
<div class="card-content">
<div>선택 애플리케이션: {{ this.form.name }}</div>
<div>선택 태그: {{ this.form.tag }}</div>
<div>선택 서버:
<span v-for="machine in this.form.machines">
{{ machine }}
</span>
</div>
</div>
</div>
<button
class="primary"
@click="moveTab('machine-tab')"
>
이전
</button>
<button
class="primary"
@click="submit()"
>
배포
</button>
</div>
</div>
</template>
<script>
import { API_URL } from 'config'
import { Toast } from 'quasar'
export default {
name: 'AppList',
data: function () {
return {
intervalLogObj: [],
qtabs: 'app-tab',
selectApp: [],
tags: [],
sections: {'b': []},
machines: [],
allSelected: false,
playlog: '',
dLog: {},
sameAsBefore: 0,
form: {
app: '',
tag: '',
machines: []
}
}
},
methods: {
getLog: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
const url = API_URL + `/app/${this.form.name}/play/log`
this.$http.post(url, this.dLog, headers).then(response => {
if (response.body === this.playlog) {
this.sameAsBefore += 1
}
else {
this.sameAsBefore = 0
}
console.log(this.sameAsBefore)
if (this.sameAsBefore === 30) {
clearInterval(this.intervalLogObj)
}
this.playlog = response.body
this.$refs.textarea.scrollTop = this.$refs.textarea.scrollHeight
}, response => {
})
},
submit: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
const url = API_URL + '/app/' + this.form.name + '/play'
this.$http.post(url, this.form, headers).then(response => {
this.$router.push('/log/' + response.body)
}, response => {
Toast.create.negative('서버 애플리케이션 배포 실패')
return
})
// this.intervalLogObj = setInterval(this.getLog, 5 * 1000)
},
toggle: function () {
let selected = []
if (this.allSelected) {
this.machines.forEach(function (machine) {
selected.push(machine)
})
}
this.form.machines = selected
},
radioVal: function () {
// get tags when app is selected.
this.getTags()
},
moveTab: function (tab) {
if (tab === 'machine-tab') {
if (!this.form.tag) {
Toast.create.negative('태그는 필수 항목입니다.')
return
}
}
if (tab === 'install-tab') {
if (!this.form.machines.length) {
Toast.create.negative('서버는 필수 항목입니다.')
return
}
}
this.qtabs = tab
},
getTags: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
const url = API_URL + '/app/' + this.form.name
this.tags = []
this.$http.get(url, headers).then(response => {
if (response.body.tag) {
let self = this
response.body.tag.forEach(function (element) {
self.tags.push({
label: element,
value: element
})
})
}
}, response => {
Toast.create.negative('태그 정보 수신 실패')
})
},
getApp: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
const url = API_URL + '/app'
this.$http.get(url, headers).then(response => {
let self = this
response.body.forEach(function (element) {
self.selectApp.push({
label: element.name,
value: element.name
})
})
}, response => {
Toast.create.negative('애플리케이션 정보 수신 실패')
})
},
getMachineListBySection: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
this.sections = {}
const url = API_URL + '/machine'
this.$http.get(url, headers).then(response => {
let self = this
let result = response.body
console.log(result)
result.forEach(function (element) {
self.sections[element.section] = []
})
result.forEach(function (element) {
self.sections[element.section].push({
name: element.name,
ip: element.ip
})
})
}, response => {
Toast.create.negative('서버 정보 수신 실패')
})
}
},
mounted: function () {
if (!this.$store.state.login) { this.$router.push('/') }
this.getApp()
this.getMachineListBySection()
},
destroyed: function () {
clearInterval(this.intervalLogObj)
}
}
</script>
<style lang="stylus">
view-flexbox
margin-top 30px
max-width 200vw
</style>
......@@ -35,7 +35,7 @@
</template>
<script>
import { API_URL } from 'config'
import { API_URL, POLLING_INTERVAL } from 'config'
import { Dialog, Toast } from 'quasar'
export default {
......@@ -44,7 +44,6 @@ export default {
return {
role: this.$store.state.role,
intervalObj: [],
intervalRefresh: [],
sections: [],
config: {
title: '그룹 목록',
......@@ -211,22 +210,7 @@ export default {
this.$http.get(url, headers).then(response => {
this.sections = response.body
}, response => {
if (response.status === 401) {
this.$router.push('/login')
}
})
},
refresh: function () {
const refreshToken = this.$store.state.refreshToken
const headers = {
headers: {
'Authorization': `Bearer ${refreshToken}`
}
}
const url = API_URL + '/account/refresh'
this.$http.post(url, {}, headers).then(response => {
this.$store.commit('setToken', response.body.token)
}, response => {
Toast.create.negative('서버 정보 수신 실패')
})
}
},
......@@ -234,13 +218,10 @@ export default {
if (!this.$store.state.login) { this.$router.push('/') }
this.loadData()
this.intervalObj = setInterval(this.loadData, 10 * 1000)
this.refresh()
this.intervalRefresh = setInterval(this.refresh, 5 * 60 * 1000)
this.intervalObj = setInterval(this.loadData, POLLING_INTERVAL)
},
destroyed: function () {
clearInterval(this.intervalObj)
clearInterval(this.intervalRefresh)
}
}
</script>
......
......@@ -67,6 +67,16 @@ export default new VueRouter({
component: load('app/Create')
},
{
path: '/app/:name/file',
name: 'appFile',
component: load('app/File')
},
{
path: '/app/:name/tag',
name: 'appTag',
component: load('app/Tag')
},
{
path: '/account',
name: 'account',
component: load('account/List')
......@@ -87,11 +97,36 @@ export default new VueRouter({
component: load('account/Pass')
},
{
path: '/account/:name/userpass',
name: 'accountUserPassword',
component: load('account/UserPass')
},
{
path: '/os',
name: 'os',
component: load('os/List')
},
{
path: '/play',
name: 'play',
component: load('play/List')
},
{
path: '/log',
name: 'log',
component: load('log/List')
},
{
path: '/log/:name',
name: 'logRead',
component: load('log/Read')
},
{
path: '/log/file/:name',
name: 'logReadFile',
component: load('log/ReadFile')
},
{
path: '/',
name: 'index',
component: load('Index')
......
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