Commit f655fbfe by Heechul Kim

updated

parent c80c0ac4
...@@ -6,9 +6,12 @@ import logging ...@@ -6,9 +6,12 @@ import logging
import logging.config import logging.config
from datetime import datetime from datetime import datetime
from datetime import timedelta
from porch.database import etcdc from porch.database import etcdc
from porch.config import ADMIN_ID from porch.config import ADMIN_ID
from porch.config import ADMIN_SALT
from porch.config import ADMIN_PW
logging.config.fileConfig('logging.conf') logging.config.fileConfig('logging.conf')
log = logging.getLogger('porch') log = logging.getLogger('porch')
...@@ -71,14 +74,27 @@ def admin_auth_account(data): ...@@ -71,14 +74,27 @@ def admin_auth_account(data):
try: try:
r = etcdc.read(s_rsc) r = etcdc.read(s_rsc)
except etcd.EtcdKeyNotFound as e: except etcd.EtcdKeyNotFound as e:
log.error(e) # auth against config file.
return b_ret s_pass = bcrypt.hashpw(data['pass'].encode(),
ADMIN_SALT.encode()).decode()
d = ast.literal_eval(r.value) if data['name'] == ADMIN_ID and s_pass == ADMIN_PW:
# Add admin entry in etcd and return True.
s_pass = bcrypt.hashpw(data['pass'].encode(), d = dict()
d['salt'].encode()).decode() d['name'] = ADMIN_ID
return data['name'] == ADMIN_ID and s_pass == d['pass'] 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']
def auth_account(data): def auth_account(data):
"""Auth account. """Auth account.
...@@ -190,15 +206,14 @@ def password_account(data): ...@@ -190,15 +206,14 @@ def password_account(data):
data['modifiedAt'] = s_modified data['modifiedAt'] = s_modified
# Put d['pass'] to oldpass entry. # Put d['pass'] to oldpass entry.
if 'oldpass' in d: if 'oldpass' in d:
new_data['oldpass'].append(d['pass']) d['oldpass'].append(d['pass'])
else: else:
new_data['oldpass'] = [d['pass']] d['oldpass'] = [d['pass']]
# Create new hashed password. # Create new hashed password.
bytes_salt = bytes(d['salt'], 'utf-8') 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() bytes_salt).decode()
d.update(new_data.items())
s_rsc = '{}/account/{}'.format(etcdc.prefix, data['name']) s_rsc = '{}/account/{}'.format(etcdc.prefix, data['name'])
try: try:
etcdc.write(s_rsc, d, prevExist=True) etcdc.write(s_rsc, d, prevExist=True)
...@@ -235,3 +250,58 @@ def _pass_validate(data): ...@@ -235,3 +250,58 @@ def _pass_validate(data):
return (False, 'PasswordPreviouslyUsed') return (False, 'PasswordPreviouslyUsed')
return (True, 'PasswordMatched') 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 ...@@ -18,11 +18,14 @@ from flask_jwt_extended import get_jwt_claims
from porch.api.restplus import api from porch.api.restplus import api
from porch.decorators import authz_required 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 accountSerializer
from porch.api.account.serializers import accountPostSerializer from porch.api.account.serializers import accountPostSerializer
from porch.api.account.serializers import accountAuthSerializer from porch.api.account.serializers import accountAuthSerializer
from porch.api.account.serializers import accountPatchSerializer from porch.api.account.serializers import accountPatchSerializer
from porch.api.account.serializers import accountPasswordSerializer 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 get_account
from porch.api.account.bizlogic import create_account from porch.api.account.bizlogic import create_account
...@@ -31,6 +34,7 @@ from porch.api.account.bizlogic import auth_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 update_account
from porch.api.account.bizlogic import delete_account from porch.api.account.bizlogic import delete_account
from porch.api.account.bizlogic import password_account from porch.api.account.bizlogic import password_account
from porch.api.account.bizlogic import user_password_account
log = logging.getLogger('porch') log = logging.getLogger('porch')
...@@ -74,6 +78,10 @@ class AdminAccountAuth(Resource): ...@@ -74,6 +78,10 @@ class AdminAccountAuth(Resource):
"""Authenticate an account. """Authenticate an account.
""" """
data = request.json data = request.json
if data['name'] != ADMIN_ID:
d_msg = { 'error': 'Authentication is failed.' }
return d_msg, 401
b_ret = admin_auth_account(data) b_ret = admin_auth_account(data)
if not b_ret: if not b_ret:
d_msg = {'error': 'Admin Authentication is failed.'} d_msg = {'error': 'Admin Authentication is failed.'}
...@@ -97,6 +105,10 @@ class AccountAuth(Resource): ...@@ -97,6 +105,10 @@ class AccountAuth(Resource):
"""Authenticate an account. """Authenticate an account.
""" """
data = request.json 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) (b_ret, s_ret) = auth_account(data)
if not b_ret: if not b_ret:
s_msg = 'Authentication is failed. ' s_msg = 'Authentication is failed. '
...@@ -137,6 +149,7 @@ class AccountRefresh(Resource): ...@@ -137,6 +149,7 @@ class AccountRefresh(Resource):
d_resp = { d_resp = {
'token': create_access_token(identity=uid) 'token': create_access_token(identity=uid)
} }
log.debug(d_resp)
return d_resp, 200 return d_resp, 200
@ns.route('/<string:name>') @ns.route('/<string:name>')
...@@ -196,7 +209,7 @@ class AccountPassword(Resource): ...@@ -196,7 +209,7 @@ class AccountPassword(Resource):
"""Updates the account password.""" """Updates the account password."""
d_claim = get_jwt_claims() d_claim = get_jwt_claims()
# check authz # check authz
if d_claim['id'] != 'admin' and name != d_claim['id']: if d_claim['id'] != 'admin':
abort(401, 'Not authorized.') abort(401, 'Not authorized.')
data = request.json data = request.json
(b_ret, s_msg) = password_account(data) (b_ret, s_msg) = password_account(data)
...@@ -206,3 +219,26 @@ class AccountPassword(Resource): ...@@ -206,3 +219,26 @@ class AccountPassword(Resource):
l_account = get_account(name) l_account = get_account(name)
return l_account[0], 200 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', { ...@@ -58,3 +58,9 @@ accountPasswordSerializer = api.model('PasswordAccount', {
'pass': fields.String(required=True, description='plaintext password'), 'pass': fields.String(required=True, description='plaintext password'),
'pass2': 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 ast
import time
import etcd import etcd
import uuid import uuid
import shutil import shutil
import signal
import tempfile import tempfile
import logging import logging
import logging.config import logging.config
from subprocess import Popen
from concurrent.futures import ProcessPoolExecutor
from datetime import datetime from datetime import datetime
from git import Repo from git import Repo
from flask import render_template from flask import render_template
from porch.utils import cmd from porch.utils import cmd
from porch.utils import run_command from porch.utils import get_port
from porch.database import etcdc from porch.database import etcdc
from porch.config import GIT_DIR from porch.config import GIT_DIR
from porch.config import PLAY_DIR from porch.config import PLAY_DIR
from porch.config import PRIKEY_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') logging.config.fileConfig('logging.conf')
log = logging.getLogger('porch') log = logging.getLogger('porch')
...@@ -164,7 +172,7 @@ def play_app(name, data): ...@@ -164,7 +172,7 @@ def play_app(name, data):
d = ast.literal_eval(r.value) d = ast.literal_eval(r.value)
# get repo and tag. # 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( o_repo = Repo.clone_from(
'file://' + GIT_DIR + '/' + d['repo'], 'file://' + GIT_DIR + '/' + d['repo'],
...@@ -183,39 +191,162 @@ def play_app(name, data): ...@@ -183,39 +191,162 @@ def play_app(name, data):
# Let's play. # Let's play.
s_prikey = PRIKEY_DIR + '/prikey' s_prikey = PRIKEY_DIR + '/prikey'
s_cmd= "ansible-playbook -i {0}/hosts --private-key={1} {0}/site.yml"\ s_ssh_options = "--ssh-common-args='-o UserKnownHostsFile=/dev/null'"
.format(s_play_dir, s_prikey) 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) log.debug(s_cmd)
# set log file.
s_logfile = s_play_dir + '/' + name + '.log' # set logfile name
b_ret = run_command(s_cmd) s_date = datetime.now().isoformat()
if not b_ret: s_logfile = APP_LOG +'/' + s_date
s_msg = 'Play is failed.' # 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) return (False, s_msg)
pool = ProcessPoolExecutor(3)
future = pool.submit(_bgprocess, s_cmd, s_logfile)
# Delete cloned app. # Delete cloned app.
try: #try:
shutil.rmtree(s_play_dir) # shutil.rmtree(s_play_dir)
except FileNotFoundError as e: #except FileNotFoundError as e:
log.error(e) # log.error(e)
return (True, 'Play is succeeded.') return (True, s_date)
def get_app_log(name): 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(s_logfile):
"""Get app log. """Get app log.
""" """
l_log = [] s_log = ''
s_play_dir = PLAY_DIR + '/' + name
s_logfile = s_play_dir + '/' + name + '.log'
log.debug(s_logfile) log.debug(s_logfile)
try: try:
with open(s_logfile, 'r') as f: with open(s_logfile, 'r') as f:
s_log = f.read() s_log = f.read()
except: except:
return l_log pass
else:
l_log.append(s_log) return s_log
def file_container_app(name):
"""Run container for app file management."""
# get port for container connection
i_port = get_port()
log.debug(l_log) # check if container is already running.
return l_log 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:
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)
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 ...@@ -14,12 +14,19 @@ from porch.api.app.serializers import appSerializer
from porch.api.app.serializers import appPostSerializer from porch.api.app.serializers import appPostSerializer
from porch.api.app.serializers import appPatchSerializer from porch.api.app.serializers import appPatchSerializer
from porch.api.app.serializers import appPlaySerializer 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 get_app
from porch.api.app.bizlogic import create_app from porch.api.app.bizlogic import create_app
from porch.api.app.bizlogic import update_app from porch.api.app.bizlogic import update_app
from porch.api.app.bizlogic import delete_app from porch.api.app.bizlogic import delete_app
from porch.api.app.bizlogic import play_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') log = logging.getLogger('porch')
...@@ -112,20 +119,64 @@ class AppPlay(Resource): ...@@ -112,20 +119,64 @@ class AppPlay(Resource):
data = request.json data = request.json
(b_ret, s_msg) = play_app(name, data) (b_ret, s_msg) = play_app(name, data)
if not b_ret: if not b_ret:
d_msg = {'error', s_msg} d_msg = {'error': s_msg}
i_ret_code = 400 i_ret_code = 400
else: return d_msg, i_ret_code
d_msg = {'msg': s_msg}
return d_msg, i_ret_code return s_msg, i_ret_code
def get(self, name): @ns.route('/<string:name>/play/log')
@ns.response(404, 'App not found.')
class AppPlayLog(Resource):
def post(self, name):
"""Returns the app play log.""" """Returns the app play log."""
i_ret_code = 200 data = request.json
l_app = get_app_log(name) s_log = get_app_log(data['log'])
if not len(l_app):
d_msg = {'error': 'name {} not found.'.format(name)} return s_log
i_ret_code = 404
return d_msg, i_ret_code @ns.route('/<string:name>/file_container')
else: @ns.response(404, 'App not found.')
return l_app, i_ret_code 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', { ...@@ -27,4 +27,8 @@ appPlaySerializer = api.model('PlayApp', {
'tag': fields.String(required=True, description='app name'), 'tag': fields.String(required=True, description='app name'),
'machines': fields.List(fields.String), '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): ...@@ -49,7 +49,7 @@ class Pxe(Resource):
s_content = '' s_content = ''
# check if it is installed. # check if it is installed.
if not d_machine['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: with open(s_src, 'rb') as o_src:
s_content = o_src.read() s_content = o_src.read()
......
import os import os
import ast import ast
import uuid import uuid
import time
import etcd import etcd
import stat import stat
import shutil import shutil
import signal
import tarfile import tarfile
import logging import logging
import logging.config import logging.config
from subprocess import Popen
from concurrent.futures import ProcessPoolExecutor
from datetime import datetime from datetime import datetime
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.asymmetric import rsa
...@@ -15,15 +19,22 @@ from cryptography.hazmat.backends import default_backend ...@@ -15,15 +19,22 @@ from cryptography.hazmat.backends import default_backend
from flask import render_template from flask import render_template
from porch.database import etcdc from porch.database import etcdc
from porch.utils import get_port
from porch.config import TFTP_FAI_DIR from porch.config import TFTP_FAI_DIR
from porch.config import PXELINUXCFG from porch.config import PXELINUXCFG
from porch.config import PRIKEY_DIR from porch.config import PRIKEY_DIR
from porch.config import PUBKEY_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_KERNEL_URL
from porch.config import FAI_INITRD_URL from porch.config import FAI_INITRD_URL
from porch.config import FAI_SQUASH_URL from porch.config import FAI_SQUASH_URL
from porch.config import DHCPEDIT 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 from porch.utils import cmd
...@@ -259,4 +270,163 @@ def delete_machine(name): ...@@ -259,4 +270,163 @@ def delete_machine(name):
s_rsc = '{}/macaddr/{}'.format(etcdc.prefix, s_mac) s_rsc = '{}/macaddr/{}'.format(etcdc.prefix, s_mac)
etcdc.delete(s_rsc) 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)) 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 ...@@ -12,6 +12,8 @@ from flask_jwt_extended import get_jwt_identity
from porch.api.restplus import api from porch.api.restplus import api
from porch.decorators import authz_required 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 machineSerializer
from porch.api.machine.serializers import machinePostSerializer from porch.api.machine.serializers import machinePostSerializer
from porch.api.machine.serializers import machinePatchSerializer from porch.api.machine.serializers import machinePatchSerializer
...@@ -21,6 +23,9 @@ from porch.api.machine.bizlogic import create_machine ...@@ -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 update_machine
from porch.api.machine.bizlogic import delete_machine from porch.api.machine.bizlogic import delete_machine
from porch.api.machine.bizlogic import provision_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') log = logging.getLogger('porch')
...@@ -112,3 +117,71 @@ class MachineProvision(Resource): ...@@ -112,3 +117,71 @@ class MachineProvision(Resource):
return d_msg, i_ret_code 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', { ...@@ -13,6 +13,7 @@ machineSerializer = api.model('ListMachine', {
default='', description='machine preseed'), default='', description='machine preseed'),
'mac': fields.String(required=True, description='machine mac'), 'mac': fields.String(required=True, description='machine mac'),
'ip': fields.String(required=True, description='machine ip address'), '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', 'state': fields.String(required=True, default='Created',
description='state: Created, Provisioned'), description='state: Created, Provisioned'),
'is_installed': fields.Boolean(required=True, default=False, 'is_installed': fields.Boolean(required=True, default=False,
...@@ -30,6 +31,7 @@ machinePostSerializer = api.model('RegisterMachine', { ...@@ -30,6 +31,7 @@ machinePostSerializer = api.model('RegisterMachine', {
description='machine preseed'), description='machine preseed'),
'mac': fields.String(required=True, description='machine mac'), 'mac': fields.String(required=True, description='machine mac'),
'ip': fields.String(required=True, description='machine ip address'), 'ip': fields.String(required=True, description='machine ip address'),
'ipmi_ip': fields.String(required=False, description='machine ipmi ip'),
}) })
machinePatchSerializer = api.model('ModifyMachine', { machinePatchSerializer = api.model('ModifyMachine', {
...@@ -38,6 +40,7 @@ machinePatchSerializer = api.model('ModifyMachine', { ...@@ -38,6 +40,7 @@ machinePatchSerializer = api.model('ModifyMachine', {
'section': fields.String(required=False, 'section': fields.String(required=False,
description='the section machine belongs to'), description='the section machine belongs to'),
'os': fields.String(required=False, description='machine os'), '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'), 'preseed': fields.String(required=False, description='machine preseed'),
'mac': fields.String(required=False, description='machine mac'), 'mac': fields.String(required=False, description='machine mac'),
'ip': fields.String(required=False, description='machine ip address'), 'ip': fields.String(required=False, description='machine ip address'),
......
...@@ -19,6 +19,7 @@ from porch.api.machine.endpoints.route import ns as machine_ns ...@@ -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.fai.endpoints.route import ns as fai_ns
from porch.api.account.endpoints.route import ns as account_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.app.endpoints.route import ns as app_ns
from porch.api.log.endpoints.route import ns as log_ns
app = Flask(__name__) app = Flask(__name__)
app.secret_key = JWT_SECRET_KEY app.secret_key = JWT_SECRET_KEY
...@@ -62,6 +63,7 @@ def initialize_app(flask_app): ...@@ -62,6 +63,7 @@ def initialize_app(flask_app):
api.add_namespace(fai_ns) api.add_namespace(fai_ns)
api.add_namespace(account_ns) api.add_namespace(account_ns)
api.add_namespace(app_ns) api.add_namespace(app_ns)
api.add_namespace(log_ns)
flask_app.register_blueprint(blueprint) flask_app.register_blueprint(blueprint)
......
...@@ -2,38 +2,75 @@ import os ...@@ -2,38 +2,75 @@ import os
from datetime import timedelta 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__)) APP_ROOT = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(APP_ROOT) PROJECT_ROOT = os.path.dirname(APP_ROOT)
# pxe # pxe
TFTP_FAI_DIR = "/srv/tftp/fai" TFTP_FAI_DIR = "/srv/tftp/fai"
BOOT = TFTP_FAI_DIR + "/boot.ipxe" BOOT = TFTP_FAI_DIR + "/boot.ipxe"
WINBOOT = TFTP_FAI_DIR + "/winpe/boot.ipxe" WINBOOT = TFTP_FAI_DIR + "/winpe/boot.ipxe"
LINBOOT = TFTP_FAI_DIR + "/lpxelinux.0" LINBOOT = TFTP_FAI_DIR + "/lpxelinux.0"
PXELINUXCFG = TFTP_FAI_DIR + '/pxelinux.cfg' 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_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_INITRD_URL = PORCH_URL + '/boot/initrd.img-3.16.0-4-amd64'
FAI_SQUASH_URL = PORCH_URL + '/boot/squash.img' FAI_SQUASH_URL = PORCH_URL + '/boot/squash.img'
## FAI related ## FAI related
FAI_MONITOR = "/usr/sbin/fai-monitor"
FAI_DIR = "/srv/fai" FAI_DIR = "/srv/fai"
PRIKEY_DIR = FAI_DIR + "/prikeys" PRIKEY_DIR = FAI_DIR + "/prikeys"
PUBKEY_DIR = FAI_DIR + "/pubkeys" PUBKEY_DIR = FAI_DIR + "/pubkeys"
WINANS_DIR = "/answer" 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 # dhcp-edit
DHCPEDIT = '/usr/sbin/dhcp-edit' DHCPEDIT = '/usr/sbin/dhcp-edit'
# git # git
GIT_DIR = APP_ROOT + '/git' GIT_DIR = APP_ROOT + '/git'
PLAY_DIR = APP_ROOT + '/play' 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'): ...@@ -15,8 +15,6 @@ def authz_required(role='admin'):
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
# Get role. # Get role.
d_claim = get_jwt_claims() d_claim = get_jwt_claims()
log.debug(d_claim)
log.debug(role)
if d_claim['role'] != 'admin': if d_claim['role'] != 'admin':
if 'role' not in d_claim or d_claim['role'] != role: if 'role' not in d_claim or d_claim['role'] != role:
d_msg = {'error': 'Authorization failed.'} d_msg = {'error': 'Authorization failed.'}
......
...@@ -2,4 +2,4 @@ default porch-generated ...@@ -2,4 +2,4 @@ default porch-generated
label porch-generated label porch-generated
kernel {{ data['fai_kernel_url'] }} 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 ...@@ -3,6 +3,8 @@ import logging.config
import subprocess import subprocess
import shlex import shlex
import time import time
import random
import socket
from subprocess import Popen from subprocess import Popen
from subprocess import PIPE from subprocess import PIPE
...@@ -11,6 +13,8 @@ from subprocess import TimeoutExpired ...@@ -11,6 +13,8 @@ from subprocess import TimeoutExpired
from porch.database import etcdc from porch.database import etcdc
from porch.config import TERM_PORT_BEGIN
from porch.config import TERM_PORT_END
logging.config.fileConfig('logging.conf') logging.config.fileConfig('logging.conf')
log = logging.getLogger('porch') log = logging.getLogger('porch')
...@@ -43,15 +47,23 @@ def cmd(s_cmd, i_timeout=30, b_sudo=False): ...@@ -43,15 +47,23 @@ def cmd(s_cmd, i_timeout=30, b_sudo=False):
return t_result return t_result
def run_command(command, name): def get_port():
process = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) """Get terminal port"""
s_rsc = '{}/log/{}'.format(etcdc.prefix, name) i_port = 0
while True: b_gotit = False
output = process.stdout.readline().decode() s_address = 'localhost'
if output == '' and process.poll() is not None: 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 break
if output: finally:
s_created = datetime.utcnow().isoformat() + 'Z' s.close()
data[s_created] = output.strip() i_port = i_port if b_gotit else 0
rc = process.poll()
return rc return i_port
export const API_URL = 'http://121.254.203.198:8000/api' 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 = [ export const selectOS = [
{label: 'CENTOS7', value: 'CENTOS7'}, {label: 'CENTOS7', value: 'CENTOS7'},
{label: 'UXENOS', value: 'UXENOS'}, {label: 'UXENOS', value: 'UXENOS'},
{label: 'JESSIE', value: 'JESSIE'} {label: 'JESSIE', value: 'JESSIE'}
] ]
*/
export const selectOS = [
{label: 'CENTOS7', value: 'CENTOS7'},
{label: 'JESSIE', value: 'JESSIE'}
]
export function checkStrength (pw) { export function checkStrength (pw) {
// initialize // initialize
var iStrength = 0 var iStrength = 0
......
...@@ -7,6 +7,15 @@ ...@@ -7,6 +7,15 @@
<q-tabs slot="navigation" class="purple"> <q-tabs slot="navigation" class="purple">
<q-tab <q-tab
v-if="this.$store.state.login" 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" icon="cast"
route="/os" route="/os"
exact exact
...@@ -61,6 +70,15 @@ ...@@ -61,6 +70,15 @@
</q-tab> </q-tab>
<q-tab <q-tab
v-if="this.$store.state.login" 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" icon="exit_to_app"
route="/logout" route="/logout"
exact exact
...@@ -99,24 +117,32 @@ ...@@ -99,24 +117,32 @@
</template> </template>
<script> <script>
import { API_URL, REFRESH_INTERVAL } from 'config'
export default { export default {
name: 'app', name: 'app',
data () { data () {
return { return {
porchVersion: '0.0.1', porchVersion: '0.0.1',
intervalRefresh: [],
position: 'bottom', position: 'bottom',
reverse: false, reverse: false,
size: 12, size: 8,
color: '#e21b0c' color: '#e21b0c'
} }
}, },
computed: { computed: {
computedSize () { computedSize () {
return this.size + 'px' return this.size + 'px'
},
computedPassUrl () {
return '/account/' + this.$store.state.name + '/userpass'
} }
}, },
methods: { methods: {
updateMenu: function () { updateMenu: function () {
/*
console.log(this.$cookie.get('token'))
if (this.$cookie.get('token')) { if (this.$cookie.get('token')) {
this.$store.commit('loggedIn', true) this.$store.commit('loggedIn', true)
this.$store.commit('setName', this.$cookie.get('name')) this.$store.commit('setName', this.$cookie.get('name'))
...@@ -124,6 +150,7 @@ export default { ...@@ -124,6 +150,7 @@ export default {
this.$store.commit('setToken', this.$cookie.get('token')) this.$store.commit('setToken', this.$cookie.get('token'))
this.$store.commit('setRefreshToken', this.$cookie.get('refreshToken')) this.$store.commit('setRefreshToken', this.$cookie.get('refreshToken'))
} }
*/
}, },
trigger () { trigger () {
this.$refs.bar.start() this.$refs.bar.start()
...@@ -133,6 +160,26 @@ export default { ...@@ -133,6 +160,26 @@ export default {
this.$refs.bar.stop() this.$refs.bar.stop()
} }
}, 5000) }, 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 () { updated () {
...@@ -140,6 +187,10 @@ export default { ...@@ -140,6 +187,10 @@ export default {
}, },
mounted () { mounted () {
this.updateMenu() this.updateMenu()
this.intervalRefresh = setInterval(this.refresh, REFRESH_INTERVAL)
},
destroyed: function () {
clearInterval(this.intervalRefresh)
} }
} }
</script> </script>
......
...@@ -11,7 +11,7 @@ export default { ...@@ -11,7 +11,7 @@ export default {
methods: { methods: {
redirect: function () { redirect: function () {
if (this.$store.state.login) { if (this.$store.state.login) {
this.$router.push('/section') this.$router.push('/machine')
} }
else { else {
this.$router.push('/login') this.$router.push('/login')
......
...@@ -73,10 +73,12 @@ export default { ...@@ -73,10 +73,12 @@ export default {
} }
this.$http.post(url, dReqBody, headers).then(response => { this.$http.post(url, dReqBody, headers).then(response => {
// save token cookie // save token cookie
/*
this.$cookie.set('token', response.body.token) this.$cookie.set('token', response.body.token)
this.$cookie.set('refreshToken', response.body.refresh_token) this.$cookie.set('refreshToken', response.body.refresh_token)
this.$cookie.set('role', response.body.role) this.$cookie.set('role', response.body.role)
this.$cookie.set('name', this.name) this.$cookie.set('name', this.name)
*/
// state update // state update
this.$store.commit('loggedIn', true) this.$store.commit('loggedIn', true)
this.$store.commit('setName', this.name) this.$store.commit('setName', this.name)
...@@ -98,6 +100,9 @@ export default { ...@@ -98,6 +100,9 @@ export default {
} }
}, },
mounted () { mounted () {
if (this.$store.state.login) {
this.$router.push('/')
}
this.$refs.name.focus() this.$refs.name.focus()
} }
} }
......
<template> <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> </template>
<script> <script>
export default { export default {
...@@ -11,16 +23,18 @@ export default { ...@@ -11,16 +23,18 @@ export default {
methods: { methods: {
logout: function () { logout: function () {
// delete token cookie // delete token cookie
/*
this.$cookie.delete('token') this.$cookie.delete('token')
this.$cookie.delete('refreshToken') this.$cookie.delete('refreshToken')
this.$cookie.delete('role') this.$cookie.delete('role')
this.$cookie.delete('name') this.$cookie.delete('name')
*/
this.$store.commit('loggedIn', false) this.$store.commit('loggedIn', false)
this.$store.commit('setName', '') this.$store.commit('setName', '')
this.$store.commit('setRole', '') this.$store.commit('setRole', '')
this.$store.commit('setToken', '') this.$store.commit('setToken', '')
this.$store.commit('setRefreshToken', '') this.$store.commit('setRefreshToken', '')
this.$router.push('/') // this.$router.push('/')
} }
}, },
mounted () { mounted () {
...@@ -28,6 +42,19 @@ export default { ...@@ -28,6 +42,19 @@ export default {
} }
} }
</script> </script>
<style lang="stylus">
<style> .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> </style>
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
class="purple small circular" class="purple small circular"
@click="goToPassword(cell.row.name)" @click="goToPassword(cell.row.name)"
> >
<i>lock</i> <i>vpn_key</i>
<q-tooltip>사용자 비밀번호 변경: {{ cell.row.name }}</q-tooltip> <q-tooltip>사용자 비밀번호 변경: {{ cell.row.name }}</q-tooltip>
</button> </button>
<button <button
...@@ -93,7 +93,7 @@ ...@@ -93,7 +93,7 @@
</template> </template>
<script> <script>
import { API_URL } from 'config' import { API_URL, POLLING_INTERVAL } from 'config'
import { Dialog, Toast } from 'quasar' import { Dialog, Toast } from 'quasar'
export default { export default {
...@@ -248,20 +248,10 @@ export default { ...@@ -248,20 +248,10 @@ export default {
this.$http.get(url, headers).then(response => { this.$http.get(url, headers).then(response => {
this.data = response.body this.data = response.body
}, response => { }, response => {
Toast.create.negative('계정 정보 수신 실패') if (response.status === 401) {
}) this.$router.push('/logout')
},
refresh: function () {
const refreshToken = this.$store.state.refreshToken
const headers = {
headers: {
'Authorization': `Bearer ${refreshToken}`
} }
} Toast.create.negative('계정 정보 수신 실패')
const url = API_URL + '/account/refresh'
this.$http.post(url, {}, headers).then(response => {
this.$store.commit('setToken', response.body.token)
}, response => {
}) })
} }
}, },
...@@ -269,13 +259,10 @@ export default { ...@@ -269,13 +259,10 @@ export default {
if (!this.$store.state.login) { this.$router.push('/') } if (!this.$store.state.login) { this.$router.push('/') }
this.loadData() this.loadData()
this.intervalObj = setInterval(this.loadData, 10 * 1000) this.intervalObj = setInterval(this.loadData, POLLING_INTERVAL)
this.refresh()
this.intervalRefresh = setInterval(this.refresh, 5 * 60 * 1000)
}, },
destroyed: function () { destroyed: function () {
clearInterval(this.intervalObj) clearInterval(this.intervalObj)
clearInterval(this.intervalRefresh)
} }
} }
</script> </script>
......
...@@ -116,30 +116,13 @@ export default { ...@@ -116,30 +116,13 @@ export default {
this.$http.patch(url, payload, headers).then(response => { this.$http.patch(url, payload, headers).then(response => {
this.$router.push('/account') this.$router.push('/account')
}, response => { }, response => {
Toast.create.negative('사용자 수정 실패: ' + response.body['error']) Toast.create.negative('비밀번호 변경 실패: ' + response.body['error'])
}) })
}, },
loadData: function () { 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 () { mounted: function () {
this.loadData()
} }
} }
</script> </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 @@ ...@@ -13,9 +13,9 @@
type="text" type="text"
v-model="name" v-model="name"
ref="name" ref="name"
placeholder="사용자 ID를 입력하세요." placeholder="관리자 ID를 입력하세요."
> >
<label>사용자 ID</label> <label>관리자 ID</label>
</div> </div>
<div class="stacked-label"> <div class="stacked-label">
<input <input
...@@ -61,10 +61,12 @@ export default { ...@@ -61,10 +61,12 @@ export default {
} }
} }
this.$http.post(url, dReqBody, headers).then(response => { this.$http.post(url, dReqBody, headers).then(response => {
/*
this.$cookie.set('token', response.body.token) this.$cookie.set('token', response.body.token)
this.$cookie.set('refreshToken', response.body.refresh_token) this.$cookie.set('refreshToken', response.body.refresh_token)
this.$cookie.set('role', response.body.role) this.$cookie.set('role', response.body.role)
this.$cookie.set('name', this.name) this.$cookie.set('name', this.name)
*/
this.$store.commit('loggedIn', true) this.$store.commit('loggedIn', true)
this.$store.commit('setName', this.name) this.$store.commit('setName', this.name)
...@@ -85,6 +87,9 @@ export default { ...@@ -85,6 +87,9 @@ export default {
} }
}, },
mounted () { mounted () {
if (this.$store.state.login) {
this.$router.push('/')
}
this.$refs.name.focus() 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 @@ ...@@ -14,10 +14,6 @@
<i>edit</i> <i>edit</i>
<q-tooltip>애플리케이션 수정: {{ cell.row.name }}</q-tooltip> <q-tooltip>애플리케이션 수정: {{ cell.row.name }}</q-tooltip>
</button> </button>
<button class="purple small circular" @click="play(cell.row)">
<i>play_arrow</i>
<q-tooltip>애플리케이션 배포: {{ cell.row.name }}</q-tooltip>
</button>
<button <button
class="purple small circular" class="purple small circular"
@click="confirmDelete(cell.row.name)" @click="confirmDelete(cell.row.name)"
...@@ -26,68 +22,27 @@ ...@@ -26,68 +22,27 @@
<i>delete</i> <i>delete</i>
<q-tooltip>애플리케이션 삭제: {{ cell.row.name }}</q-tooltip> <q-tooltip>애플리케이션 삭제: {{ cell.row.name }}</q-tooltip>
</button> </button>
<button
class="purple small circular"
@click="goToFile(cell.row.name)"
>
<i>folder</i>
<q-tooltip>애플리케이션 파일 배포: {{ cell.row.name }}</q-tooltip>
</button>
<button
class="purple small circular"
@click="processTag(cell.row.name)"
>
<i>bookmark</i>
<q-tooltip>애플리케이션 태그 자동 생성: {{ cell.row.name }}</q-tooltip>
</button>
</template> </template>
</q-data-table> </q-data-table>
<q-modal
ref="playModal" :content-css="{minWidth: '80vw', minHeight: '80vh'}"
>
<q-layout>
<div slot="header" class="toolbar purple">
<button @click="$refs.playModal.close()">
<i>keyboard_arrow_left</i>
</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
>
</textarea>
</div>
<div>
<button class="purple small" @click="playApp()">
배포
</button>
</div>
</div>
</div>
</q-layout>
</q-modal>
</div> </div>
</template> </template>
<script> <script>
import { API_URL } from 'config' import { API_URL, POLLING_INTERVAL } from 'config'
import { Dialog, Toast } from 'quasar' import { Dialog, Toast } from 'quasar'
export default { export default {
...@@ -164,87 +119,59 @@ export default { ...@@ -164,87 +119,59 @@ export default {
goToCreate: function () { goToCreate: function () {
this.$router.push('/app/create') this.$router.push('/app/create')
}, },
toggle: function (section) { goToFile: function (name) {
for (let i in this.sections) { this.$router.push('/app/' + name + '/file')
console.log(i)
}
}, },
play: function (row) { processTag: function (name) {
this.getMachineListBySection() let self = this
this.form.name = row.name Dialog.create({
// Get tag names title: `애플리케이션: ${name}`,
this.tagOptions = [] message: '태그 생성',
for (let i in row.tag) { form: {
this.tagOptions.push({ tag: {
label: row.tag[i], type: 'textbox',
value: row.tag[i] label: '태그'
}) }
} },
this.$refs.playModal.open() buttons: [
}, '취소',
playApp: function () { {
console.log(this.form.name) label: '확인',
console.log(this.form.tag) preventClose: true,
console.log(this.form.machines) handler (data, close) {
const payload = { // Validate data
name: this.form.name, if (!data.tag) {
tag: this.form.tag, Toast.create.negative('태그는 필수 항목입니다.')
machines: this.form.machines return
} }
console.log(payload)
const token = this.$store.state.token const url = API_URL + `/app/${name}/tag`
const headers = { const token = self.$store.state.token
headers: { const headers = {
'Authorization': `Bearer ${token}` headers: {
} 'Authorization': `Bearer ${token}`
} }
let url = API_URL + `/app/${this.form.name}/play` }
this.intervalLogObj = setInterval(this.getLog, 3000) self.$http.patch(url, data, headers).then(response => {
this.$http.post(url, payload, headers).then(response => { // close dialog
Toast.create.positive(`애플리케이션 ${this.form.name} 배포 성공`) close(() => {
this.$refs.playModal.close() Toast.create.positive({
}, response => { html: `애플리케이션 ${name} 태그 생성 완료`,
Toast.create.negative(`애플리케이션 ${this.form.name} 배포 실패`) timeout: 5000
}) })
}, })
getLog: function () { self.loadData()
const token = this.$store.state.token }, response => {
const headers = { let err = `애플리케이션 ${name} 태그 실패`
headers: { Toast.create.negative({
'Authorization': `Bearer ${token}` html: err,
} timeout: 3000
} })
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)
}, response => {
})
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) { processEdit: function (row) {
let self = this let self = this
...@@ -357,32 +284,16 @@ export default { ...@@ -357,32 +284,16 @@ export default {
}, response => { }, response => {
Toast.create.negative('애플리케이션 정보 수신 실패') 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 () { mounted: function () {
if (!this.$store.state.login) { this.$router.push('/') } if (!this.$store.state.login) { this.$router.push('/') }
this.loadData() this.loadData()
this.intervalObj = setInterval(this.loadData, 10 * 1000) this.intervalObj = setInterval(this.loadData, POLLING_INTERVAL)
this.refresh()
this.intervalRefresh = setInterval(this.refresh, 5 * 60 * 1000)
}, },
destroyed: function () { destroyed: function () {
clearInterval(this.intervalObj) clearInterval(this.intervalObj)
clearInterval(this.intervalRefresh)
} }
} }
</script> </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 @@ ...@@ -3,7 +3,7 @@
<h6> <h6>
서버 생성 서버 생성
</h6> </h6>
<p></p> <br>
<form> <form>
<div class="stacked-label"> <div class="stacked-label">
<input <input
...@@ -12,26 +12,21 @@ ...@@ -12,26 +12,21 @@
ref="name" ref="name"
class="full-width" class="full-width"
:class="{'has-error': $v.form.name.$error}" :class="{'has-error': $v.form.name.$error}"
placeholder="Type machine name" placeholder="서버 이름을 입력하세요."
> >
<label>이름</label> <label>이름</label>
<span <span
class="text-negative" class="text-negative"
v-if="!$v.form.name.required"> v-if="!$v.form.name.required">
Field is required 이름은 필수 입력 항목입니다.
</span> </span>
<span <span
class="text-negative" class="text-negative"
v-if="!$v.form.name.minLength"> v-if="!$v.form.name.minLength">
Name must be longer than 3 letters. 이름은 3문자 이상이어야 합니다.
</span>
<span
class="text-negative"
v-if="!$v.form.name.alphaNum">
Name must be alphanumeric or hyphen.
</span> </span>
</div> </div>
<p></p> <br><br>
<div class="stacked-label"> <div class="stacked-label">
<label>종류</label><p></p> <label>종류</label><p></p>
<q-select <q-select
...@@ -41,7 +36,7 @@ ...@@ -41,7 +36,7 @@
:options="machineType"> :options="machineType">
</q-select> </q-select>
</div> </div>
<p></p> <br><br>
<div class="stacked-label"> <div class="stacked-label">
<label>OS</label><p></p> <label>OS</label><p></p>
<q-select <q-select
...@@ -51,7 +46,18 @@ ...@@ -51,7 +46,18 @@
:options="selectOS"> :options="selectOS">
</q-select> </q-select>
</div> </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"> <div class="stacked-label">
<label>MAC주소</label><p></p> <label>MAC주소</label><p></p>
<input <input
...@@ -60,15 +66,15 @@ ...@@ -60,15 +66,15 @@
ref="mac" ref="mac"
class="full-width" class="full-width"
:class="{'has-error': $v.form.mac.$error}" :class="{'has-error': $v.form.mac.$error}"
placeholder="Type machine Mac Address" placeholder="서버 MAC 주소를 입력하세요."
> >
<span <span
class="text-negative" class="text-negative"
v-if="!$v.form.mac.required"> v-if="!$v.form.mac.required">
Field is required MAC 주소는 필수 입력 항목입니다.
</span> </span>
</div> </div>
<p></p> <br><br>
<div class="stacked-label"> <div class="stacked-label">
<label>IP주소</label><p></p> <label>IP주소</label><p></p>
<input <input
...@@ -77,15 +83,15 @@ ...@@ -77,15 +83,15 @@
ref="ip" ref="ip"
class="full-width" class="full-width"
:class="{'has-error': $v.form.ip.$error}" :class="{'has-error': $v.form.ip.$error}"
placeholder="Type machine IP Address" placeholder="서버 IP주소를 입력하세요."
> >
<span <span
class="text-negative" class="text-negative"
v-if="!$v.form.ip.required"> v-if="!$v.form.ip.required">
Field is required IP 주소는 필수 입력 항목입니다.
</span> </span>
</div> </div>
<p></p> <br><br>
<div class="stacked-label"> <div class="stacked-label">
<label>설명</label><p></p> <label>설명</label><p></p>
<input <input
...@@ -93,10 +99,10 @@ ...@@ -93,10 +99,10 @@
v-model="form.desc" v-model="form.desc"
ref="desc" ref="desc"
class="full-width" class="full-width"
placeholder="Type machine description" placeholder="서버 설명을 입력하세요."
> >
</div> </div>
<p></p> <br><br>
<div> <div>
<button class="purple" @click="submit"> <button class="purple" @click="submit">
생성 생성
...@@ -109,7 +115,7 @@ ...@@ -109,7 +115,7 @@
<script> <script>
import { API_URL, selectOS } from 'config' import { API_URL, selectOS } from 'config'
import { Toast } from 'quasar' import { Toast } from 'quasar'
import { required, minLength, alphaNum } from 'vuelidate/lib/validators' import { required, minLength } from 'vuelidate/lib/validators'
export default { export default {
name: 'machineCreate', name: 'machineCreate',
...@@ -119,6 +125,7 @@ export default { ...@@ -119,6 +125,7 @@ export default {
name: '', name: '',
type: '', type: '',
os: '', os: '',
ipmi_ip: '',
mac: '', mac: '',
ip: '', ip: '',
desc: '' desc: ''
...@@ -134,7 +141,6 @@ export default { ...@@ -134,7 +141,6 @@ export default {
form: { form: {
name: { name: {
required, required,
alphaNum,
minLength: minLength(3) minLength: minLength(3)
}, },
mac: { mac: {
...@@ -147,11 +153,11 @@ export default { ...@@ -147,11 +153,11 @@ export default {
}, },
methods: { methods: {
submit: function () { submit: function () {
const url = API_URL + '/machine'
const payload = { const payload = {
name: this.form.name, name: this.form.name,
type: this.form.type, type: this.form.type,
os: this.form.os, os: this.form.os,
ipmi_ip: this.form.ipmi_ip,
mac: this.form.mac, mac: this.form.mac,
ip: this.form.ip, ip: this.form.ip,
desc: this.form.desc, desc: this.form.desc,
...@@ -162,6 +168,7 @@ export default { ...@@ -162,6 +168,7 @@ export default {
return return
} }
const token = this.$store.state.token const token = this.$store.state.token
const url = API_URL + '/machine'
const headers = { const headers = {
headers: { headers: {
'Authorization': `Bearer ${token}` '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 @@ ...@@ -51,14 +51,43 @@
<i>delete</i> <i>delete</i>
<q-tooltip>서버 삭제: {{ cell.row.name }}</q-tooltip> <q-tooltip>서버 삭제: {{ cell.row.name }}</q-tooltip>
</button> </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> </template>
</q-data-table> </q-data-table>
</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.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> </div>
</template> </template>
<script> <script>
import { API_URL, selectOS } from 'config' import { API_URL, selectOS, POLLING_INTERVAL } from 'config'
import { Loading, Dialog, Toast } from 'quasar' import { Loading, Dialog, Toast } from 'quasar'
function show (options) { function show (options) {
...@@ -76,6 +105,8 @@ export default { ...@@ -76,6 +105,8 @@ export default {
data: function () { data: function () {
return { return {
intervalObj: [], intervalObj: [],
machine: '',
consoleUrl: '',
machines: [], machines: [],
config: { config: {
rowHeight: '10px', rowHeight: '10px',
...@@ -114,6 +145,13 @@ export default { ...@@ -114,6 +145,13 @@ export default {
sort: true sort: true
}, },
{ {
label: 'IPMI 주소',
field: 'ipmi_ip',
width: '100px',
filter: true,
sort: false
},
{
label: 'MAC 주소', label: 'MAC 주소',
field: 'mac', field: 'mac',
width: '100px', width: '100px',
...@@ -131,22 +169,16 @@ export default { ...@@ -131,22 +169,16 @@ export default {
label: '상태', label: '상태',
field: 'state', field: 'state',
width: '100px', width: '100px',
filter: true,
sort: true sort: true
}, },
{ {
label: '설치유무', label: '설치유무',
field: 'is_installed', field: 'is_installed',
width: '70px', width: '70px',
filter: true,
sort: 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 } { label: '기능', field: 'functions', width: '150px', sort: false }
] ]
} }
...@@ -170,11 +202,48 @@ export default { ...@@ -170,11 +202,48 @@ export default {
show() show()
this.$http.post(url, {}, headers).then(response => { this.$http.post(url, {}, headers).then(response => {
Loading.hide() Loading.hide()
this.loadData()
Toast.create.positive(`서버 ${row.name} 준비 완료`) Toast.create.positive(`서버 ${row.name} 준비 완료`)
}, response => { }, response => {
Toast.create.negative(`서버 ${row.name} 준비 실패`) 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) { processEdit: function (row) {
let self = this let self = this
Dialog.create({ Dialog.create({
...@@ -215,6 +284,11 @@ export default { ...@@ -215,6 +284,11 @@ export default {
type: 'heading', type: 'heading',
label: '입력 항목' label: '입력 항목'
}, },
ipmi_ip: {
type: 'textbox',
label: 'IPMI IP 주소',
model: row.ipmi_ip ? row.ipmi_ip : ''
},
mac: { mac: {
type: 'textbox', type: 'textbox',
label: 'MAC 주소', label: 'MAC 주소',
...@@ -351,32 +425,16 @@ export default { ...@@ -351,32 +425,16 @@ export default {
}, response => { }, response => {
Toast.create.negative('서버 정보 수신 실패') 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 () { mounted: function () {
if (!this.$store.state.login) { this.$router.push('/') } if (!this.$store.state.login) { this.$router.push('/') }
this.loadData() this.loadData()
this.intervalObj = setInterval(this.loadData, 10 * 1000) this.intervalObj = setInterval(this.loadData, POLLING_INTERVAL)
this.refresh()
this.intervalRefresh = setInterval(this.refresh, 5 * 60 * 1000)
}, },
destroyed: function () { destroyed: function () {
clearInterval(this.intervalObj) clearInterval(this.intervalObj)
clearInterval(this.intervalRefresh)
} }
} }
</script> </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 @@ ...@@ -35,7 +35,7 @@
</template> </template>
<script> <script>
import { API_URL } from 'config' import { API_URL, POLLING_INTERVAL } from 'config'
import { Dialog, Toast } from 'quasar' import { Dialog, Toast } from 'quasar'
export default { export default {
...@@ -44,7 +44,6 @@ export default { ...@@ -44,7 +44,6 @@ export default {
return { return {
role: this.$store.state.role, role: this.$store.state.role,
intervalObj: [], intervalObj: [],
intervalRefresh: [],
sections: [], sections: [],
config: { config: {
title: '그룹 목록', title: '그룹 목록',
...@@ -211,22 +210,7 @@ export default { ...@@ -211,22 +210,7 @@ export default {
this.$http.get(url, headers).then(response => { this.$http.get(url, headers).then(response => {
this.sections = response.body this.sections = response.body
}, response => { }, response => {
if (response.status === 401) { Toast.create.negative('서버 정보 수신 실패')
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 => {
}) })
} }
}, },
...@@ -234,13 +218,10 @@ export default { ...@@ -234,13 +218,10 @@ export default {
if (!this.$store.state.login) { this.$router.push('/') } if (!this.$store.state.login) { this.$router.push('/') }
this.loadData() this.loadData()
this.intervalObj = setInterval(this.loadData, 10 * 1000) this.intervalObj = setInterval(this.loadData, POLLING_INTERVAL)
this.refresh()
this.intervalRefresh = setInterval(this.refresh, 5 * 60 * 1000)
}, },
destroyed: function () { destroyed: function () {
clearInterval(this.intervalObj) clearInterval(this.intervalObj)
clearInterval(this.intervalRefresh)
} }
} }
</script> </script>
......
...@@ -67,6 +67,16 @@ export default new VueRouter({ ...@@ -67,6 +67,16 @@ export default new VueRouter({
component: load('app/Create') 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', path: '/account',
name: 'account', name: 'account',
component: load('account/List') component: load('account/List')
...@@ -87,11 +97,36 @@ export default new VueRouter({ ...@@ -87,11 +97,36 @@ export default new VueRouter({
component: load('account/Pass') component: load('account/Pass')
}, },
{ {
path: '/account/:name/userpass',
name: 'accountUserPassword',
component: load('account/UserPass')
},
{
path: '/os', path: '/os',
name: 'os', name: 'os',
component: load('os/List') 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: '/', path: '/',
name: 'index', name: 'index',
component: load('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