diff --git a/.gitignore b/.gitignore index 5bfa7f30..313eb8e8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ LXDUI.egg-info logs/ conf/lxdui.conf snap/ +tmp/ diff --git a/app/__metadata__.py b/app/__metadata__.py index 049d99dc..bd08be5e 100644 --- a/app/__metadata__.py +++ b/app/__metadata__.py @@ -1,6 +1,6 @@ APP_NAME = 'LXDUI' APP_CLI_CMD = 'lxdui' -VERSION = '2.1.2' +VERSION = '2.2' GIT_URL = 'https://github.com/AdaptiveScale/lxdui.git' LXD_URL = 'http://localhost:8443' LICENSE = 'Apache 2.0' @@ -8,11 +8,12 @@ AUTHOR_URL = 'http://www.adaptivescale.com' AUTHOR_EMAIL = 'info@adaptivescale.com' KEYWORDS = 'lxc lxc-containers lxd' +IMAGE_HUB = 'http://hub.kuti.io' ''' -The following section is for the default configuration -that will be written to the lxdui.conf file if the file +The following section is for the default configuration +that will be written to the lxdui.conf file if the file does not already exist. ''' @@ -20,11 +21,13 @@ DEFAULT_CONFIG_FORMAT = 'ini' __default_config__ = """ [LXDUI] +lxdui.host = 0.0.0.0 lxdui.port = 15151 lxdui.images.remote = https://images.linuxcontainers.org +#lxdui.lxd.remote = https://lxd.host.org:8443/ +#lxdui.lxd.sslverify = true +#lxdui.lxd.remote.name = host lxdui.jwt.token.expiration = 1200 -lxdui.jwt.secret.key = AC8d83&21Almnis710sds -lxdui.jwt.auth.url.rule = /api/user/login lxdui.admin.user = admin lxdui.conf.dir = {{app_root}}/conf lxdui.conf.file = ${lxdui.conf.dir}/lxdui.conf @@ -41,7 +44,8 @@ lxdui.zfs.pool.name = lxdpool lxdui.app.alias = LXDUI lxdui.cli = cli - +lxdui.set_limits_cpu = false + [LXDUI_CERT] lxdui.cert.country = US lxdui.cert.state = Texas diff --git a/app/api/controllers/auth.py b/app/api/controllers/auth.py new file mode 100644 index 00000000..b79f7c6c --- /dev/null +++ b/app/api/controllers/auth.py @@ -0,0 +1,23 @@ +from flask import Blueprint, request, jsonify +from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required +from app.api.utils.authentication import authenticate + +auth_api = Blueprint('auth_api', __name__) + +@auth_api.route('/login', methods=['POST']) +def login(): + username = request.get_json()["username"] + password = request.get_json()["password"] + auth_resp = authenticate(username,password) + if auth_resp == False: + return jsonify('{description: "Invalid credentials", error: "Bad Request", status_code: 401}'), 401 + else: + access_token = create_access_token(identity = username) + return '{ "access_token": "'+access_token+'" }' + +@auth_api.route('/refresh', methods=['POST']) +@jwt_required() +def refresh(): + current_user = get_jwt_identity() + access_token = create_access_token(identity=current_user) + return '{ "access_token": "'+access_token+'" }' diff --git a/app/api/controllers/container.py b/app/api/controllers/container.py index c56b7291..6bd5c624 100644 --- a/app/api/controllers/container.py +++ b/app/api/controllers/container.py @@ -1,5 +1,5 @@ from flask import Blueprint, request -from flask_jwt import jwt_required +from flask_jwt_extended import jwt_required from app.api.schemas.container_schema import doValidate, doValidateCloneMove, doValidateImageExport from app.api.models.LXCContainer import LXCContainer @@ -37,7 +37,7 @@ def getContainer(name): @jwt_required() def createContainer(): input = request.get_json(silent=True) - validation = doValidate(input) + validation = doValidate(input, LXDModule().setLimitsCPU()) if validation: return response.reply(message=validation.message, status=403) @@ -56,7 +56,7 @@ def createContainer(): @jwt_required() def updateContainer(): input = request.get_json(silent=True) - validation = doValidate(input) + validation = doValidate(input, LXDModule().setLimitsCPU()) if validation: return response.reply(message=validation.message, status=403) @@ -222,4 +222,4 @@ def removeProxy(name, proxy): container = LXCContainer({'name': name}) return response.replySuccess(container.removeProxy(proxy)) except ValueError as e: - return response.replyFailed(message=e.__str__()) \ No newline at end of file + return response.replyFailed(message=e.__str__()) diff --git a/app/api/controllers/fileManager.py b/app/api/controllers/fileManager.py index 55778173..ef32c4d3 100644 --- a/app/api/controllers/fileManager.py +++ b/app/api/controllers/fileManager.py @@ -1,10 +1,9 @@ from flask import Blueprint, request, send_file from flask import jsonify -from flask_jwt import jwt_required +from flask_jwt_extended import jwt_required from app.api.models.LXCFileManager import LXCFileManager from app.api.utils import response -from app.api.utils.authentication import jwt_decode_handler import io import json @@ -113,12 +112,9 @@ def download(name): @file_manager_api.route('/download/container/', methods=['GET']) +@jwt_required() def download_file(name): path = request.args.get('path') - token = request.args.get('token') - print (token) - if not checkAuthentication(token): - return response.replyFailed('Not authorized') if path == None: return jsonify([]) @@ -210,9 +206,3 @@ def delete_profile(name): except ValueError as ex: return response.replyFailed(message=ex.__str__()) - -def checkAuthentication(token): - try: - return jwt_decode_handler(token) - except Exception as e: - return False diff --git a/app/api/controllers/image.py b/app/api/controllers/image.py index 1609235e..f204dd11 100644 --- a/app/api/controllers/image.py +++ b/app/api/controllers/image.py @@ -1,5 +1,5 @@ from flask import Blueprint, request, jsonify -from flask_jwt import jwt_required +from flask_jwt_extended import jwt_required from app.api.models.LXCImage import LXCImage from app.api.models.LXDModule import LXDModule @@ -84,4 +84,36 @@ def downloadImage(): client = LXDModule() return response.replySuccess(client.downloadImage(input.get('image')), message='Image {} downloaded successfully.'.format(input.get('image'))) except ValueError as e: - return response.replyFailed(message=e.__str__()) \ No newline at end of file + return response.replyFailed(message=e.__str__()) + + +import json +@image_api.route('/hub/publish', methods=['POST']) +@jwt_required() +def publishHubImage(): + #input = request.get_json(silent=True) + input = json.loads(request.form.get('input')) + logo = request.files['logo'] + input['logo'] = logo.filename + try: + client = LXCImage(input) + client.exportImage(input, logo) + client.pushImage(input) + return response.replySuccess(message='Image {} pushed successfully.'.format(input.get('fingerprint'))) + except ValueError as e: + return response.replyFailed(message=e.__str__()) + + +@image_api.route('/hub', methods=['POST']) +@jwt_required() +def downloadHubImage(): + input = request.get_json(silent=True) + validation = doValidate(input) + if validation: + return response.replyFailed(message=validation.message) + input['fingerprint'] = input.get('image') + try: + client = LXCImage(input) + return response.replySuccess(client.importImage(input), message='Image {} downloaded successfully.'.format(input.get('fingerprint'))) + except ValueError as e: + return response.replyFailed(message=e.__str__()) diff --git a/app/api/controllers/imageRegistry.py b/app/api/controllers/imageRegistry.py new file mode 100644 index 00000000..03a4d60d --- /dev/null +++ b/app/api/controllers/imageRegistry.py @@ -0,0 +1,29 @@ +from flask import Blueprint, request, send_file +from flask_jwt_extended import jwt_required + +from app.api.models.LXCImage import LXCImage +from app.api.utils import response + +from app.api.schemas.publishImageSchema import doValidate + +image_registry_api = Blueprint('image_registry_api', __name__) + + +@image_registry_api.route('/', methods=['POST']) +@jwt_required() +def publishImage(fingerprint): + input = request.get_json(silent=True) + validation = doValidate(input) + if validation: + return response.replyFailed(message=validation.message) + + input['fingerprint'] = fingerprint + try: + image = LXCImage({'fingerprint': fingerprint}) + + #Export Image - Image registry + image.exportImage(input) + + return response.replySuccess(image.getImage()) + except ValueError as e: + return response.replyFailed(message=e.__str__()) diff --git a/app/api/controllers/lxd.py b/app/api/controllers/lxd.py index 5b5ca1cb..1e665c21 100644 --- a/app/api/controllers/lxd.py +++ b/app/api/controllers/lxd.py @@ -1,5 +1,5 @@ from flask import Blueprint, jsonify -from flask_jwt import jwt_required +from flask_jwt_extended import jwt_required from app.api.models.LXDModule import LXDModule from app.api.utils import response @@ -33,4 +33,4 @@ def config(): client = LXDModule() return response.replySuccess(client.config()) except ValueError as e: - return response.replyFailed(message=e.__str__()) \ No newline at end of file + return response.replyFailed(message=e.__str__()) diff --git a/app/api/controllers/network.py b/app/api/controllers/network.py index 671d1ead..d5ba324f 100644 --- a/app/api/controllers/network.py +++ b/app/api/controllers/network.py @@ -1,5 +1,5 @@ from flask import Blueprint, request -from flask_jwt import jwt_required +from flask_jwt_extended import jwt_required from app.api.models.LXDModule import LXDModule from app.api.models.LXCContainer import LXCContainer @@ -36,10 +36,11 @@ def updateNetwork(name): if validation: return response.replyFailed(message=validation.message) + input['name'] = name input['IPv6_ENABLED'] = False - network = LXCNetwork({'name': name}) - network.updateNetwork(input, name) + network = LXCNetwork(input) + network.updateNetwork() mainConfig = network.info() for container in mainConfig['used_by']: @@ -50,13 +51,14 @@ def updateNetwork(name): @jwt_required() def creatNetwork(name): input = request.get_json(silent=True) + input['name'] = name validation = doValidate(input) if validation: return response.replyFailed(message=validation.message) input['IPv6_ENABLED'] = False - network = LXCNetwork({'name': name}) - network.createNetwork(input, name) + network = LXCNetwork(input) + network.createNetwork() mainConfig = network.info() return response.replySuccess(mainConfig['result'], message='Network {} created successfully.'.format(name)) @@ -68,4 +70,4 @@ def deleteNetwork(name): network.deleteNetwork() client = LXDModule() - return response.replySuccess(client.listNetworks(), message='Network {} deleted successfully.'.format(name)) \ No newline at end of file + return response.replySuccess(client.listNetworks(), message='Network {} deleted successfully.'.format(name)) diff --git a/app/api/controllers/profile.py b/app/api/controllers/profile.py index d04d1e5e..074722cd 100644 --- a/app/api/controllers/profile.py +++ b/app/api/controllers/profile.py @@ -1,6 +1,6 @@ from flask import Blueprint, request from flask import jsonify -from flask_jwt import jwt_required +from flask_jwt_extended import jwt_required from app.api.models.LXDModule import LXDModule from app.api.models.LXCProfile import LXCProfile diff --git a/app/api/controllers/snapshot.py b/app/api/controllers/snapshot.py index ec92fcc9..35e0e94c 100644 --- a/app/api/controllers/snapshot.py +++ b/app/api/controllers/snapshot.py @@ -1,5 +1,5 @@ from flask import Blueprint, request -from flask_jwt import jwt_required +from flask_jwt_extended import jwt_required from app.api.schemas.container_schema import doValidateCloneMove from app.api.models.LXCSnapshot import LXCSnapshot diff --git a/app/api/controllers/storagePool.py b/app/api/controllers/storagePool.py index 40fd094c..40ab64ae 100644 --- a/app/api/controllers/storagePool.py +++ b/app/api/controllers/storagePool.py @@ -1,6 +1,5 @@ from flask import Blueprint, request -from flask import jsonify -from flask_jwt import jwt_required +from flask_jwt_extended import jwt_required from app.api.models.LXDModule import LXDModule from app.api.models.LXCStoragePool import LXCStoragePool diff --git a/app/api/controllers/terminal.py b/app/api/controllers/terminal.py index dbec7aa4..2cb3b50a 100644 --- a/app/api/controllers/terminal.py +++ b/app/api/controllers/terminal.py @@ -12,52 +12,73 @@ from app.api.models.LXCContainer import LXCContainer from app.lib.termmanager import NamedTermManager from app.api.utils import mappings +from app.lib.conf import Config +from app import __metadata__ as meta + TEMPLATE_DIR = os.path.dirname(__file__).replace('/api/controllers','/ui/templates/') STATIC_DIR = os.path.dirname(__file__).replace('/api/controllers','/ui/static/') -from app.api.utils.authentication import jwt_decode_handler +from flask_jwt_extended import decode_token def findShellTypeOfContainer(container): - containerImage = container.info()['config']['image.os'].lower() - for image in mappings.OS_SHELL_MAPPINGS: - if image in containerImage: - return mappings.OS_SHELL_MAPPINGS[image] + containerImage = container.info()['config'].get('image.os') + if containerImage: + for image in mappings.OS_SHELL_MAPPINGS: + if image in containerImage.lower(): + return mappings.OS_SHELL_MAPPINGS[image] return 'bash' -def checkAuthentication(token): - try: - return jwt_decode_handler(token) - except Exception as e: - return False - class TerminalPageHandler(tornado.web.RequestHandler): """Render the /ttyX pages""" def get(self, term_name, token): - if not checkAuthentication(token): + token = self.get_cookie("access_token_cookie") + if token == None: + raise tornado.web.HTTPError(403) + with self.app.app_context(): + try: + decode_token(token.encode()) + except: raise tornado.web.HTTPError(403) return self.render("termpage.html",static=self.static_url, xstatic=self.application.settings['xstatic_url'], ws_url_path="/_websocket/"+term_name) + def initialize(self,app): + self.app = app + class NewTerminalHandler(tornado.web.RequestHandler): """Redirect to an unused terminal name""" def get(self, name='new', token=None): - if not checkAuthentication(token): + token = self.get_cookie("access_token_cookie") + if token == None: + raise tornado.web.HTTPError(403) + with self.app.app_context(): + try: + decode_token(token.encode()) + except: raise tornado.web.HTTPError(403) shellType = findShellTypeOfContainer(LXCContainer({'name': name})) - shell = ['bash', '-c', 'lxc exec {} -- /bin/{}'.format(name, shellType)] + try: + hostName = Config().get(meta.APP_NAME,'lxdui.lxd.remote.name') + shell = ['bash', '-c', 'lxc exec {}:{} -- /bin/{}'.format(hostName, name, shellType)] + except: + shell = ['bash', '-c', 'lxc exec {} -- /bin/{}'.format(name, shellType)] + name, terminal = self.application.settings['term_manager'].new_named_terminal(shell_command=shell) - self.redirect("/terminal/open/" + name+'/'+token, permanent=False) + self.redirect("/terminal/open/" + name+'/', permanent=False) + def initialize(self,app): + self.app = app + -def terminal(app, port, debug=False): +def terminal(app, host, port, debug=False): term_manager = NamedTermManager(shell_command=None, max_terminals=100) wrapped_app = WSGIContainer(app) handlers = [ (r"/_websocket/(\w+)", TermSocket, {'term_manager': term_manager}), - (r"/terminal/new/([a-zA-Z\-0-9\.]+)/(.*)/?", NewTerminalHandler), - (r"/terminal/open/([a-zA-Z\-0-9\.]+)/(.*)/?", TerminalPageHandler), + (r"/terminal/new/([a-zA-Z\-0-9\.]+)/(.*)/?", NewTerminalHandler, {'app': app}), + (r"/terminal/open/([a-zA-Z\-0-9\.]+)/(.*)/?", TerminalPageHandler, {'app': app}), (r"/xstatic/(.*)", tornado_xstatic.XStaticFileHandler), ("/(.*)", tornado.web.FallbackHandler, {'fallback': wrapped_app}), ] @@ -68,5 +89,5 @@ def terminal(app, port, debug=False): term_manager=term_manager, debug=debug) http_server = HTTPServer(tornado_app) - http_server.listen(port, '0.0.0.0') - IOLoop.instance().start() \ No newline at end of file + http_server.listen(port, host) + IOLoop.instance().start() diff --git a/app/api/core.py b/app/api/core.py index b0cf5401..9bf9f7b6 100644 --- a/app/api/core.py +++ b/app/api/core.py @@ -19,6 +19,9 @@ # Authentication section initAuth(app) +from app.api.controllers.auth import auth_api +app.register_blueprint(auth_api, url_prefix='/api/user') + from app.api.controllers.lxd import lxd_api app.register_blueprint(lxd_api, url_prefix='/api/lxd') @@ -43,6 +46,9 @@ from app.api.controllers.storagePool import storage_pool_api app.register_blueprint(storage_pool_api, url_prefix='/api/storage_pool') +from app.api.controllers.imageRegistry import image_registry_api +app.register_blueprint(image_registry_api, url_prefix='/api/image_registry') + from app.api.controllers.terminal import terminal @app.route('/') @@ -112,7 +118,7 @@ def stop(): logging.info(e) -def start(port, debug=False, uiPages=None): +def start(host, port, debug=False, uiPages=None): logging.debug('Checking UI availability.') if uiPages is not None: @@ -130,7 +136,7 @@ def start(port, debug=False, uiPages=None): with open(PID, 'w') as f: f.write(str(pid)) - print("LXDUI started. Running on http://0.0.0.0:{}".format(port)) + print("LXDUI started. Running on http://{}:{}".format(host, port)) print("PID={}, Press CTRL+C to quit".format(pid)) - terminal(app, port, debug) - # app.run(debug=debug, host='0.0.0.0', port=port) \ No newline at end of file + terminal(app, host, port, debug) + # app.run(debug=debug, host='0.0.0.0', port=port) diff --git a/app/api/models/LXCContainer.py b/app/api/models/LXCContainer.py index a3798eb5..509200fb 100644 --- a/app/api/models/LXCContainer.py +++ b/app/api/models/LXCContainer.py @@ -16,9 +16,9 @@ def __init__(self, input): logging.info('Connecting to LXD') super(LXCContainer, self).__init__(remoteHost=self.remoteHost) - if self.client.containers.exists(self.data.get('name')): + if self.client.instances.exists(self.data.get('name')): existing = self.info() - self.data['config'] = existing['config']; + self.data['config'] = existing['config'] self.data['devices'] = existing['devices'] if input.get('image'): @@ -61,7 +61,8 @@ def __init__(self, input): if input.get('config'): self.setConfig(input.get('config')) - + if input.get('devices'): + self.setDevices(input.get('devices')) def setImageType(self, input): # Detect image type (alias or fingerprint) @@ -99,15 +100,17 @@ def initConfig(self): def setCPU(self, input): self.initConfig() - if input.get('count'): - logging.debug('Set CPU count to {}'.format(input.get('count'))) - self.data['config']['limits.cpu']=input.get('count') - if input.get('percentage'): - if input.get('hardLimitation'): - self.data['config']['limits.cpu.allowance']='{}ms/100ms'.format(input.get('percentage')) - else: - self.data['config']['limits.cpu.allowance'] = '{}%'.format(input.get('percentage')) - logging.debug('CPU allowance limit set to {}'.format(self.data['config']['limits.cpu.allowance'])) + if LXDModule().setLimitsCPU(): + if input.get('cores'): + logging.debug('Set CPU count to {}'.format(input.get('cores'))) + self.data['config']['limits.cpu']='{}'.format(input.get('cores')) + else: + if input.get('percentage'): + if input.get('hardLimitation'): + self.data['config']['limits.cpu.allowance']='{}ms/100ms'.format(input.get('percentage')) + else: + self.data['config']['limits.cpu.allowance'] = '{}%'.format(input.get('percentage')) + logging.debug('CPU allowance limit set to {}'.format(self.data['config']['limits.cpu.allowance'])) def setMemory(self, input): self.initConfig() @@ -121,6 +124,9 @@ def setMemory(self, input): def setNewContainer(self, input): self.data['newContainer'] = input + def setDevices(self, input): + self.data['devices'] = input + def setImageAlias(self, input): logging.debug('Setting image alias as {}'.format(input)) self.data['imageAlias'] = input @@ -149,9 +155,9 @@ def setConfig(self, input): def info(self): try: logging.info('Reading container {} information'.format(self.data.get('name'))) - c = self.client.containers.get(self.data.get('name')) + c = self.client.instances.get(self.data.get('name')) - container = self.client.api.containers[self.data.get('name')].get().json()['metadata'] + container = self.client.api.instances[self.data.get('name')].get().json()['metadata'] container['cpu'] = c.state().cpu container['memory'] = c.state().memory container['network'] = c.state().network @@ -167,8 +173,15 @@ def info(self): def create(self, waitIt=True): try: + instanceType = '' + for image in LXDModule().listLocalImages(): + if(image["fingerprint"] == self.data['source']['fingerprint']): + instanceType = image["type"] + break + logging.info('Creating container {}'.format(self.data.get('name'))) - self.client.containers.create(self.data, wait=waitIt) + self.data['type'] = instanceType + self.client.instances.create(self.data, wait=waitIt) if self.data['config']['boot.autostart'] == '1': self.start(waitIt) return self.info() @@ -180,7 +193,7 @@ def create(self, waitIt=True): def delete(self, force=False): try: logging.info('Deleting container with {} enforcement set to {}'.format(self.data.get('name'), force)) - container = self.client.containers.get(self.data.get('name')) + container = self.client.instances.get(self.data.get('name')) if self.info().get('ephemeral'): container.stop(wait=True) return @@ -195,7 +208,7 @@ def delete(self, force=False): def update(self): try: logging.info('Updating container {}'.format(self.data.get('name'))) - container = self.client.containers.get(self.data.get('name')) + container = self.client.instances.get(self.data.get('name')) if self.data.get('config'): container.config = self.data.get('config') @@ -217,7 +230,7 @@ def update(self): def start(self, waitIt=True): try: logging.info('Starting container {}'.format(self.data.get('name'))) - container = self.client.containers.get(self.data.get('name')) + container = self.client.instances.get(self.data.get('name')) container.start(wait=waitIt) except Exception as e: logging.error('Failed to start container {}'.format(self.data.get('name'))) @@ -227,7 +240,7 @@ def start(self, waitIt=True): def stop(self, waitIt=True): try: logging.info('Stopping container {}'.format(self.data.get('name'))) - container = self.client.containers.get(self.data.get('name')) + container = self.client.instances.get(self.data.get('name')) container.stop(wait=waitIt) except Exception as e: logging.error('Failed to stop container {}'.format(self.data.get('name'))) @@ -237,20 +250,17 @@ def stop(self, waitIt=True): def restart(self, waitIt=True): try: logging.info('Restarting container {}'.format(self.data.get('name'))) - container = self.client.containers.get(self.data.get('name')) + container = self.client.instances.get(self.data.get('name')) container.restart(wait=waitIt) except Exception as e: logging.error('Failed to restart container {}'.format(self.data.get('name'))) logging.exception(e) raise ValueError(e) - def move(self): - pass - def clone(self): try: logging.info('Cloning container {}'.format(self.data.get('name'))) - container = self.client.containers.get(self.data.get('name')) + container = self.client.instances.get(self.data.get('name')) if container.status == 'Running': container.stop(wait=True) @@ -258,10 +268,10 @@ def clone(self): copyData['source'] = {'type': 'copy', 'source': self.data.get('name')} copyData['name'] = self.data.get('newContainer') - newContainer = self.client.containers.create(copyData, wait=True) + newContainer = self.client.instances.create(copyData, wait=True) container.start(wait=True) newContainer.start(wait=True) - return self.client.api.containers[self.data.get('newContainer')].get().json()['metadata'] + return self.client.api.instances[self.data.get('newContainer')].get().json()['metadata'] except Exception as e: logging.error('Failed to clone container {}'.format(self.data.get('name'))) logging.exception(e) @@ -270,7 +280,7 @@ def clone(self): def move(self): try: logging.info('Moving container {}'.format(self.data.get('name'))) - container = self.client.containers.get(self.data.get('name')) + container = self.client.instances.get(self.data.get('name')) if container.status == 'Running': container.stop(wait=True) @@ -278,11 +288,11 @@ def move(self): copyData['source'] = {'type': 'copy', 'source': self.data.get('name')} copyData['name'] = self.data.get('newContainer') - newContainer = self.client.containers.create(copyData, wait=True) + newContainer = self.client.instances.create(copyData, wait=True) newContainer.start(wait=True) container.delete(wait=True) - return self.client.api.containers[self.data.get('newContainer')].get().json()['metadata'] + return self.client.api.instances[self.data.get('newContainer')].get().json()['metadata'] except Exception as e: logging.error('Failed to move container {}'.format(self.data.get('name'))) logging.exception(e) @@ -292,12 +302,17 @@ def move(self): def export(self, force=False): try: logging.info('Exporting container {}'.format(self.data.get('name'))) - container = self.client.containers.get(self.data.get('name')) + container = self.client.instances.get(self.data.get('name')) if force and container.status == 'Running': container.stop(wait=True) image = container.publish(wait=True) image.add_alias(self.data.get('imageAlias'), self.data.get('name')) + try: + fingerprint = container.config.get('volatile.base_image') + self.client.api.images[image.fingerprint].put(json={'properties': self.client.api.images[fingerprint].get().json()['metadata']['properties']}) + except: + logging.error('Image does not exist.') container.start(wait=True) return self.client.api.images[image.fingerprint].get().json()['metadata'] except Exception as e: @@ -311,7 +326,7 @@ def rename(self, force=True): if self.data.get('newName'): if self.containerExists(self.data.get('newName')): raise ValueError('Container with that name already exists') - container = self.client.containers.get(self.data.get('name')) + container = self.client.instances.get(self.data.get('name')) previousState = container.status if previousState == 'Running': if force == False: @@ -331,7 +346,7 @@ def rename(self, force=True): def freeze(self, waitIt=True): try: logging.info('Freezing container {}'.format(self.data.get('name'))) - container = self.client.containers.get(self.data.get('name')) + container = self.client.instances.get(self.data.get('name')) container.freeze(wait=waitIt) except Exception as e: logging.error('Failed to freeze container {}'.format(self.data.get('name'))) @@ -342,7 +357,7 @@ def freeze(self, waitIt=True): def unfreeze(self, waitIt=True): try: logging.info('Unfreezing container {}'.format(self.data.get('name'))) - container = self.client.containers.get(self.data.get('name')) + container = self.client.instances.get(self.data.get('name')) container.unfreeze(wait=waitIt) except Exception as e: logging.error('Failed to unfreeze container {}'.format(self.data.get('name'))) @@ -357,7 +372,7 @@ def addNetwork(self, network): self.initNetwork() self.data['devices'][network['name']]=network try: - container = self.client.containers.get(self.data['name']) + container = self.client.instances.get(self.data['name']) container.devices = self.data['devices'] container.save() return self.info() @@ -368,7 +383,7 @@ def removeNetwork(self, networkName): self.initNetwork() del self.data['devices'][networkName] try: - container = self.client.containers.get(self.data['name']) + container = self.client.instances.get(self.data['name']) container.devices = self.data['devices'] container.save() return self.info() @@ -379,7 +394,7 @@ def addProxy(self, name, proxy): self.initNetwork() self.data['devices'][name] = proxy try: - container = self.client.containers.get(self.data['name']) + container = self.client.instances.get(self.data['name']) container.devices = self.data['devices'] container.save() return self.info() @@ -390,7 +405,7 @@ def removeProxy(self, name): self.initNetwork() del self.data['devices'][name] try: - container = self.client.containers.get(self.data['name']) + container = self.client.instances.get(self.data['name']) container.devices = self.data['devices'] container.save() return self.info() diff --git a/app/api/models/LXCFileManager.py b/app/api/models/LXCFileManager.py index 0fe7f0e2..cb5bc5b3 100644 --- a/app/api/models/LXCFileManager.py +++ b/app/api/models/LXCFileManager.py @@ -8,7 +8,7 @@ class LXCFileManager(LXDModule): def __init__(self, input): logging.info('Connecting to LXD') - self.client = Client() + super().__init__() self.input = input def list(self): @@ -17,7 +17,7 @@ def list(self): def download(self): try: logging.info('Download file {} from container {}'.format(self.input.get('path'), self.input.get('name'))) - container = self.client.containers.get(self.input.get('name')) + container = self.client.instances.get(self.input.get('name')) file = container.files.get(self.input.get('path')) return file except Exception as e: @@ -28,7 +28,7 @@ def download(self): def push(self): try: logging.info('Push file {} to container {}'.format(self.input.get('path'), self.input.get('name'))) - container = self.client.containers.get(self.input.get('name')) + container = self.client.instances.get(self.input.get('name')) return container.files.put(self.input.get('path'), self.input.get('file')) except Exception as e: logging.error('Push file {} to container {} failed.'.format(self.input.get('path'), self.input.get('name'))) @@ -39,7 +39,7 @@ def push(self): def delete(self): try: logging.info('Delete file {} from container {}'.format(self.input.get('path'), self.input.get('name'))) - container = self.client.containers.get(self.input.get('name')) + container = self.client.instances.get(self.input.get('name')) return container.files.delete(self.input.get('path')) except Exception as e: logging.error('Delete file {} from container {} failed.'.format(self.input.get('path'), self.input.get('name'))) diff --git a/app/api/models/LXCImage.py b/app/api/models/LXCImage.py index 85f8ee49..f4dd6175 100644 --- a/app/api/models/LXCImage.py +++ b/app/api/models/LXCImage.py @@ -1,5 +1,14 @@ from app.api.models.LXDModule import LXDModule +from app.lib.conf import MetaConf +from app.api.utils.firebaseAuthentication import firebaseLogin +from app import __metadata__ as meta import logging +import requests +import subprocess +import shutil +import os +import yaml +import tarfile logging = logging.getLogger(__name__) @@ -53,4 +62,227 @@ def deleteImage(self): except Exception as e: logging.error('Failed to delete the image {}'.format(self.data.get('fingerprint'))) logging.exception(e) + raise ValueError(e) + + #TODO Refactor this part + def exportImage(self, input, logo=None): + try: + #Check if image exists & Update the fingerprint with the full fingerprint + self.data['fingerprint'] = self.client.images.get(self.data.get('fingerprint')).fingerprint + + logging.info('Exporting image {}'.format(self.data.get('fingerprint'))) + p2 = subprocess.Popen(["lxc", "image", "export", self.data.get('fingerprint')], stdout=subprocess.PIPE) + output_rez = p2.stdout.read() + + #Make dir for the export + shutil.rmtree('tmp/images/{}/'.format(self.data.get('fingerprint')), ignore_errors=True) + os.makedirs('tmp/images/{}'.format(self.data.get('fingerprint')), exist_ok=True) + + #Move the export - Check for both extenstion .tar.gz & .tar.xz + if os.path.exists('{}.tar.gz'.format(self.data.get('fingerprint'))): + shutil.move('{}.tar.gz'.format(self.data.get('fingerprint')), 'tmp/images/{}/'.format(self.data.get('fingerprint'))) + input['image'] = '{}.tar.gz'.format(self.data.get('fingerprint')) + if os.path.exists('{}.tar.xz'.format(self.data.get('fingerprint'))): + shutil.move('{}.tar.xz'.format(self.data.get('fingerprint')), 'tmp/images/{}/'.format(self.data.get('fingerprint'))) + input['image'] = '{}.tar.xz'.format(self.data.get('fingerprint')) + if os.path.exists('{}.squashfs'.format(self.data.get('fingerprint'))): + shutil.move('{}.squashfs'.format(self.data.get('fingerprint')), 'tmp/images/{}/'.format(self.data.get('fingerprint'))) + input['image'] = '{}.squashfs'.format(self.data.get('fingerprint')) + if os.path.exists('meta-{}.tar.gz'.format(self.data.get('fingerprint'))): + shutil.move('meta-{}.tar.gz'.format(self.data.get('fingerprint')), 'tmp/images/{}/'.format(self.data.get('fingerprint'))) + input['metadata'] = 'meta-{}.tar.gz'.format(self.data.get('fingerprint')) + if os.path.exists('meta-{}.tar.xz'.format(self.data.get('fingerprint'))): + shutil.move('meta-{}.tar.xz'.format(self.data.get('fingerprint')), 'tmp/images/{}/'.format(self.data.get('fingerprint'))) + input['metadata'] = 'meta-{}.tar.xz'.format(self.data.get('fingerprint')) + if os.path.exists('meta-{}.tar.xz'.format(self.data.get('fingerprint'))): + shutil.move('meta-{}.squashfs'.format(self.data.get('fingerprint')), 'tmp/images/{}/'.format(self.data.get('fingerprint'))) + input['metadata'] = 'meta-{}.squashfs'.format(self.data.get('fingerprint')) + + #Prepare & Move the yaml file + self.prepareImageYAML(input) + shutil.move('image.yaml', 'tmp/images/{}/'.format(self.data.get('fingerprint'))) + + #TODO Prepare README.md + file = open('tmp/images/{}/README.md'.format(self.data.get('fingerprint')), 'a') + file.write('#README\n') + file.write(input.get('documentation')) + file.close() + + #TODO Prepare Logo + if logo: + logo.save('tmp/images/{}/{}'.format(self.data.get('fingerprint'), 'logo.png')) + + return MetaConf().getConfRoot() + '/tmp/images/{}'.format(self.data.get('fingerprint')) + except Exception as e: + logging.error('Failed to export the image {}'.format(self.data.get('fingerprint'))) + logging.exception(e) + raise ValueError(e) + + def prepareImageYAML(self, input): + if input.get('metadata') == None: input['metadata'] = '' + data = { + 'title': input.get('imageAlias', ''), + 'description': input.get('imageDescription', ''), + 'author': { + 'name': input.get('authorName', ''), + 'alias': '', + 'email': input.get('authorEmail', '') + }, + 'license': input.get('license', ''), + 'readme': 'README.md', + 'tags': input.get('imageTags').split(','), + 'logo': 'logo.png', + 'image': input.get('image'), + 'metadata': input.get('metadata'), + 'fingerprint': self.data.get('fingerprint'), + 'public': True + } + + data.update(self.client.api.images[self.data.get('fingerprint')].get().json()['metadata']) + + with open('image.yaml', 'w') as yamlFile: + yaml.dump(data, yamlFile, default_flow_style=False) + + + def pushImage(self, input): + try: + #Login + result = firebaseLogin(input.get('username'), input.get('password')) + if result.ok: + token = result.json()['idToken'] + else: + raise ValueError('Login failed: {}'.format(result.json()['error']['message'])) + + self.data['fingerprint'] = self.client.images.get(self.data.get('fingerprint')).fingerprint + + if os.path.exists('tmp/images/{}'.format(self.data.get('fingerprint'))): + logging.info('Image exists. Ready for push.') + print ("Image exists. Ready for push.") + + #Prepare the files for upload. + with open('tmp/images/{}/image.yaml'.format(self.data.get('fingerprint'))) as stream: + yamlData = yaml.safe_load(stream) + + files = { + 'yaml': open('tmp/images/{}/image.yaml'.format(self.data.get('fingerprint')), 'rb') + } + + headers = {'Authorization': token} + + response = requests.post('{}/cliAddPackage'.format(meta.IMAGE_HUB), headers=headers, files=files, data={'id': self.data.get('fingerprint')}) + + if response.ok == False: + logging.error('Failed to push the image {}'.format(self.data.get('fingerprint'))) + raise ValueError(response.json()['message']) + + print("yaml uploaded successfully.") + + print("Uploading:") + for file in response.json()['filesRequired']: + for key in file: + files = {} + if file[key] != '': + if os.path.exists('tmp/images/{}/{}'.format(self.data.get('fingerprint'), file[key])): + files['file'] = open('tmp/images/{}/{}'.format(self.data.get('fingerprint'), file[key]), 'rb') + requests.post('{}/cliAddFile'.format(meta.IMAGE_HUB), headers=headers, files=files, data={'id': self.data.get('fingerprint')}).json() + print('File {} uploaded successfully'.format(file[key])) + else: + print('File {} does not exist'.format(file[key])) + + else: + logging.error('Failed to push the image {}'.format(self.data.get('fingerprint'))) + logging.exception('Image is not prepared. Please prepare the image using the command lxdui image prep ') + raise ValueError('Image is not prepared. Please prepare the image using the command: lxdui image prep ') + + except Exception as e: + logging.error('Failed to push the image {}'.format(self.data.get('fingerprint'))) + logging.exception(e) + raise ValueError(e) + + + def importImage(self, input): + + logging.info('Importing image {}'.format(self.data.get('fingerprint'))) + + shutil.rmtree('tmp/downloaded/{}/'.format(self.data.get('fingerprint')), ignore_errors=True) + os.makedirs('tmp/downloaded/{}'.format(self.data.get('fingerprint')), exist_ok=True) + + # Download and extract the file + r = requests.get('{}/cliDownloadRepo/{}'.format(meta.IMAGE_HUB, self.data.get('fingerprint')), stream=True) + with open('tmp/downloaded/{}/package.tar.gz'.format(self.data.get('fingerprint')), 'wb') as f: + for chunk in r.iter_content(chunk_size=1024): + if chunk: + f.write(chunk) + + tfile = tarfile.open('tmp/downloaded/{}/package.tar.gz'.format(self.data.get('fingerprint')), 'r:gz') + tfile.extractall('tmp/downloaded/{}/'.format(self.data.get('fingerprint'))) + + with open('tmp/downloaded/{}/image.yaml'.format(self.data.get('fingerprint'))) as stream: + yamlData = yaml.safe_load(stream) + + + if os.path.exists("tmp/downloaded/{0}/meta-{0}.tar.xz".format(self.data.get('fingerprint'))) and os.path.exists("tmp/downloaded/{0}/{0}.tar.xz".format(self.data.get('fingerprint'))): + p2 = subprocess.Popen(["lxc", "image", "import", + "tmp/downloaded/{0}/meta-{0}.tar.xz".format(self.data.get('fingerprint')), + "tmp/downloaded/{0}/{0}.tar.xz".format(self.data.get('fingerprint'))], stdout=subprocess.PIPE) + output_rez = p2.stdout.read() + + elif os.path.exists("tmp/downloaded/{0}/meta-{0}.tar.gz".format(self.data.get('fingerprint'))) and os.path.exists("tmp/downloaded/{0}/{0}.tar.gz".format(self.data.get('fingerprint'))): + p2 = subprocess.Popen(["lxc", "image", "import", + "tmp/downloaded/{0}/meta-{0}.tar.gz".format(self.data.get('fingerprint')), + "tmp/downloaded/{0}/{0}.tar.gz".format(self.data.get('fingerprint'))], stdout=subprocess.PIPE) + output_rez = p2.stdout.read() + + elif os.path.exists("tmp/downloaded/{0}/meta-{0}.tar.gz".format(self.data.get('fingerprint'))) == False and os.path.exists("tmp/downloaded/{0}/{0}.tar.gz".format(self.data.get('fingerprint'))): + p2 = subprocess.Popen(["lxc", "image", "import", + "tmp/downloaded/{0}/{0}.tar.gz".format(self.data.get('fingerprint'))], stdout=subprocess.PIPE) + output_rez = p2.stdout.read() + + elif os.path.exists("tmp/downloaded/{0}/meta-{0}.tar.xz".format(self.data.get('fingerprint'))) == False and os.path.exists("tmp/downloaded/{0}/{0}.tar.gz".format(self.data.get('fingerprint'))): + p2 = subprocess.Popen(["lxc", "image", "import", + "tmp/downloaded/{0}/{0}.tar.xz".format(self.data.get('fingerprint'))], stdout=subprocess.PIPE) + output_rez = p2.stdout.read() + + elif os.path.exists("tmp/downloaded/{0}/meta-{0}.tar.xz".format(self.data.get('fingerprint'))) == False and os.path.exists("tmp/downloaded/{0}/{0}.squashfs".format(self.data.get('fingerprint'))): + p2 = subprocess.Popen(["lxc", "image", "import", + "tmp/downloaded/{0}/{0}.squashfs".format(self.data.get('fingerprint'))], stdout=subprocess.PIPE) + output_rez = p2.stdout.read() + + elif os.path.exists("tmp/downloaded/{0}/meta-{0}.tar.xz".format(self.data.get('fingerprint'))) and os.path.exists("tmp/downloaded/{0}/{0}.squashfs".format(self.data.get('fingerprint'))): + p2 = subprocess.Popen(["lxc", "image", "import", + "tmp/downloaded/{0}/meta-{0}.tar.xz".format(self.data.get('fingerprint')), + "tmp/downloaded/{0}/{0}.squashfs".format(self.data.get('fingerprint'))], stdout=subprocess.PIPE) + output_rez = p2.stdout.read() + + elif os.path.exists("tmp/downloaded/{0}/meta-{0}.squashfs".format(self.data.get('fingerprint'))) and os.path.exists("tmp/downloaded/{0}/{0}.squashfs".format(self.data.get('fingerprint'))): + p2 = subprocess.Popen(["lxc", "image", "import", + "tmp/downloaded/{0}/meta-{0}.squashfs".format(self.data.get('fingerprint')), + "tmp/downloaded/{0}/{0}.squashfs".format(self.data.get('fingerprint'))], stdout=subprocess.PIPE) + output_rez = p2.stdout.read() + + shutil.rmtree('tmp/downloaded/{}/'.format(self.data.get('fingerprint')), ignore_errors=True) + + image = self.client.images.get(self.data.get('fingerprint')) + image.add_alias(yamlData['title'], yamlData['title']) + + # self.client.images.create(image_data='tmp/images/394986c986a778f64903fa043a3e280bda41e4793580b22c5d991ec948ced6dd/394986c986a778f64903fa043a3e280bda41e4793580b22c5d991ec948ced6dd.tar.xz', + # metadata='tmp/images/394986c986a778f64903fa043a3e280bda41e4793580b22c5d991ec948ced6dd/meta-394986c986a778f64903fa043a3e280bda41e4793580b22c5d991ec948ced6dd.tar.xz') + + + def listHub(self, input): + try: + logging.info('Listing images') + output = "# | Title | Fingerprint | OS | Author\n" + + result = requests.get('{}/cliListRepos'.format(meta.IMAGE_HUB)) + + i = 1 + for r in result.json(): + output += '{} | {} | {} | {} | {}\n'.format(i, r['title'], r['fingerprint'], r['properties'].get('name'), r['author']['name']) + i+=1 + + return output + except Exception as e: + logging.error('Failed to list images from kuti.io') + logging.exception(e) raise ValueError(e) \ No newline at end of file diff --git a/app/api/models/LXCNetwork.py b/app/api/models/LXCNetwork.py index 9e809682..46a22aea 100644 --- a/app/api/models/LXCNetwork.py +++ b/app/api/models/LXCNetwork.py @@ -1,9 +1,5 @@ from app.api.models.LXDModule import LXDModule -from pylxd import Client -import subprocess -from itertools import takewhile from netaddr import IPAddress -import time import socket import struct import logging @@ -14,141 +10,91 @@ class LXCNetwork(LXDModule): def __init__(self, input): logging.info('Connecting to LXD') - self.client = Client() + super().__init__() logging.debug('Setting network input to {}'.format(input)) self.input = input + logging.debug('Setting network name to {}'.format(input.get('name'))) + self.name = input.get('name') - logging.debug('Setting network parameters') - self.MAP = {"ipv4.address": ["IPv4_ENABLED", "IPv4_ADDR", "IPv4_NETMASK", "IPv4_AUTO"], - "ipv6.address": ["IPv6_ENABLED", "IPv6_ADDR", "IPv6_NETMASK", "IPv6_AUTO"], - "ipv4.nat": "IPv4_NAT", - "ipv6.nat": "IPv6_NAT", - "ipv4.dhcp": "IPv4_DHCP", - "ipv4.dhcp.ranges": ["IPv4_DHCP_START", "IPv4_DHCP_END"], - "ipv6.dhcp": "IPv6_DHCP", - "ipv6.dhcp.ranges": ["IPv6_DHCP_START", "IPv6_DHCP_END"]} - self.AUTO_YAML_TERMS = ['auto', '"auto"', "'auto'"] - self.NONE_YAML_TERMS = ['none', '"none"', "'none'"] - self.TRUE_YAML_TERMS = ['true', '"true"', "'true'"] - + super(LXCNetwork, self).__init__() + try: + self.network = self.client.networks.get(self.name) + except: + logging.debug('Network {} does not exist.'.format(self.name)) + self.network = None def info(self): try: - logging.info('Reading network {} information'.format(self.input.get('name'))) - tmp_start_reading = False - used_by_containers = [] - p = subprocess.Popen(["lxc", "network", "show", self.input.get('name')], stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - output_rez, err_rez = p.communicate() - - if err_rez.strip() == "error: not found": - return {'error': True, "message": "LXD Network bridge {} does not exists !".format(name)} - elif "is lxd installed and running" in str(err_rez).lower(): - return {'error': True, - "message": "We are having troubles connecting, this might be due to the LXD daemon not running !"} - elif "command not found" in str(err_rez).lower(): - return {'error': True, - "message": "LXC seems to not be installed or the the LXD daemon might not running !"} - else: - # found - p2 = subprocess.Popen(["lxc", "network", "show", self.input.get('name')], stdout=subprocess.PIPE) - output_rez = p2.stdout.read() - arr = str(output_rez).split("\\n") - - rez = list(takewhile(lambda line: line.strip() != "name: {}".format(self.input.get('name')), arr)) - - # ===================================================================== - for k in arr: - k = k.strip() - - if tmp_start_reading: - if len(k) > 18: - if (k[:18] == "- /1.0/containers/"): - used_by_containers.append(k[18:].strip()) - else: - break; - if k[:7] == 'used_by': - tmp_start_reading = True - - return {'error': False, "result": self._structure_data(rez), 'used_by': used_by_containers} + logging.info('Reading network {} information'.format(self.name)) + + config = {} + + if self.network.config.get('ipv4.address'): + config['IPv4_ADDR'] = self.network.config.get('ipv4.address').split('/')[0] + config['IPv4_NETMASK'] = self._CIDR_suffix_to_netmask_ipv4( + int(self.network.config.get('ipv4.address').split('/')[1])) + + config['IPv4_NAT'] = self.network.config.get('ipv4.nat') + config['IPv4_DHCP'] = self.network.config.get('ipv4.dhcp') + + if self.network.config.get('ipv4.dhcp.ranges'): + config['IPv4_DHCP_START'] = self.network.config.get('ipv4.dhcp.ranges').split('-')[0] + config['IPv4_DHCP_END'] = self.network.config.get('ipv4.dhcp.ranges').split('-')[1] + + used_by = [ c[18:].strip() for c in self.network.used_by ] if self.network.used_by else [] + return {'error': False, "result": config, 'used_by': used_by} except Exception as e: - logging.error('Failed to retrieve information for network {}'.format(self.input.get('name'))) + logging.error('Failed to retrieve information for network {}'.format(self.name)) logging.exception(e) raise ValueError(e) - def createNetwork(self, input, name): + def createNetwork(self): try: - logging.info('Creating network {}'.format(name)) - return self._executeLXCNetworkTerminal(self._formToLXCSetTask(input), name) + logging.info('Creating network {}'.format(self.name)) + config = { + 'ipv4.address': '{}/{}'.format(self.input.get('IPv4_ADDR'), + self._netmaskToCIDRSuffix(self.input.get('IPv4_NETMASK'))), + 'ipv4.nat': self.input.get('IPv4_NAT'), + 'ipv4.dhcp': self.input.get('IPv4_DHCP'), + 'ipv4.dhcp.ranges': '{}-{}'.format(self.input.get('IPv4_DHCP_START'), + self.input.get('IPv4_DHCP_END')), + } + self.client.networks.create(self.name, type='bridge', config=config) + self.network = self.client.networks.get(self.name) + return {"completed": True} except Exception as e: - logging.error('Failed to create network {}'.format(name)) + logging.error('Failed to create network {}'.format(self.name)) logging.exception(e) raise ValueError(e) def deleteNetwork(self): try: - logging.info('Deleting network {}'.format(self.input.get('name'))) - p = subprocess.Popen(['lxc', 'network', 'delete', self.input.get('name')], stdout=subprocess.PIPE) + logging.info('Deleting network {}'.format(self.name)) + self.network.delete() return {'completed': True} except Exception as e: - logging.error('Failed to delete network {}'.format(self.input.get('name'))) + logging.error('Failed to delete network {}'.format(self.name)) logging.exception(e) raise ValueError(e) - def updateNetwork(self, input, name): + def updateNetwork(self): try: - logging.info('Updating network {}'.format(name)) - return self._executeLXCNetworkTerminal(self._formToLXCSetTask(input), name) + logging.info('Updating network {}'.format(self.name)) + + logging.info(self.network.config) + ipv4_cidr = self._netmaskToCIDRSuffix(self.input.get('IPv4_NETMASK')) + self.network.config['ipv4.address'] = '{}/{}'.format(self.input.get('IPv4_ADDR'), ipv4_cidr) + self.network.config['ipv4.nat'] = self.input.get('IPv4_NAT') + self.network.config['ipv4.dhcp'] = self.input.get('IPv4_DHCP') + self.network.config['ipv4.dhcp.ranges'] = '{}-{}'.format(self.input.get('IPv4_DHCP_START'), + self.input.get('IPv4_DHCP_END')) + self.network.save() + return {"completed": True} except Exception as e: - logging.error('Failed to update network {}'.format(name)) + logging.error('Failed to update network {}'.format(self.name)) logging.exception(e) raise ValueError(e) - - def _executeLXCNetworkTerminal(self, lines_to_exec, name): - logging.info('Execute network tasks on terminal') - p = subprocess.Popen(["lxc", "network", "create", name], stdout=subprocess.PIPE) - time.sleep(2) - for lxc_network_value in lines_to_exec['unset']: - p = subprocess.Popen(["lxc", "network", "unset", name, lxc_network_value], - stdout=subprocess.PIPE) - - time.sleep(1) - - for l in lines_to_exec["set"]: - LXC_NET_ATTR = list(l.keys())[0] - LXC_NET_ATTR_VAL = l[LXC_NET_ATTR] - p = subprocess.Popen(["lxc", "network", "set", name, LXC_NET_ATTR, LXC_NET_ATTR_VAL], - stdout=subprocess.PIPE) - - time.sleep(1) - - return {"completed": True} - - - def _formToLXCSetTask(self, data): - logging.info('Prepare network tasks') - TO_DOS = {"set": [], "unset": []} - - if data.get("IPv4_ENABLED") == False: - TO_DOS["set"].append({"ipv4.address": "none"}) - TO_DOS["set"].append({"ipv4.nat": "false"}) - else: - TO_DOS["set"].append({"ipv4.nat": "true"}) - if data.get("IPv4_AUTO") == True: - TO_DOS["unset"].append("ipv4.dhcp.ranges") - TO_DOS["set"].append({"ipv4.address": "auto"}) - else: - CIDR_MASK = self._netmaskToCIDRSuffix(data.get("IPv4_NETMASK")) - - TO_DOS["set"].append({"ipv4.address": data.get("IPv4_ADDR") + '/' + CIDR_MASK}) - if data.get("IPv4_DHCP_START") and data.get("IPv4_DHCP_END"): - TO_DOS["set"].append( - {"ipv4.dhcp.ranges": data.get("IPv4_DHCP_START") + '-' + data.get("IPv4_DHCP_END")}) - - return TO_DOS - - def _netmaskToCIDRSuffix(self, IP_MASK_ADDR): return str(IPAddress(IP_MASK_ADDR).netmask_bits()) @@ -156,64 +102,3 @@ def _CIDR_suffix_to_netmask_ipv4(self, nr): host_bits = 32 - nr netmask = socket.inet_ntoa(struct.pack('!I', (1 << 32) - (1 << host_bits))) return netmask - - def _structure_data(self, linez): - - rez = {} - - for line in linez: - sl = line.lstrip().lower() - #print(sl) - - if sl[:12] == "ipv4.address": - i = self.MAP["ipv4.address"] - value = sl[sl.index(':') + 1:].strip() - if value in self.NONE_YAML_TERMS: - rez[i[0]] = False - elif value in self.AUTO_YAML_TERMS: - rez[i[0]] = True - rez[i[3]] = True - else: - v = value.split('/') - rez[i[0]] = True - rez[i[3]] = False - rez[i[1]] = v[0] - rez[i[2]] = self._CIDR_suffix_to_netmask_ipv4(int(v[1])) - - elif sl[:16] == "ipv4.dhcp.ranges": - i = self.MAP["ipv4.dhcp.ranges"] - value = sl[sl.index(':') + 1:].strip().split('-') - # start - rez[i[0]] = value[0] - rez[i[1]] = value[1] - # finish - - # NAT and - elif sl[:8] == "ipv4.nat": - i = self.MAP["ipv4.nat"] - value = sl[sl.index(':') + 1:].strip() - rez[i] = True if (value in self.TRUE_YAML_TERMS) else False - elif sl[:8] == "ipv6.nat": - i = self.MAP["ipv6.nat"] - value = sl[sl.index(':') + 1:].strip() - rez[i] = True if (value in self.TRUE_YAML_TERMS) else False - - - elif sl[:12] == "ipv6.address": - i = self.MAP["ipv6.address"] - # could be none - value = sl[sl.index(':') + 1:].strip() - if value in self.NONE_YAML_TERMS: - rez[i[0]] = False - elif value in self.AUTO_YAML_TERMS: - rez[i[0]] = True - rez[i[3]] = True - else: - v = value.split('/') - rez[i[0]] = True - rez[i[3]] = False - rez[i[1]] = v[0] - # without the translation - rez[i[2]] = int(v[1]) - - return rez diff --git a/app/api/models/LXCProfile.py b/app/api/models/LXCProfile.py index d408f8c5..12cb9a6e 100644 --- a/app/api/models/LXCProfile.py +++ b/app/api/models/LXCProfile.py @@ -8,7 +8,7 @@ class LXCProfile(LXDModule): def __init__(self, input): logging.info('Connecting to LXD') - self.client = Client() + super().__init__() self.input = input def info(self): diff --git a/app/api/models/LXCSnapshot.py b/app/api/models/LXCSnapshot.py index a509160f..54761e76 100644 --- a/app/api/models/LXCSnapshot.py +++ b/app/api/models/LXCSnapshot.py @@ -43,7 +43,7 @@ def setNewContainer(self, input): def snapshotList(self): try: logging.info('Reading snapshot list for container {}'.format(self.data.get('container'))) - container = self.client.containers.get(self.data.get('container')) + container = self.client.instances.get(self.data.get('container')) result = [] for snap in container.snapshots.all(): result.append(getSnapshotData(snap)) @@ -57,7 +57,7 @@ def snapshotList(self): def snapshotInfo(self): try: logging.info('Reading snapshot {} info for container {}'.format(self.data.get('name'), self.data.get('container'))) - return self.client.api.containers[self.data.get('container')].snapshots[self.data.get('name')].get().json()['metadata'] + return self.client.api.instances[self.data.get('container')].snapshots[self.data.get('name')].get().json()['metadata'] except Exception as e: logging.error('Failed to retrieve snapshot {} info for container {}'.format(self.data.get('name'), self.data.get('container'))) logging.exception(e) @@ -67,13 +67,13 @@ def snapshot(self, s=False): try: logging.info( 'Creating snapshot {} for container {}'.format(self.data.get('name'), self.data.get('container'))) - container = self.client.containers.get(self.data.get('container')) + container = self.client.instances.get(self.data.get('container')) snapName = self.data.get('name') if self._checkSnapshot(container) == False: logging.error('Failed to create snapshot {}. Snapshot with name {} already exists.'.format(self.data.get('name'), snapName)) raise ValueError('Snapshot with name {} already exists.'.format(snapName)) container.snapshots.create(snapName, stateful=s, wait=True) - return self.client.api.containers[self.data.get('container')].snapshots.get().json()['metadata'] + return self.client.api.instances[self.data.get('container')].snapshots.get().json()['metadata'] except Exception as e: logging.error('Failed to create snapshot {}'.format(self.data.get('name'))) logging.exception(e) @@ -82,7 +82,7 @@ def snapshot(self, s=False): def snapshotDelete(self): try: logging.info('Deleting snapshot {} for container {}'.format(self.data.get('name'), self.data.get('container'))) - container = self.client.containers.get(self.data.get('container')) + container = self.client.instances.get(self.data.get('container')) container.snapshots.get(self.data.get('name')).delete() except Exception as e: logging.error('Failed to delete snapshot {} for container {}'.format(self.data.get('name'), self.data.get('container'))) @@ -94,7 +94,7 @@ def snapshotRestore(self): try: logging.info( 'Restoring snapshot {} for container {}'.format(self.data.get('name'), self.data.get('container'))) - return self.client.api.containers[self.data.get('container')].put(json={'restore': self.data.get('name')}).json()['metadata'] + return self.client.api.instances[self.data.get('container')].put(json={'restore': self.data.get('name')}).json()['metadata'] except Exception as e: logging.error('Failed to restore snapshot {} for container {}'.format(self.data.get('name'), self.data.get('container'))) @@ -105,7 +105,7 @@ def snapshotPublish(self): try: logging.info( 'Publishing snapshot {} for container {}'.format(self.data.get('name'), self.data.get('container'))) - container = self.client.containers.get(self.data.get('container')) + container = self.client.instances.get(self.data.get('container')) image = container.snapshots.get(self.data.get('name')).publish(wait=True) image.add_alias(self.data.get('name'), self.data.get('name')) return self.client.api.images[image.fingerprint].get().json()['metadata'] @@ -119,11 +119,11 @@ def snapshotCreateContainer(self): try: logging.info( 'Creating container {} from snapshot {}'.format(self.data.get('newContainer'), self.data.get('name'))) - container = self.client.containers.get(self.data.get('container')) + container = self.client.instances.get(self.data.get('container')) image = container.snapshots.get(self.data.get('name')).publish(wait=True) image.add_alias(self.data.get('name'), self.data.get('name')) config = {'name': self.data.get('newContainer'), 'source': {'type': 'image', 'alias': self.data.get('name')}} - self.client.containers.create(config, wait=True) + self.client.instances.create(config, wait=True) newImage = self.client.images.get(image.fingerprint) newImage.delete(wait=True) diff --git a/app/api/models/LXCStoragePool.py b/app/api/models/LXCStoragePool.py index 4f15af9e..39f3498d 100644 --- a/app/api/models/LXCStoragePool.py +++ b/app/api/models/LXCStoragePool.py @@ -8,7 +8,7 @@ class LXCStoragePool(LXDModule): def __init__(self, input): logging.info('Connecting to LXD') - self.client = Client() + super().__init__() self.input = input def info(self): diff --git a/app/api/models/LXDModule.py b/app/api/models/LXDModule.py index 5279946a..5ae5521b 100644 --- a/app/api/models/LXDModule.py +++ b/app/api/models/LXDModule.py @@ -6,19 +6,39 @@ from pylxd import Client import requests import logging +import urllib3 + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) logging = logging.getLogger(__name__) class LXDModule(Base): # Default 127.0.0.1 -> Move to Config def __init__(self, remoteHost='127.0.0.1'): + + conf = Config() logging.info('Accessing PyLXD client') - self.client = Client() + try: + remoteHost = Config().get(meta.APP_NAME, '{}.lxd.remote'.format(meta.APP_NAME.lower())) + sslKey = conf.get(meta.APP_NAME, '{}.ssl.key'.format(meta.APP_NAME.lower())) + sslCert = conf.get(meta.APP_NAME, '{}.ssl.cert'.format(meta.APP_NAME.lower())) + sslVerify = conf.get(meta.APP_NAME, '{}.lxd.sslverify'.format(meta.APP_NAME.lower())) + + if sslVerify.lower in ['true', '1', 't', 'y', 'yes', 'yeah', 'yup', 'certainly']: + sslVerify = True + else: + sslVerify = False + + self.client = Client(endpoint=remoteHost, + cert=(sslCert, sslKey), verify=sslVerify) + except: + logging.info('using local socket') + self.client = Client() def listContainers(self): try: logging.info('Reading container list') - return self.client.containers.all() + return self.client.instances.all() except Exception as e: logging.error('Failed to read container list: ') logging.exception(e) @@ -58,6 +78,17 @@ def listNightlyImages(self): logging.exception(e) raise ValueError(e) + def listHubImages(self): + try: + logging.info('Listing images') + result = requests.get('{}/cliListRepos'.format(meta.IMAGE_HUB)) + + return result.json() + except Exception as e: + logging.error('Failed to list images from kuti.io') + logging.exception(e) + raise ValueError(e) + def detailsRemoteImage(self, alias): try: remoteImagesLink = Config().get(meta.APP_NAME, '{}.images.remote'.format(meta.APP_NAME.lower())) @@ -106,6 +137,14 @@ def listStoragePools(self): except Exception as e: raise ValueError(e) + def setLimitsCPU(self): + conf = Config() + set_cpu = conf.get(meta.APP_NAME, '{}.set_limits_cpu'.format(meta.APP_NAME.lower())) + if set_cpu == 'true': + return True + else: + return False + def createProfile(self): pass @@ -158,7 +197,7 @@ def containerExists(self, containerName): lxdModule = LXDModule() try: logging.info('Checking if container exists.') - container = self.client.containers.get(containerName) + container = self.client.instances.get(containerName) return True except Exception as e: logging.error('Failed to verify container:') @@ -193,4 +232,4 @@ def clone(self): raise NotImplementedError() def snapshot(self): - raise NotImplementedError() \ No newline at end of file + raise NotImplementedError() diff --git a/app/api/schemas/container_schema.py b/app/api/schemas/container_schema.py index 757a622c..590ac1ca 100644 --- a/app/api/schemas/container_schema.py +++ b/app/api/schemas/container_schema.py @@ -20,6 +20,10 @@ 'image':{ 'type':'string', 'description':'Image alias or hash' + }, + 'type':{ + 'type':'string', + 'description':'Type of instance' }, 'newName': { 'type': 'string', @@ -30,16 +34,12 @@ 'description':'Stateful container' }, 'profiles':{ - 'type':'array', - 'items':[ - {'type':'string'} - ] + 'type': 'array', + 'items': {'type': 'string'} }, 'network': { 'type': 'array', - 'items': [ - {'type': 'string'} - ] + 'items': {'type': 'string'} }, 'cpu': { 'type': 'object', @@ -87,6 +87,88 @@ } } +set_cpu_limit_schema = { + "oneOf": [ + {"$ref": "#/definitions/singleObject"}, # plain object + { + "type": "array", # array of plain objects + "items": {"$ref": "#/definitions/singleObject"} + } + ], + "definitions": { + "singleObject": { + 'type':'object', + 'required': ['name', 'image'], + 'properties':{ + 'name':{ + 'type':'string', + 'description':'Container name' + }, + 'image':{ + 'type':'string', + 'description':'Image alias or hash' + }, + 'type':{ + 'type':'string', + 'description':'Type of instance' + }, + 'newName': { + 'type': 'string', + 'description': 'New Container name' + }, + 'stateful':{ + 'type':'boolean', + 'description':'Stateful container' + }, + 'profiles':{ + 'type': 'array', + 'items': {'type': 'string'} + }, + 'network': { + 'type': 'array', + 'items': {'type': 'string'} + }, + 'cpu': { + 'type': 'object', + 'description': 'CPU Limitation', + 'required':['percentage','hardLimitation'], + 'properties':{ + 'cores':{ + 'type':'integer', + 'description':'Set the number of CPU cores', + 'minimum':1 + }, + } + }, + 'memory':{ + 'type': 'object', + 'description': 'Memory limitation', + 'required': ['sizeInMB', 'hardLimitation'], + 'properties': { + 'sizeInMB': { + 'type': 'integer', + 'description': 'Set memory limitation', + 'minimum': 32 + }, + 'hardLimitation':{ + 'type':'boolean', + 'description':'Set as hard limitation (soft limitation presumed on false)' + } + } + }, + 'autostart':{ + 'type':'boolean', + 'description':'autostart instance' + }, + 'description': { + 'type': 'string', + 'description': 'Description instance' + } + } + } + } +} + copyMoveSchema = { "oneOf": [ {"$ref": "#/definitions/singleObject"}, # plain object @@ -137,9 +219,12 @@ def doValidateCloneMove(input): except ValidationError as e: return e -def doValidate(input): +def doValidate(input, setCPU = False): try: - validate(input, schema) + if setCPU: + validate(input, set_cpu_limit_schema) + else: + validate(input, schema) return None except ValidationError as e: - return e \ No newline at end of file + return e diff --git a/app/api/schemas/profile_schema.py b/app/api/schemas/profile_schema.py index 88390508..76105a31 100644 --- a/app/api/schemas/profile_schema.py +++ b/app/api/schemas/profile_schema.py @@ -118,9 +118,7 @@ }, 'used_by': { 'type': 'array', - 'items': [ - {'type': 'string'} - ] + 'items': {'type': 'string'} }, 'new_name': { 'type': 'string', diff --git a/app/api/schemas/publishImageSchema.py b/app/api/schemas/publishImageSchema.py new file mode 100644 index 00000000..8d19cf40 --- /dev/null +++ b/app/api/schemas/publishImageSchema.py @@ -0,0 +1,26 @@ +from jsonschema import validate, ValidationError + +schema = { + "oneOf": [ + {"$ref": "#/definitions/singleObject"}, # plain object + ], + "definitions": { + "singleObject": { + 'type':'object', + #'required': [], + 'properties':{ + 'name':{ + 'type':'string', + 'description':'image (name/distribution/architecture)' + } + } + } + } +} + +def doValidate(input): + try: + validate(input, schema) + return None + except ValidationError as e: + return e \ No newline at end of file diff --git a/app/api/utils/authentication.py b/app/api/utils/authentication.py index 8ec2f6b6..46f5dfd0 100644 --- a/app/api/utils/authentication.py +++ b/app/api/utils/authentication.py @@ -1,12 +1,12 @@ -import jwt - from app.lib.auth import User from app.lib.conf import Config from datetime import timedelta -from flask_jwt import JWT +from flask_jwt_extended import JWTManager + from app.api.utils import converters import app.__metadata__ as meta import logging +import secrets logging = logging.getLogger(__name__) @@ -14,34 +14,23 @@ def authenticate(username, password): logging.info("Authenticate user {}".format(username)) if User().authenticate(username, password)[0] == True: logging.info("User {} successfully authenticated".format(username)) - return converters.json2obj('{"id": 1, "username": "'+username+'", "password": "'+password+'"}') + return True else: logging.warning("Authentication failed for user {}".format(username)) return False - -def identity(payload): - return payload - - def initAuth(app): APP = meta.APP_NAME tokenExpiration = int(Config().get(APP, '{}.jwt.token.expiration'.format(APP.lower()))) - secretKey = Config().get(APP, '{}.jwt.secret.key'.format(APP.lower())) - authUrlRule = Config().get(APP, '{}.jwt.auth.url.rule'.format(APP.lower())) + # create a new secretKet whenever the system is started + secretKey = secrets.token_urlsafe(32) if (tokenExpiration == None): tokenExpiration = 1200 - app.config['SECRET_KEY'] = secretKey - app.config['JWT_AUTH_URL_RULE'] = authUrlRule - app.config['JWT_EXPIRATION_DELTA'] = timedelta(seconds=tokenExpiration) - JWT(app, authenticate, identity) + tokenLocation = ["headers","cookies"] -def jwt_decode_handler(token): - try: - APP = meta.APP_NAME - secretKey = Config().get(APP, '{}.jwt.secret.key'.format(APP.lower())) - payload = jwt.decode(token, secretKey, algorithm='HS256') - return True - except jwt.InvalidTokenError: - return False + app.config['JWT_SECRET_KEY'] = secretKey + app.config['JWT_TOKEN_LOCATION'] = tokenLocation + app.config['JWT_EXPIRATION_DELTA'] = timedelta(seconds=tokenExpiration) + app.config['JWT_COOKIE_CSRF_PROTECT'] = False + jwt = JWTManager(app) diff --git a/app/api/utils/containerMapper.py b/app/api/utils/containerMapper.py index aa32f462..d01d9573 100644 --- a/app/api/utils/containerMapper.py +++ b/app/api/utils/containerMapper.py @@ -1,20 +1,64 @@ - +import re def getContainerDetails(container): - ip = 'N/A' - if container.state().network != None and container.state().network.get('eth0') != None: - if len(container.state().network.get('eth0')['addresses']) > 0: - ip = container.state().network['eth0']['addresses'][0].get('address', 'N/A') + ip = [] + + # Retrieve all IPv4 addresses with global scope + instanceNetwork = container.state().network + if instanceNetwork != None: + for network_interface in instanceNetwork: + current_network = instanceNetwork.get(network_interface) + network_addresses = current_network.get('addresses') + if len(network_addresses) > 0: + for address in network_addresses: + if isIPV4(address.get('address')) and (address.get('scope') == 'global'): + ip.append(address.get('address')) + + # in case no IP addresses were found in all the previous network interfaces + if len(ip) == 0: + ip = 'N/A' image = 'N/A' if container.config.get('image.os') != None and container.config.get('image.release') != None and container.config.get('image.architecture') != None: image = ''.join(container.config.get('image.os') + ' ' + container.config.get('image.release') + ' ' + container.config.get('image.architecture')) + instance_type = 'Container' + if container.type == 'virtual-machine': + instance_type = 'Virtual Machine' + + memory = container.config.get('limits.memory') + if memory is None: + memory = 'N/A' + + cpu = container.config.get('limits.cpu') + if cpu is None: + cpu = 'N/A' + + try: + # attempt to extract the size of the root disk + disk = container.devices.get('root')['size'] + except: + # in case the root device was not set (passed on through profile) + disk = 'N/A' + return { 'name': container.name, 'status': container.status, 'ip': ip, + 'memory': memory, + 'cpu': cpu, + 'disk': disk, 'ephemeral': container.ephemeral, + 'type': instance_type, 'image': image, - 'created_at': container.created_at - } \ No newline at end of file + 'created_at': container.created_at, + } + +def isIPV4(address): + + valid = re.match('[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+', address) + + if valid: + return True + else: + return False diff --git a/app/api/utils/firebaseAuthentication.py b/app/api/utils/firebaseAuthentication.py new file mode 100644 index 00000000..8b418f22 --- /dev/null +++ b/app/api/utils/firebaseAuthentication.py @@ -0,0 +1,15 @@ +import requests + +__FIREBASE_USER_VERIFY_SERVICE = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword" +__FIREBASE_API_KEY = "AIzaSyAVIWywqLtxfRLxZfjAauCK6tH-udR_jeo" + +def firebaseLogin(email, passwd): + url = "%s?key=%s" % (__FIREBASE_USER_VERIFY_SERVICE, __FIREBASE_API_KEY) + data = {"email": email, + "password": passwd, + "returnSecureToken": True} + + result = requests.post(url, json=data) + json_result = result + + return json_result \ No newline at end of file diff --git a/app/cli/cli.py b/app/cli/cli.py index eedc6977..964d35b0 100644 --- a/app/cli/cli.py +++ b/app/cli/cli.py @@ -5,6 +5,8 @@ from app.lib.cert import Certificate from app.api import core from app.ui.blueprint import uiPages +from app.api.models.LXCImage import LXCImage +from getpass import getpass import click import os import signal @@ -30,16 +32,25 @@ lxdui cert create #generate new SSL certs (overwrite old files) lxdui cert list #list SSL certs lxdui cert delete #remove SSL certs -lxdui user add -u -p #create a new user that can access the UI -lxdui user update -u -p #the user specified in lxdui.admin.user can't be deleted +lxdui user add -u #create a new user that can access the UI +lxdui user update -u #the user specified in lxdui.admin.user can't be deleted lxdui user delete -u #remove a user from the auth file lxdui user list #list the users in the auth file ''' +def getuserpass(): + """Securely retrieve a user password from the environment or from console""" + if 'LXDUI_PASSWORD' in os.environ: + return os.environ['LXDUI_PASSWORD'] + else: + return getpass() + + ''' Command Groups ''' + @click.group() @click.version_option(version=meta.VERSION, message='{} v{} \n{}\n{}'.format(meta.APP_NAME, meta.VERSION, meta.AUTHOR, meta.AUTHOR_URL)) # @click.version_option(message=meta.APP_NAME + ' version ' + meta.VERSION + '\n' + meta.AUTHOR + '\n' + meta.AUTHOR_URL ) @@ -49,10 +60,14 @@ def lxdui(): ''' lxdui root level group of commands ''' @lxdui.command() -@click.option('-p', '--password', prompt=True, hide_input=True, confirmation_prompt=True, help='Password') -def init(password): - """Initialize and configure LXDUI""" +def init(): + """Initialize and configure LXDUI + + The password is taken from the LXDUI_PASSWORD environment variable or read from console if not set.""" click.echo("Initialize and configure %s" % APP) + + password = getuserpass() + Init(password) @@ -60,7 +75,7 @@ def init(password): # @click.otion('-b', '--daemon', default=False, help='Run in background as a daemon.') # @click.otion('-c', '--conf', default=False, help='Config file.') # @click.otion('-p', '--port', default=False, help='TCP Port number.') -@click.option('-d', '--debug', default=False, help='Run the app in debug mode') +@click.option('-d', '--debug', is_flag=True, default=False, help='Run the app in debug mode') def start(debug): """Start LXDUI""" @@ -69,14 +84,16 @@ def start(debug): # Private Functions def _start(debug=False): + host = '0.0.0.0' port = 5000 try: + host = Config().get('LXDUI', 'lxdui.host') port = int(Config().get('LXDUI', 'lxdui.port')) except: print('Please initialize {} first. e.g: {} init '.format(meta.APP_NAME, meta.APP_CLI_CMD)) exit() - core.start(port, debug, uiPages) + core.start(host, port, debug, uiPages) if debug: _start(debug=True) @@ -94,12 +111,13 @@ def stop(): @lxdui.command() def restart(): """Restart LXDUI""" + host = Config().get('LXDUI', 'lxdui.host') port = int(Config().get('LXDUI', 'lxdui.port')) click.echo('Restarting with defaults.') core.stop() click.echo('Port = {} \nDebug = False\nMode = Foreground\n'.format(port)) time.sleep(3) - core.start(port, False, uiPages) + core.start(host, port, False, uiPages) @lxdui.command() def status(): @@ -112,16 +130,95 @@ def status(): else: click.echo("=============") for k, v in s.items(): - click.echo(' {} : {}'.format(k, v)) + click.echo(' {} : {}'.format(k, v)) + + +@click.group() +def image(): + """Work with image registry""" + pass + +@image.command() +@click.argument('fingerprint', nargs=1) +def prep(fingerprint): + """Prepare an image for upload""" + try: + input = {} + image = LXCImage({'fingerprint': fingerprint}) + + # Export Image - Image registry + path = image.exportImage(input) + + click.echo("Image prepared successfully.") + click.echo("The image path is: {}".format(path)) + click.echo("Modify the image.yaml, upload the logo and update README.md") + click.echo("To publish the image use the command:") + click.echo("lxdui image push -u -p ") + except Exception as e: + click.echo("LXDUI failed to prepare the image.") + click.echo(e.__str__()) + +@image.command() +@click.argument('fingerprint', nargs=1) +@click.option('-u', '--username', required=True, nargs=1, help='Username') +def push(fingerprint, username): + """Push an image to hub.kuti.io + + The password is taken from the LXDUI_PASSWORD environment variable or read from console if not set.""" + try: + input = {} + input['username'] = username + input['password'] = getuserpass() + image = LXCImage({'fingerprint': fingerprint}) + + # Export Image - Image registry + image.pushImage(input) + + click.echo("Image pushed successfully.") + except Exception as e: + click.echo("LXDUI failed to push the image.") + click.echo(e.__str__()) + +@image.command() +@click.argument('fingerprint', nargs=1) +def pull(fingerprint): + """Pull an image from hub.kuti.io""" + try: + input = {} + + image = LXCImage({'fingerprint': fingerprint}) + + print("Downlaoding image with fingerprint {}".format(fingerprint)) + # Import Image - Image registry + image.importImage(input) + + click.echo("Image imported successfully.") + except Exception as e: + click.echo("LXDUI failed to download/import the image.") + click.echo(e.__str__()) + +@image.command() +def list(): + """List images from hub.kuti.io""" + try: + input = {} + image = LXCImage({'fingerprint': 'a'}) + + # List Images from kuti.io + print (image.listHub(input)) + + except Exception as e: + click.echo("LXDUI failed to list the images from kuti.io.") + click.echo(e.__str__()) ''' User level group of commands - lxdui user list #list the users in the auth file - lxdui user add -u -p #create a new user that can access the UI - lxdui user update -u -p #the user specified in lxdui.admin.user can't be deleted - lxdui user delete -u #remove a user from the auth file + lxdui user list #list the users in the auth file + lxdui user add -u #create a new user that can access the UI + lxdui user update -u #the user specified in lxdui.admin.user can't be deleted + lxdui user delete -u #remove a user from the auth file ''' @@ -141,24 +238,26 @@ def list(): @user.command() -@click.option('-u', '--username', help='User Name') -@click.option('-p', '--password', prompt=True, hide_input=True, confirmation_prompt=True, help='Password') -def add(username, password): - """Create a new user account""" - User().add(username, password) +@click.option('-u', '--username', required=True, help='User Name') +def add(username): + """Create a new user account + + The password is taken from the LXDUI_PASSWORD environment variable or read from console if not set.""" + User().add(username, getuserpass()) @user.command() -@click.option('-u', '--username', nargs=1, help='User Name') -@click.option('-p', '--password', prompt=True, hide_input=True, confirmation_prompt=True, help='Password') -def update(username, password): - """Change user password""" +@click.option('-u', '--username', required=True, nargs=1, help='User Name') +def update(username): + """Change user password + + The password is taken from the LXDUI_PASSWORD environment variable or read from console if not set.""" click.echo("Change user password") - User().update(username, password) + User().update(username, getuserpass()) @user.command() -@click.option('-u', '--username', nargs=1, help='User Name') +@click.option('-u', '--username', required=True, nargs=1, help='User Name') def delete(username): """Delete a user account""" click.echo("Delete user account") @@ -246,8 +345,8 @@ def create(): """Create client certificates""" c = Certificate() key, crt = c.create() - c.save(key) - c.save(crt) + c.save(APP.lower(),key) + c.save(APP.lower(),crt) @cert.command() @@ -288,6 +387,7 @@ def delete(): lxdui.add_command(user) lxdui.add_command(config) lxdui.add_command(cert) +lxdui.add_command(image) if __name__ == '__main__': diff --git a/app/cli/init.py b/app/cli/init.py index 228ff276..d2fabeb6 100644 --- a/app/cli/init.py +++ b/app/cli/init.py @@ -52,7 +52,7 @@ class Init(object): def __init__(self, password): c = conf.Config() - self.password = auth.User.sha_password(password) + self.password = auth.User.bcrypt_password(password) self.username = c.get(self.APP, '{}.admin.user'.format(self.APP.lower())) self.auth_file = c.get(self.APP, '{}.auth.conf'.format(self.APP.lower())) self.cert_file = c.get(self.APP, '{}.ssl.cert'.format(self.APP.lower())) diff --git a/app/lib/auth.py b/app/lib/auth.py index 40eb1e9a..62373e41 100644 --- a/app/lib/auth.py +++ b/app/lib/auth.py @@ -4,6 +4,7 @@ import json import hashlib import logging +import bcrypt log = logging.getLogger(__name__) @@ -65,6 +66,11 @@ def sha_password(cls, password): sha1_password = hashlib.sha1(password.encode('utf-8')).hexdigest() return sha1_password + @classmethod + def bcrypt_password(cls, password): + hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') + return hashed_password + def show(self): # retrieve just the user names counter = 0 @@ -82,8 +88,7 @@ def add(self, username, password): exit() data = self.users - sha1_password = hashlib.sha1(password.encode('utf-8')).hexdigest() - data.append({'username': username, 'password': sha1_password}) + data.append({'username': username, 'password': self.bcrypt_password(password)}) self.save(data) def delete(self, username): @@ -112,7 +117,7 @@ def update(self, username, password): if account is None: return err - account['password'] = self.sha_password(password) + account['password'] = self.bcrypt_password(password) self.users.remove(account) self.users.append(account) # save the new auth state when done @@ -124,7 +129,13 @@ def authenticate(self, username, password): if account is None: return 'Error', err - if account['password'] == self.sha_password(password): + if len(account['password']) == 40: + # Can't just `and` this in the above conditional because then incorrect SHA passwords get passed to bcrypt + if account['password'] == self.sha_password(password): + # Migrate to bcrypt + self.update(username, password) + return True, 'Authenticated' + elif bcrypt.checkpw(password.encode('utf-8'), account['password'].encode('utf-8')): return True, 'Authenticated' - else: - return False, 'Incorrect password.' + + return False, 'Incorrect password.' diff --git a/app/lib/conf.py b/app/lib/conf.py index d6ebadac..113d5401 100644 --- a/app/lib/conf.py +++ b/app/lib/conf.py @@ -155,6 +155,7 @@ def set(self, section, key, value): :return: """ self.config.set(section, key, value) + self.save() def show(self): """ diff --git a/app/lib/termmanager.py b/app/lib/termmanager.py index a9e9c04e..cdc49556 100644 --- a/app/lib/termmanager.py +++ b/app/lib/termmanager.py @@ -54,6 +54,10 @@ def kill(self, name, sig=signal.SIGTERM): term = self.terminals[name] term.kill(sig) # This should lead to an EOF + def client_disconnected(self, websocket): + if (len(self.terminals[websocket.term_name].clients)==0): + del self.terminals[websocket.term_name] + @gen.coroutine def terminate(self, name, force=False): term = self.terminals[name] diff --git a/app/ui/blueprint.py b/app/ui/blueprint.py index ec4761db..cf4afb5a 100644 --- a/app/ui/blueprint.py +++ b/app/ui/blueprint.py @@ -4,12 +4,32 @@ from app.lib.conf import Config from app import __metadata__ as meta from app.__metadata__ import VERSION +from flask_jwt_extended import jwt_required +from jwt.exceptions import PyJWTError +from functools import wraps + import json import os +import platform +import subprocess uiPages = Blueprint('uiPages', __name__, template_folder='./templates', static_folder='./static') +def jwt_ui(func): + """ + Catches JWT Errors and returns an error page + rather than a json encoded error. + """ + @wraps(func) + def wrapper_function(*args, **kwargs): + try: + retval = func(*args, **kwargs) + except PyJWTError as e: + return render_template('auth_error.html', error=e) + return retval + return wrapper_function + def memory(): mem_bytes = os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES') return int(mem_bytes / (1024. ** 2)) # convert to mb @@ -19,6 +39,8 @@ def index(): return render_template('login.html', currentpage='Login') @uiPages.route('/containers') +@jwt_ui +@jwt_required() def container(): try: containers = LXDModule().listContainers() @@ -28,11 +50,22 @@ def container(): images = LXDModule().listLocalImages() profiles = LXDModule().listProfiles() + storagePools = LXDModule().listStoragePools() + limitsCPU = LXDModule().setLimitsCPU() + # While Python does offer a cpu_count() function in their os library this function returns + # the number of CPU cores allocated to the UI container if LXDUI is installed in a container. + # To get the number of cores of the host we have to execute 'lscpu' through the shell which + # returns all the data regarding the hosts physical CPU from which we can extract the maximum number of cores. + cpuCount = subprocess.check_output("lscpu | grep 'CPU(s):' | head -1 | grep -o -E '[0-9]+' | tr -d '\n'", shell=True, text=True) + return render_template('containers.html', currentpage='Containers', containers=result, images = images, profiles = profiles, - memory=memory(), + memory = memory(), + limitsCPU = limitsCPU, + cpu = cpuCount, + storagePools = storagePools, lxdui_current_version=VERSION) except: return render_template('containers.html', currentpage='Containers', @@ -40,10 +73,15 @@ def container(): images=[], profiles=[], memory=memory(), + storagePools = [], + cpu = cpuCount, + limitsCPU = limitsCPU, lxdui_current_version=VERSION) @uiPages.route('/containers/') +@jwt_ui +@jwt_required() def containerDetails(name): try: container = LXCContainer({'name': name}) @@ -62,6 +100,8 @@ def containerDetails(name): @uiPages.route('/profiles') +@jwt_ui +@jwt_required() def profile(): try: profiles = LXDModule().listProfiles() @@ -72,6 +112,8 @@ def profile(): profiles=[], lxdui_current_version=VERSION) @uiPages.route('/storage-pools') +@jwt_ui +@jwt_required() def storagePools(): try: storagePools = LXDModule().listStoragePools() @@ -82,6 +124,8 @@ def storagePools(): storagePools=[], lxdui_current_version=VERSION) @uiPages.route('/network') +@jwt_ui +@jwt_required() def network(): try: network = LXDModule().listNetworks() @@ -92,25 +136,32 @@ def network(): network=[], lxdui_current_version=VERSION) @uiPages.route('/images') +@jwt_ui +@jwt_required() def images(): localImages = getLocalImages() profiles = getProfiles() - remoteImages = getRemoteImages() - nightlyImages = getNightlyImages() + remoteImages = [] + nightlyImages = [] + hubImages = getHubImages() remoteImagesLink = Config().get(meta.APP_NAME, '{}.images.remote'.format(meta.APP_NAME.lower())) return render_template('images.html', currentpage='Images', localImages=localImages, remoteImages=remoteImages, nightlyImages=nightlyImages, + hubImages=hubImages, profiles=profiles, jsData={ 'local': json.dumps(localImages), 'remote': json.dumps(remoteImages), - 'nightly': json.dumps(nightlyImages) + 'nightly': json.dumps(nightlyImages), + 'hub': json.dumps(hubImages) }, memory=memory(), lxdui_current_version=VERSION, - remoteImagesLink=remoteImagesLink) + remoteImagesLink=remoteImagesLink, + imageHubLink=meta.IMAGE_HUB, + architecture=platform.machine()) def getLocalImages(): @@ -141,6 +192,15 @@ def getNightlyImages(): return nightlyImages + +def getHubImages(): + try: + hubImages = LXDModule().listHubImages() + except: + hubImages = [] + + return hubImages + def getProfiles(): try: profiles = LXDModule().listProfiles() diff --git a/app/ui/static/css/app.css b/app/ui/static/css/app.css index 08304dba..30a51b79 100644 --- a/app/ui/static/css/app.css +++ b/app/ui/static/css/app.css @@ -166,6 +166,7 @@ .legend { padding-bottom:15px; + text-align:center; } .bootstrap-select { @@ -208,6 +209,12 @@ line-height:1.1; } +/* CHANGING BUTTON LogOut BACKGROUND COLOR ON HOVER */ +#button-log-out:hover{ + background-color:red; + color:#fff; +} + .profiles .label .btn { color: #fff; padding: 0px 2px; @@ -325,12 +332,6 @@ border-right: 1px solid #ddd; } -.panel > div:nth-child(6) { - border-bottom: 1px solid #ddd; - border-left: 1px solid #ddd; - border-right: 1px solid #ddd; -} - .panel { border: none; } @@ -461,6 +462,37 @@ color: #000 !important; } +#exTab > ul > li.tab-background.active a { + background: #7d7b7b; + color: #fff !important; +} + +#exTab > ul > li.active > a span { + background: #fff; + color: #000; +} + +#exTab > ul > li.active > a { + background: #7d7b7b; + color: #fff !important; +} + +#exTab .nav-tabs>li>a { + color: #000 !important; + font-weight: 700; +} + +.modal-footer { + text-align: center; +} + +#publishImageModal #documentation { + width: 100%; + height: 280px; +} + + + #exTab3 > ul > li > a { background: #dddddd; @@ -709,9 +741,9 @@ color: #000 !important; } -.dropdown-menu { +/*.dropdown-menu { top: -95px !important; - left: -169px !important; + left: -169px !important;*/ } .tbl-header { -webkit-border-radius: 10px 10px 0 0; @@ -839,7 +871,9 @@ color: #000 !important; vertical-align: middle; margin-right: -4px; /* Adjusts for spacing */ } - +.modal-title{ +text-align: center; +} .modal-dialog { display: inline-block; text-align: left; @@ -847,7 +881,7 @@ color: #000 !important; } .status-div { - color: #000 !important; + color:fff; padding: 0px; background: #5cba5b; text-align: center; @@ -867,9 +901,7 @@ color: #000 !important; margin-right: -18px; } -.status-div .label-success { - color: #000 !important; -} + .pd0 { padding: 0px !important; @@ -903,4 +935,14 @@ color: #000 !important; margin-left: 5px; } +.list-details { + padding-left: 45px; +} +.parsley-required { + color: red; +} +.parsley-errors-list { + list-style: none; + padding: 0; +} diff --git a/app/ui/static/css/jquery-ui.min.css b/app/ui/static/css/jquery-ui.min.css new file mode 100644 index 00000000..776e2595 --- /dev/null +++ b/app/ui/static/css/jquery-ui.min.css @@ -0,0 +1,7 @@ +/*! jQuery UI - v1.12.1 - 2016-09-14 +* http://jqueryui.com +* Includes: core.css, accordion.css, autocomplete.css, menu.css, button.css, controlgroup.css, checkboxradio.css, datepicker.css, dialog.css, draggable.css, resizable.css, progressbar.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?bgShadowXPos=&bgOverlayXPos=&bgErrorXPos=&bgHighlightXPos=&bgContentXPos=&bgHeaderXPos=&bgActiveXPos=&bgHoverXPos=&bgDefaultXPos=&bgShadowYPos=&bgOverlayYPos=&bgErrorYPos=&bgHighlightYPos=&bgContentYPos=&bgHeaderYPos=&bgActiveYPos=&bgHoverYPos=&bgDefaultYPos=&bgShadowRepeat=&bgOverlayRepeat=&bgErrorRepeat=&bgHighlightRepeat=&bgContentRepeat=&bgHeaderRepeat=&bgActiveRepeat=&bgHoverRepeat=&bgDefaultRepeat=&iconsHover=url(%22images%2Fui-icons_555555_256x240.png%22)&iconsHighlight=url(%22images%2Fui-icons_777620_256x240.png%22)&iconsHeader=url(%22images%2Fui-icons_444444_256x240.png%22)&iconsError=url(%22images%2Fui-icons_cc0000_256x240.png%22)&iconsDefault=url(%22images%2Fui-icons_777777_256x240.png%22)&iconsContent=url(%22images%2Fui-icons_444444_256x240.png%22)&iconsActive=url(%22images%2Fui-icons_ffffff_256x240.png%22)&bgImgUrlShadow=&bgImgUrlOverlay=&bgImgUrlHover=&bgImgUrlHighlight=&bgImgUrlHeader=&bgImgUrlError=&bgImgUrlDefault=&bgImgUrlContent=&bgImgUrlActive=&opacityFilterShadow=Alpha(Opacity%3D30)&opacityFilterOverlay=Alpha(Opacity%3D30)&opacityShadowPerc=30&opacityOverlayPerc=30&iconColorHover=%23555555&iconColorHighlight=%23777620&iconColorHeader=%23444444&iconColorError=%23cc0000&iconColorDefault=%23777777&iconColorContent=%23444444&iconColorActive=%23ffffff&bgImgOpacityShadow=0&bgImgOpacityOverlay=0&bgImgOpacityError=95&bgImgOpacityHighlight=55&bgImgOpacityContent=75&bgImgOpacityHeader=75&bgImgOpacityActive=65&bgImgOpacityHover=75&bgImgOpacityDefault=75&bgTextureShadow=flat&bgTextureOverlay=flat&bgTextureError=flat&bgTextureHighlight=flat&bgTextureContent=flat&bgTextureHeader=flat&bgTextureActive=flat&bgTextureHover=flat&bgTextureDefault=flat&cornerRadius=3px&fwDefault=normal&ffDefault=Arial%2CHelvetica%2Csans-serif&fsDefault=1em&cornerRadiusShadow=8px&thicknessShadow=5px&offsetLeftShadow=0px&offsetTopShadow=0px&opacityShadow=.3&bgColorShadow=%23666666&opacityOverlay=.3&bgColorOverlay=%23aaaaaa&fcError=%235f3f3f&borderColorError=%23f1a899&bgColorError=%23fddfdf&fcHighlight=%23777620&borderColorHighlight=%23dad55e&bgColorHighlight=%23fffa90&fcContent=%23333333&borderColorContent=%23dddddd&bgColorContent=%23ffffff&fcHeader=%23333333&borderColorHeader=%23dddddd&bgColorHeader=%23e9e9e9&fcActive=%23ffffff&borderColorActive=%23003eff&bgColorActive=%23007fff&fcHover=%232b2b2b&borderColorHover=%23cccccc&bgColorHover=%23ededed&fcDefault=%23454545&borderColorDefault=%23c5c5c5&bgColorDefault=%23f6f6f6 +* Copyright jQuery Foundation and other contributors; Licensed MIT */ + +.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;font-size:100%}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-button{padding:.4em 1em;display:inline-block;position:relative;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2em;box-sizing:border-box;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-button-icon-only{text-indent:0}.ui-button-icon-only .ui-icon{position:absolute;top:50%;left:50%;margin-top:-8px;margin-left:-8px}.ui-button.ui-icon-notext .ui-icon{padding:0;width:2.1em;height:2.1em;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-icon-notext .ui-icon{width:auto;height:auto;text-indent:0;white-space:normal;padding:.4em 1em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-controlgroup{vertical-align:middle;display:inline-block}.ui-controlgroup > .ui-controlgroup-item{float:left;margin-left:0;margin-right:0}.ui-controlgroup > .ui-controlgroup-item:focus,.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus{z-index:9999}.ui-controlgroup-vertical > .ui-controlgroup-item{display:block;float:none;width:100%;margin-top:0;margin-bottom:0;text-align:left}.ui-controlgroup-vertical .ui-controlgroup-item{box-sizing:border-box}.ui-controlgroup .ui-controlgroup-label{padding:.4em 1em}.ui-controlgroup .ui-controlgroup-label span{font-size:80%}.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item{border-left:none}.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item{border-top:none}.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content{border-right:none}.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content{border-bottom:none}.ui-controlgroup-vertical .ui-spinner-input{width:75%;width:calc( 100% - 2.4em )}.ui-controlgroup-vertical .ui-spinner .ui-spinner-up{border-top-style:solid}.ui-checkboxradio-label .ui-icon-background{box-shadow:inset 1px 1px 1px #ccc;border-radius:.12em;border:none}.ui-checkboxradio-radio-label .ui-icon-background{width:16px;height:16px;border-radius:1em;overflow:visible;border:none}.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon{background-image:none;width:8px;height:8px;border-width:4px;border-style:solid}.ui-checkboxradio-disabled{pointer-events:none}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker .ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat;left:.5em;top:.3em}.ui-dialog{position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-n{height:2px;top:0}.ui-dialog .ui-resizable-e{width:2px;right:0}.ui-dialog .ui-resizable-s{height:2px;bottom:0}.ui-dialog .ui-resizable-w{width:2px;left:0}.ui-dialog .ui-resizable-se,.ui-dialog .ui-resizable-sw,.ui-dialog .ui-resizable-ne,.ui-dialog .ui-resizable-nw{width:7px;height:7px}.ui-dialog .ui-resizable-se{right:0;bottom:0}.ui-dialog .ui-resizable-sw{left:0;bottom:0}.ui-dialog .ui-resizable-ne{right:0;top:0}.ui-dialog .ui-resizable-nw{left:0;top:0}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");height:100%;filter:alpha(opacity=25);opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-text{display:block;margin-right:20px;overflow:hidden;text-overflow:ellipsis}.ui-selectmenu-button.ui-button{text-align:left;white-space:nowrap;width:14em}.ui-selectmenu-icon.ui-icon{float:right;margin-top:0}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:.222em 0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:2em}.ui-spinner-button{width:1.6em;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top-style:none;border-bottom-style:none;border-right-style:none}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #c5c5c5}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #c5c5c5;background:#f6f6f6;font-weight:normal;color:#454545}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{color:#454545;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus,.ui-button:hover,.ui-button:focus{border:1px solid #ccc;background:#ededed;font-weight:normal;color:#2b2b2b}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#2b2b2b;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #003eff;background:#007fff;font-weight:normal;color:#fff}.ui-icon-background,.ui-state-active .ui-icon-background{border:#003eff;background-color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #dad55e;background:#fffa90;color:#777620}.ui-state-checked{border:1px solid #dad55e;background:#fffa90}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#777620}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #f1a899;background:#fddfdf;color:#5f3f3f}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#5f3f3f}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#5f3f3f}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:url("images/ui-icons_555555_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("images/ui-icons_777620_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cc0000_256x240.png")}.ui-button .ui-icon{background-image:url("images/ui-icons_777777_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:3px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:3px}.ui-widget-overlay{background:#aaa;opacity:.003;filter:Alpha(Opacity=.3)}.ui-widget-shadow{-webkit-box-shadow:0 0 5px #666;box-shadow:0 0 5px #666} \ No newline at end of file diff --git a/app/ui/static/css/jquery.tagit.css b/app/ui/static/css/jquery.tagit.css new file mode 100644 index 00000000..f18650d9 --- /dev/null +++ b/app/ui/static/css/jquery.tagit.css @@ -0,0 +1,69 @@ +ul.tagit { + padding: 1px 5px; + overflow: auto; + margin-left: inherit; /* usually we don't want the regular ul margins. */ + margin-right: inherit; +} +ul.tagit li { + display: block; + float: left; + margin: 2px 5px 2px 0; +} +ul.tagit li.tagit-choice { + position: relative; + line-height: inherit; +} +input.tagit-hidden-field { + display: none; +} +ul.tagit li.tagit-choice-read-only { + padding: .2em .5em .2em .5em; +} + +ul.tagit li.tagit-choice-editable { + padding: .2em 18px .2em .5em; +} + +ul.tagit li.tagit-new { + padding: .25em 4px .25em 0; +} + +ul.tagit li.tagit-choice a.tagit-label { + cursor: pointer; + text-decoration: none; +} +ul.tagit li.tagit-choice .tagit-close { + cursor: pointer; + position: absolute; + right: .1em; + top: 50%; + margin-top: -8px; + line-height: 17px; +} + +/* used for some custom themes that don't need image icons */ +ul.tagit li.tagit-choice .tagit-close .text-icon { + display: none; +} + +ul.tagit li.tagit-choice input { + display: block; + float: left; + margin: 2px 5px 2px 0; +} +ul.tagit input[type="text"] { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; + + border: none; + margin: 0; + padding: 0; + width: inherit; + background-color: inherit; + outline: none; +} diff --git a/app/ui/static/css/tagit.ui-zendesk.css b/app/ui/static/css/tagit.ui-zendesk.css new file mode 100644 index 00000000..b91181bf --- /dev/null +++ b/app/ui/static/css/tagit.ui-zendesk.css @@ -0,0 +1,98 @@ + +/* Optional scoped theme for tag-it which mimics the zendesk widget. */ + + +ul.tagit { + border-style: solid; + border-width: 1px; + border-color: #C6C6C6; + background: inherit; +} +ul.tagit li.tagit-choice { + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-border-radius: 6px; + border: 1px solid #CAD8F3; + + background: none; + background-color: #DEE7F8; + + font-weight: normal; +} +ul.tagit li.tagit-choice .tagit-label:not(a) { + color: #555; +} +ul.tagit li.tagit-choice a.tagit-close { + text-decoration: none; +} +ul.tagit li.tagit-choice .tagit-close { + right: .4em; +} +ul.tagit li.tagit-choice .ui-icon { + display: none; +} +ul.tagit li.tagit-choice .tagit-close .text-icon { + display: inline; + font-family: arial, sans-serif; + font-size: 16px; + line-height: 16px; + color: #777; +} +ul.tagit li.tagit-choice:hover, ul.tagit li.tagit-choice.remove { + background-color: #bbcef1; + border-color: #6d95e0; +} +ul.tagit li.tagit-choice a.tagLabel:hover, +ul.tagit li.tagit-choice a.tagit-close .text-icon:hover { + color: #222; +} +ul.tagit input[type="text"] { + color: #333333; + background: none; +} +.ui-widget { + font-size: 1.1em; +} + +/* Forked from a jQuery UI theme, so that we don't require the jQuery UI CSS as a dependency. */ +.tagit-autocomplete.ui-autocomplete { position: absolute; cursor: default; } +* html .tagit-autocomplete.ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */ +.tagit-autocomplete.ui-menu { + list-style:none; + padding: 2px; + margin: 0; + display:block; + float: left; +} +.tagit-autocomplete.ui-menu .ui-menu { + margin-top: -3px; +} +.tagit-autocomplete.ui-menu .ui-menu-item { + margin:0; + padding: 0; + zoom: 1; + float: left; + clear: left; + width: 100%; +} +.tagit-autocomplete.ui-menu .ui-menu-item a { + text-decoration:none; + display:block; + padding:.2em .4em; + line-height:1.5; + zoom:1; +} +.tagit-autocomplete .ui-menu .ui-menu-item a.ui-state-hover, +.tagit-autocomplete .ui-menu .ui-menu-item a.ui-state-active { + font-weight: normal; + margin: -1px; +} +.tagit-autocomplete.ui-widget-content { border: 1px solid #aaaaaa; background: #ffffff 50% 50% repeat-x; color: #222222; } +.tagit-autocomplete.ui-corner-all, .tagit-autocomplete .ui-corner-all { -moz-border-radius: 4px; -webkit-border-radius: 4px; -khtml-border-radius: 4px; border-radius: 4px; } +.tagit-autocomplete .ui-state-hover, .tagit-autocomplete .ui-state-focus { border: 1px solid #999999; background: #dadada; font-weight: normal; color: #212121; } +.tagit-autocomplete .ui-state-active { border: 1px solid #aaaaaa; } + +.tagit-autocomplete .ui-widget-content { border: 1px solid #aaaaaa; } +.tagit .ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px,1px,1px,1px); } + + diff --git a/app/ui/static/js/app.js b/app/ui/static/js/app.js index 9096b2a0..dce678f6 100644 --- a/app/ui/static/js/app.js +++ b/app/ui/static/js/app.js @@ -49,7 +49,8 @@ var App = App || { }, getInfoSuccess:function(response){ this.info = response.data; - $('#stamplike').text('LXD Version: ' + this.info.environment.server_version); + this.lxdVersion = this.info.environment.server_version; + $('#stamplike').text('LXD Version: ' + this.lxdVersion); $('#stamplike').removeClass('label-danger').addClass('label-success'); }, getInfoError:function(response) { @@ -63,7 +64,7 @@ var App = App || { if(window.location.href!==WEB){ $.ajaxSetup({ headers:{ - Authorization:'JWT '+sessionStorage.getItem('authToken'), + Authorization:'Bearer '+sessionStorage.getItem('authToken'), // 'Content-Type':'application/json' //commented for file upload as temporary workaround }, beforeSend: function(xhr, settings) { @@ -95,7 +96,7 @@ var App = App || { updateTokenExpiration: function(){ App.tokenRefreshing = true; $.ajax({ - url:App.baseAPI+'user/login', + url:App.baseAPI+'user/refresh', method:'POST', contentType: "application/json; charset=utf-8", dataType:'json', @@ -104,7 +105,10 @@ var App = App || { }); }, tokenUpdateSuccess:function(response){ + sessionStorage.removeItem('authToken'); + document.cookie = 'access_token_cookie' + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'; sessionStorage.setItem('authToken', response.access_token); + document.cookie = "access_token_cookie" + "=" + response.access_token + ";path=/"; App.tokenRefreshing=false; }, setActiveLink: function(name) { @@ -161,4 +165,4 @@ var App = App || { $(function(){ App.init(); -}); \ No newline at end of file +}); diff --git a/app/ui/static/js/container-details.js b/app/ui/static/js/container-details.js index c5a18ac6..c0db2cb9 100644 --- a/app/ui/static/js/container-details.js +++ b/app/ui/static/js/container-details.js @@ -66,12 +66,18 @@ App.containerDetails = App.containerDetails || { $('#buttonExportContainerDetail').on('click', $.proxy(this.showExportContainer, this)); $('#buttonSnapshotContainerDetail').on('click', $.proxy(this.showSnapshotContainer, this)); - $('#cloneContainerForm').on('submit', $.proxy(this.cloneContainerDetail, this)); - $('#moveContainerForm').on('submit', $.proxy(this.moveContainerDetail, this)); - $('#exportContainerForm').on('submit', $.proxy(this.exportContainerDetail, this)); - $('#snapshotContainerForm').on('submit', $.proxy(this.snapshotContainerDetail, this)); + $('#cloneContainerSubmit').on('submit', $.proxy(this.cloneContainerDetail, this)); + $('#moveContainerSubmit').on('submit', $.proxy(this.moveContainerDetail, this)); + $('#exportContainerSubmit').on('submit', $.proxy(this.exportContainerDetail, this)); + $('#snapshotContainerSubmit').on('submit', $.proxy(this.snapshotContainerDetail, this)); $('#containerFromSnapshotForm').on('submit', $.proxy(this.newContainerFromSnapshotDetail, this)); + $('#cloneContainerSubmit').on('click', $.proxy(this.cloneContainerDetail, this)); + $('#moveContainerSubmit').on('click', $.proxy(this.moveContainerDetail, this)); + $('#exportContainerSubmit').on('click', $.proxy(this.exportContainerDetail, this)); + $('#snapshotContainerSubmit').on('click', $.proxy(this.snapshotContainerDetail, this)); + $('#containerFromSnapshotForm').on('click', $.proxy(this.newContainerFromSnapshotDetail, this)); + $('.profileTag').on('click', $.proxy(this.deleteProfile, this)); $('#buttonAdd').on('click', $.proxy(this.onAddProfileClick, this)); $('.formModifier').on('click', $.proxy(this.formChanged, this)); @@ -130,7 +136,7 @@ App.containerDetails = App.containerDetails || { this.activeNode = $("#tree").fancytree('getActiveNode'); var activeNode = this.activeNode; if (!activeNode.folder) - this.downloadURI(App.baseAPI+'file/download/container/' + this.name + '?path=' + activeNode.getKeyPath() + "&token=" + sessionStorage.getItem('authToken')) + this.downloadURI(App.baseAPI+'file/download/container/' + this.name + '?path=' + activeNode.getKeyPath()) }, home: function() { $("#tree").fancytree('getTree').visit(function(node) { @@ -292,6 +298,9 @@ App.containerDetails = App.containerDetails || { '
' + '
' + '' + + '' + + '' + + '' + '
' + '
' + '
' + @@ -827,4 +836,4 @@ App.containerDetails = App.containerDetails || { success:$.proxy(this.removeProxySuccess, this) }); } -} \ No newline at end of file +} diff --git a/app/ui/static/js/container.js b/app/ui/static/js/container.js index d6f63fae..21aea699 100644 --- a/app/ui/static/js/container.js +++ b/app/ui/static/js/container.js @@ -97,7 +97,6 @@ App.containers = App.containers || { $('#containerCPUPercentage').on('change', $.proxy(this.updateValue, this, $('#cpu_percentage'))); $('#memory_percentage').on('change', $.proxy(this.updateValue, this, $('#containerMemoryPercentage'))); $('#containerMemoryPercentage').on('change', $.proxy(this.updateValue, this, $('#memory_percentage'))); - if(window.location.hash && window.location.hash=='#createContainer') this.switchView('form'); @@ -128,6 +127,9 @@ App.containers = App.containers || { '
' + '
' + '' + + '' + + '' + + '' + '
' + '
' + '
' + @@ -295,11 +297,20 @@ App.containers = App.containers || { cpu:{ percentage: Number(formData.cpu.percentage), hardLimitation: formData.cpu['hardLimitation']?true:false, + cores: Number(formData.cpu.cores), }, memory:{ sizeInMB: Number(formData.memory.sizeInMB), hardLimitation: formData.memory['hardLimitation']?true:false }, + devices:{ + root:{ + path: '/', + pool: formData.storagePool['name'], + size: formData.storagePool['size']+'GB', + type: 'disk', + } + }, profiles:formData.profiles }; }, @@ -408,7 +419,7 @@ App.containers = App.containers || { onCloneSuccess: function(response){ console.log(response); console.log('clonedSuccess:', 'TODO - add alert and refresh local data'); - $("#myModal").modal("hide"); + $("#cloneContainerModal").modal("hide"); location.reload(); }, moveContainer: function() { @@ -426,7 +437,7 @@ App.containers = App.containers || { onMoveSuccess: function(response){ console.log(response); console.log('Moved Success:', 'TODO - add alert and refresh local data'); - $("#myModal").modal("hide"); + $("#moveContainerModal").modal("hide"); location.reload(); }, exportContainer: function() { @@ -445,7 +456,7 @@ App.containers = App.containers || { onExportSuccess: function(response){ console.log(response); console.log('Export Success:', 'TODO - add alert and refresh local data'); - $("#myModal").modal("hide"); + $("#exportContainerModal").modal("hide"); }, snapshotContainer: function() { $.ajax({ @@ -462,7 +473,7 @@ App.containers = App.containers || { onSnapshotSuccess: function(response){ console.log(response); console.log('Snapshot Success:', 'TODO - add alert and refresh local data'); - $("#myModal").modal("hide"); + $("#snapshotContainerModal").modal("hide"); }, toggleSelectAll(event){ if(event.target.checked){ @@ -475,6 +486,6 @@ App.containers = App.containers || { target.val(event.target.value); }, showTerminalContainer: function(container) { - window.open('/terminal/new/' + container + '/' + sessionStorage.getItem('authToken'), '_blank'); + window.open('/terminal/new/' + container + '/', '_blank'); } -} \ No newline at end of file +} diff --git a/app/ui/static/js/images.js b/app/ui/static/js/images.js index 87eb37c3..3907f5df 100644 --- a/app/ui/static/js/images.js +++ b/app/ui/static/js/images.js @@ -7,6 +7,7 @@ App.images = App.images || { tableLocal: null, tableRemote: null, tableNightly: null, + tableHub: null, tableSettings: { searching:true, responsive: { @@ -38,12 +39,16 @@ App.images = App.images || { tempButton.on('click', $.proxy(App.images.showJSON, App.images)); $('#'+$(this).closest('table').attr('id')+'_wrapper .json-place').prepend(tempButton); tempButton.show(); - }, + } }, containerTemplate:null, newContainerForm:null, + publishImageForm: null, itemTemplate:null, rawJson:null, + simplemde:null, + publishPage:0, + publishFormValid:false, init: function(opts){ this.data = constLocalImages || []; this.remoteData = constRemoteImages || []; @@ -54,14 +59,17 @@ App.images = App.images || { $('#btnLocalImages').on('click', $.proxy(this.switchView, this, 'localList')); $('#btnRemoteImages').on('click', $.proxy(this.switchView, this, 'remoteList')); $('#btnNightlyImages').on('click', $.proxy(this.switchView, this, 'nightlyList')); + $('#btnHubImages').on('click', $.proxy(this.switchView, this, 'hubList')); $('#buttonUpdate').on('click', $.proxy(this.getData, this)); $('#buttonDelete').on('click', $.proxy(this.doDeleteLocalImages, this)); $('#buttonDownload').on('click', $.proxy(this.doDownload, this)); $('#buttonDownloadNightly').on('click', $.proxy(this.doDownload, this)); + $('#buttonDownloadHub').on('click', $.proxy(this.doDownload, this)); $('#buttonLaunchContainers').on('click', $.proxy(this.launchContainers, this)); $('#buttonBack').on('click', $.proxy(this.switchView, this, 'localList')); $('.image').on('click', $.proxy(this.setActive, this)); + $('#buttonPublish').on('click', $.proxy(this.publishImage, this)); App.setActiveLink('image'); this.newContainerForm = $('#newContainerForm'); this.newContainerForm.on('submit', $.proxy(this.doCreateContainer, this)); @@ -69,16 +77,54 @@ App.images = App.images || { this.initLocalTable(); this.initRemoteTable(); this.initNightlyTable(); + this.initHubTable(); + + this.tableLocal.on('select', $.proxy(this.onRowSelected, this)); + this.tableLocal.on('deselect', $.proxy(this.onRowSelected, this)); + $('#selectAllLocal').on('change', $.proxy(this.toggleSelectAll, this, 'Local')); $('#selectAllRemote').on('change', $.proxy(this.toggleSelectAll, this, 'Remote')); this.itemTemplate = $('.itemTemplate').clone(); $('#modalDownloadButton').on('click', $.proxy(this.doDownload, this)); $('#exTab2 > ul > li:nth-child(1)').addClass('active');// set first tab as active - + $('#exTab > ul > li:nth-child(1)').addClass('active');// set first tab as active $('#architectureRemote').on('change', $.proxy(this.filterRemoteTable, this)); $('#architectureNightly').on('change', $.proxy(this.filterNightlyTable, this)); + this.publishImageForm = $('#publishImageToHubForm'); + this.publishImageForm.on('submit', $.proxy(this.doPublishImage, this)); + $('#publishToHub').on('click', $.proxy(this.doPublishImage, this)); + + this.simplemde = new SimpleMDE({ + element: document.getElementById("documentation"), + spellChecker: false, + hideIcons: ["side-by-side", "fullscreen"], + }); this.initKeyValuePairs(); + + if (architecture == 'x86_64') { + architecture = 'amd64'; + } + this.tableRemote.search(architecture).draw(); + this.tableNightly.search(architecture).draw(); + + $('#architectureNightly').val(architecture); + $('#architectureRemote').val(architecture); + + if (localImagesLength == 0){ + this.switchView('nightlyList'); + $('.nav-tabs li:eq(1) a').tab('show'); + } + $('#buttonPublishNext').on('click', $.proxy(this.onPublishNext, this)); + $('#buttonPublishBack').on('click', $.proxy(this.onPublishBack, this)); + + $('#btnImageDetails').on('click', $.proxy(this.onPublishSwitchToPage, this, 0)); + $('#btnReadme').on('click', $.proxy(this.onPublishSwitchToPage, this, 1)); + $('#btnAuthorization').on('click', $.proxy(this.onPublishSwitchToPage, this, 2)); + $('#publishImageToHubForm').parsley().on('form:validate', $.proxy(this.onPublishFormValidation, this)); + $('#imageTags').tagit({ + fieldName:'imageTags' + }); }, convertImageSize:function(index, item){ $(item).text(App.formatBytes($(item).text())); @@ -108,6 +154,9 @@ App.images = App.images || { '
' + '
' + '' + + '' + + '' + + '' + '
' + '
' + '
' + @@ -143,12 +192,28 @@ App.images = App.images || { this.tableNightly.on('select', $.proxy(this.onItemSelectChange, this)); this.tableNightly.on('deselect', $.proxy(this.onItemSelectChange, this)); }, + initHubTable: function() { + this.tableHub =$('#tableImagesHub').DataTable(App.mergeProps(this.tableSettings, {rowId: 'fingerprint'})); + this.setHubTableEvents(); + }, + setHubTableEvents: function() { + this.tableHub.on('select', $.proxy(this.onItemSelectChange, this)); + this.tableHub.on('deselect', $.proxy(this.onItemSelectChange, this)); + }, filterRemoteTable: function(e) { this.tableRemote.search(e.target.value).draw(); }, filterNightlyTable: function(e) { this.tableNightly.search(e.target.value).draw(); }, + onRowSelected: function(e, dt, type, indexes ){ + if(this.tableLocal.rows({selected:true}).count() == 1){ + $('#buttonPublish').removeAttr('disabled', 'disabled'); + } + else { + $('#buttonPublish').attr('disabled', 'disabled'); + } + }, onItemSelectChange : function(e, dt, type, indexes ){ if(this.activeTab=='local'){ var state = this.tableLocal.rows({selected:true}).count()>0; @@ -172,6 +237,13 @@ App.images = App.images || { $('#selectAllNightly').prop('checked',this.tableNightly.rows({selected:true}).count()==this.tableNightly.rows().count()); return; } + if(this.activeTab=='hub'){ + var state = this.tableHub.rows({selected:true}).count()>0 + var visibility= !state?'attr':'removeAttr'; + $('#buttonDownloadHub')[visibility]('disabled', 'disabled'); + $('#selectAllHub').prop('checked',this.tableHub.rows({selected:true}).count()==this.tableHub.rows().count()); + return; + } }, doDeleteLocalImages: function(){ this.tableLocal.rows( { selected: true } ).data().map(function(row){ @@ -191,7 +263,9 @@ App.images = App.images || { }, doDownload: function(){ activeTab = this.activeTab; - $('#modalDownloadButton').attr('disabled', 'disabled'); + $('#modalDownloadButton').hide(); + toastr.success('Image is being downloaded','Downloading'); + $('.imageDownloadLoader').show(); if(activeTab=='nightly') { this.tableNightly.rows({selected: true}).data().map(function(row){ $.ajax({ @@ -220,6 +294,20 @@ App.images = App.images || { }); this.tableRemote.row('#'+row['image']).remove().draw(false); }.bind(this)); + } else if(activeTab=='hub') { + this.tableHub.rows({selected: true}).data().map(function(row){ + $.ajax({ + url:App.baseAPI+'image/hub', + type: 'POST', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify({ + image:row['fingerprint'] + }), + success: $.proxy(this.onDownloadSuccess, this, row['fingerprint']) + }); + //this.tableHub.row('#'+row['image']).remove().draw(false); + }.bind(this)); } }, onDownloadSuccess: function(imageName, response){ @@ -232,6 +320,9 @@ App.images = App.images || { //return $.get(App.baseAPI+'image', $.proxy(this.getDataSuccess, this)); if(this.activeTab=='remote') return $.get(App.baseAPI+'image/remote', $.proxy(this.getDataSuccess, this)); + + if(this.activeTab=='nightly') + return $.get(App.baseAPI+'image/remote/nightly/list', $.proxy(this.getDataSuccess, this)); }, getDataJSON: function(){ //this.setLoading(true); @@ -241,10 +332,14 @@ App.images = App.images || { return $.get(App.baseAPI+'image/remote', $.proxy(this.getDataSuccess, this)); if(this.activeTab=='nightly') return $.get(App.baseAPI+'image/remote/nightly/list', $.proxy(this.getDataSuccess, this)); + if(this.activeTab=='hub') + return $.get(App.baseAPI+'image/remote/hub/list', $.proxy(this.getDataSuccess, this)); }, activateScreen: function(screen){ this.tableLocal.rows({selected:true}).deselect(); this.tableRemote.rows({selected:true}).deselect(); + this.tableNightly.rows({selected:true}).deselect(); + this.tableHub.rows({selected:true}).deselect(); $('.mg-bottom15').show(); if(screen==='local'){ $('#tableImagesLocalWrapper').show(); @@ -265,10 +360,16 @@ App.images = App.images || { $('#buttonDownload').show(); $('#buttonDelete').hide(); this.activeTab = 'remote'; + this.getData(); return; } - if(screen=='nightly') { + if(screen==='nightly') { this.activeTab = 'nightly'; + this.getData(); + return; + } + if(screen==='hub') { + this.activeTab = 'hub'; return; } }, @@ -295,6 +396,7 @@ App.images = App.images || { })); }, updateRemoteTable: function(jsonData){ + $('#remoteLength').text(jsonData.length); this.remoteData = jsonData; this.tableRemote.clear(); this.tableRemote.destroy(); @@ -303,12 +405,78 @@ App.images = App.images || { data : this.remoteData, columns : [ { title:'Select', data: null, defaultContent:''}, - { title: 'Distribution', data : 'distribution' }, - { title: 'Architecture', data : 'architecture' }, - { title: 'Image', data : 'image' }, - { title: 'Name', data : 'name' } + { title: 'OS', data : 'image', render(field, type, full, meta) { + return ''+full.name+'' + }}, + { title: 'Description', data: null, render(field, type, full, meta) { + return ''+full.name+' '+full.distribution+' ' + full.architecture + + '' + }}, + { title: 'Alias', data : 'image' }, + { title: 'Ver', data : 'distribution' }, + { title: 'Arch', data : 'architecture' } ] })); + this.tableRemote.search(architecture).draw(); + }, + updateNightlyTable: function(jsonData){ + $('#nightlyLength').text(jsonData.length); + this.nightlyData = jsonData; + this.tableNightly.clear(); + this.tableNightly.destroy(); + this.tableNightly=$('#tableImagesNightly').DataTable(App.mergeProps(this.tableSettings, { + rowId:'fingerprint', + data : this.nightlyData, + columns : [ + { title:'Select', data: null, defaultContent:''}, + { title: 'OS', data : null, render(field, type, full, meta) { + if(full.metadata.properties['description'] === undefined) { + return ''+full.metadata.name+'' + } + else { + return ''+full.metadata.properties['os']+''; + } + + }}, + { title: 'Description', data: null, render(field, type, full, meta) { + if(full.metadata.properties['description'] === undefined) { + return full.metadata.aliases[0].description; + } + else { + return full.metadata.properties.description; + } + }}, + { title: 'Alias', data : null, render(field, type, full, meta) { + if(full.metadata.properties['description'] === undefined) { + return full.metadata.aliases[0]['description']; + } + else { + var aliases = ''; + for (i=0; i' + } + + return aliases; + } + } }, + { title: 'Ver', data : null, render(field, type, full, meta) { + return full.metadata.properties['release']; + } }, + { title: 'Arch', data : null, render(field, type, full, meta) { + return full.metadata.properties['architecture']; + } }, + { title: 'Size', data: null, render(field, type, full, meta) { + return ''+full.metadata.size+''; + }} + ] + })); + this.tableNightly.search(architecture).draw(); + $('#tableImagesNightly .imageSize').each(this.convertImageSize); }, getDataSuccess: function(response){ this.setLoading(false); @@ -316,9 +484,12 @@ App.images = App.images || { // if(this.activeTab=='local'){ // this.updateLocalTable(response.data); // } -// if(this.activeTab == 'remote'){ -// this.updateRemoteTable(response.data); -// } + if(this.activeTab == 'remote'){ + this.updateRemoteTable(response.data); + } + if (this.activeTab == 'nightly') { + this.updateNightlyTable(response.data); + } }, showJSON: function(e) { this.rawJson.setValue(''); @@ -424,6 +595,9 @@ App.images = App.images || { if(view=='nightlyList'){ return this.activateScreen('nightly'); } + if (view=='hubList') { + return this.activateScreen('hub'); + } $('#buttonLaunchContainers').hide(); $('#buttonDelete').hide(); $('#rawJSONImages').hide(); @@ -564,7 +738,7 @@ App.images = App.images || { var aliasesList = $('#aliasesList'); tempData.aliases.forEach(function(alias, index){ - aliasesList.append('
Alias '+(index+1)+'
') + aliasesList.append('
Alias '+(index+1)+'
') aliasesList.append(this.generateItem('Description',alias.description)); aliasesList.append(this.generateItem('Name',alias.name)); aliasesList.append(this.generateItem('Target',alias.target)); @@ -572,5 +746,117 @@ App.images = App.images || { modalBody.append(aliasesList); $('#myModal').modal().show(); + }, + publishImage: function(e) { + $("#publishImageModal").modal("show"); + var image = this.getImageByFingerPrint(this.data, this.tableLocal.rows({selected:true}).data()[0]['fingerprint']); + + $('#lxcVersion').text(App.lxdVersion); + $('#fingerprint').text(image.fingerprint); + $('#source').text('NA'); + $('#size').text(App.formatBytes(image.size)); + $('#architecture').text(image.architecture); + $('#os').text(image.properties['os']); + $('#release').text(image.properties['release']); + this.publishPage=0; + this.updatePublishButtons(); + }, + doPublishImage: function(e){ + e.preventDefault(); + if(!$("#publishImageToHubForm").parsley().validate()){ +// if(!this.publishFormValid){ + return false; + } + var image = this.getImageByFingerPrint(this.data, this.tableLocal.rows({selected:true}).data()[0]['fingerprint']); + + var logoImg = $('input[name="logo"]').get(0).files[0]; + + var tempJSON = this.publishImageForm.serializeJSON(); + + var formData = new FormData(); + formData.append('logo', logoImg); + + tempJSON['fingerprint'] = image.fingerprint; + tempJSON['documentation'] = this.simplemde.value(); + + formData.append('input', JSON.stringify(tempJSON)); + + console.log('formData', formData); + + $.ajax({ + url: App.baseAPI +'image/hub/publish', + type:'POST', + processData: false, + contentType: false, + enctype: 'multipart/form-data', + data: formData, + success: $.proxy(this.onPublishSuccess, this), + error: $.proxy(this.onPublishFailed, this) + }); + }, + onPublishSuccess: function(response){ +// location.reload(); + }, + onPublishFailed: function(response) { + console.log('failed'); + }, + updatePublishButtons: function(){ + $('.tabImageDetails, .tabImageReadme, .tabImageAuthorization').removeClass('active'); + switch(this.publishPage){ + case 0: + $('#buttonPublishCancel').show(); + $('#buttonPublishNext').show(); + $('#buttonPublishBack').hide(); + $('#buttonPublishToHUB').hide(); + $('.tabImageDetails').addClass('active'); + $('#5').show(); + $('#6, #7').hide(); + return; + case 1: + $('#buttonPublishCancel').hide(); + $('#buttonPublishToHUB').hide(); + $('#buttonPublishBack').show(); + $('#buttonPublishNext').show(); + $('.tabImageReadme').addClass('active'); + $('#6').show(); + $('#5, #7').hide(); + return; + case 2: + $('#buttonPublishCancel').hide(); + $('#buttonPublishNext').hide(); + $('#buttonPublishBack').show(); + $('#buttonPublishToHUB').show(); + $('.tabImageAuthorization').addClass('active'); + $('#7').show(); + $('#6, #5').hide(); + return; + } + }, + onPublishBack: function(){ + this.publishPage--; + this.updatePublishButtons(); + }, + onPublishNext: function(){ + console.log('here'); + this.publishPage++; + this.updatePublishButtons(); + }, + onPublishSwitchToPage:function(pageNumber){ + this.publishPage=pageNumber; + this.updatePublishButtons(); + }, + onPublishFormValidation: function(formInstance){ + if(!formInstance.isValid({group: 'block1'})){ + formInstance.valiidationResult = false; + return this.onPublishSwitchToPage(0); + } + if(!formInstance.isValid({group: 'block2'})){ + formInstance.valiidationResult = false; + return this.onPublishSwitchToPage(1); + } + if(!formInstance.isValid({group: 'block3'})){ + formInstance.valiidationResult = false; + return this.onPublishSwitchToPage(2); + } } } \ No newline at end of file diff --git a/app/ui/static/js/jquery-ui.min.js b/app/ui/static/js/jquery-ui.min.js new file mode 100644 index 00000000..25398a16 --- /dev/null +++ b/app/ui/static/js/jquery-ui.min.js @@ -0,0 +1,13 @@ +/*! jQuery UI - v1.12.1 - 2016-09-14 +* http://jqueryui.com +* Includes: widget.js, position.js, data.js, disable-selection.js, effect.js, effects/effect-blind.js, effects/effect-bounce.js, effects/effect-clip.js, effects/effect-drop.js, effects/effect-explode.js, effects/effect-fade.js, effects/effect-fold.js, effects/effect-highlight.js, effects/effect-puff.js, effects/effect-pulsate.js, effects/effect-scale.js, effects/effect-shake.js, effects/effect-size.js, effects/effect-slide.js, effects/effect-transfer.js, focusable.js, form-reset-mixin.js, jquery-1-7.js, keycode.js, labels.js, scroll-parent.js, tabbable.js, unique-id.js, widgets/accordion.js, widgets/autocomplete.js, widgets/button.js, widgets/checkboxradio.js, widgets/controlgroup.js, widgets/datepicker.js, widgets/dialog.js, widgets/draggable.js, widgets/droppable.js, widgets/menu.js, widgets/mouse.js, widgets/progressbar.js, widgets/resizable.js, widgets/selectable.js, widgets/selectmenu.js, widgets/slider.js, widgets/sortable.js, widgets/spinner.js, widgets/tabs.js, widgets/tooltip.js +* Copyright jQuery Foundation and other contributors; Licensed MIT */ + +(function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)})(function(t){function e(t){for(var e=t.css("visibility");"inherit"===e;)t=t.parent(),e=t.css("visibility");return"hidden"!==e}function i(t){for(var e,i;t.length&&t[0]!==document;){if(e=t.css("position"),("absolute"===e||"relative"===e||"fixed"===e)&&(i=parseInt(t.css("zIndex"),10),!isNaN(i)&&0!==i))return i;t=t.parent()}return 0}function s(){this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},t.extend(this._defaults,this.regional[""]),this.regional.en=t.extend(!0,{},this.regional[""]),this.regional["en-US"]=t.extend(!0,{},this.regional.en),this.dpDiv=n(t("
"))}function n(e){var i="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return e.on("mouseout",i,function(){t(this).removeClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&t(this).removeClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&t(this).removeClass("ui-datepicker-next-hover")}).on("mouseover",i,o)}function o(){t.datepicker._isDisabledDatepicker(m.inline?m.dpDiv.parent()[0]:m.input[0])||(t(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),t(this).addClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&t(this).addClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&t(this).addClass("ui-datepicker-next-hover"))}function a(e,i){t.extend(e,i);for(var s in i)null==i[s]&&(e[s]=i[s]);return e}function r(t){return function(){var e=this.element.val();t.apply(this,arguments),this._refresh(),e!==this.element.val()&&this._trigger("change")}}t.ui=t.ui||{},t.ui.version="1.12.1";var h=0,l=Array.prototype.slice;t.cleanData=function(e){return function(i){var s,n,o;for(o=0;null!=(n=i[o]);o++)try{s=t._data(n,"events"),s&&s.remove&&t(n).triggerHandler("remove")}catch(a){}e(i)}}(t.cleanData),t.widget=function(e,i,s){var n,o,a,r={},h=e.split(".")[0];e=e.split(".")[1];var l=h+"-"+e;return s||(s=i,i=t.Widget),t.isArray(s)&&(s=t.extend.apply(null,[{}].concat(s))),t.expr[":"][l.toLowerCase()]=function(e){return!!t.data(e,l)},t[h]=t[h]||{},n=t[h][e],o=t[h][e]=function(t,e){return this._createWidget?(arguments.length&&this._createWidget(t,e),void 0):new o(t,e)},t.extend(o,n,{version:s.version,_proto:t.extend({},s),_childConstructors:[]}),a=new i,a.options=t.widget.extend({},a.options),t.each(s,function(e,s){return t.isFunction(s)?(r[e]=function(){function t(){return i.prototype[e].apply(this,arguments)}function n(t){return i.prototype[e].apply(this,t)}return function(){var e,i=this._super,o=this._superApply;return this._super=t,this._superApply=n,e=s.apply(this,arguments),this._super=i,this._superApply=o,e}}(),void 0):(r[e]=s,void 0)}),o.prototype=t.widget.extend(a,{widgetEventPrefix:n?a.widgetEventPrefix||e:e},r,{constructor:o,namespace:h,widgetName:e,widgetFullName:l}),n?(t.each(n._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,o,i._proto)}),delete n._childConstructors):i._childConstructors.push(o),t.widget.bridge(e,o),o},t.widget.extend=function(e){for(var i,s,n=l.call(arguments,1),o=0,a=n.length;a>o;o++)for(i in n[o])s=n[o][i],n[o].hasOwnProperty(i)&&void 0!==s&&(e[i]=t.isPlainObject(s)?t.isPlainObject(e[i])?t.widget.extend({},e[i],s):t.widget.extend({},s):s);return e},t.widget.bridge=function(e,i){var s=i.prototype.widgetFullName||e;t.fn[e]=function(n){var o="string"==typeof n,a=l.call(arguments,1),r=this;return o?this.length||"instance"!==n?this.each(function(){var i,o=t.data(this,s);return"instance"===n?(r=o,!1):o?t.isFunction(o[n])&&"_"!==n.charAt(0)?(i=o[n].apply(o,a),i!==o&&void 0!==i?(r=i&&i.jquery?r.pushStack(i.get()):i,!1):void 0):t.error("no such method '"+n+"' for "+e+" widget instance"):t.error("cannot call methods on "+e+" prior to initialization; "+"attempted to call method '"+n+"'")}):r=void 0:(a.length&&(n=t.widget.extend.apply(null,[n].concat(a))),this.each(function(){var e=t.data(this,s);e?(e.option(n||{}),e._init&&e._init()):t.data(this,s,new i(n,this))})),r}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
",options:{classes:{},disabled:!1,create:null},_createWidget:function(e,i){i=t(i||this.defaultElement||this)[0],this.element=t(i),this.uuid=h++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=t(),this.hoverable=t(),this.focusable=t(),this.classesElementLookup={},i!==this&&(t.data(i,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===i&&this.destroy()}}),this.document=t(i.style?i.ownerDocument:i.document||i),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this._create(),this.options.disabled&&this._setOptionDisabled(this.options.disabled),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:function(){return{}},_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){var e=this;this._destroy(),t.each(this.classesElementLookup,function(t,i){e._removeClass(i,t)}),this.element.off(this.eventNamespace).removeData(this.widgetFullName),this.widget().off(this.eventNamespace).removeAttr("aria-disabled"),this.bindings.off(this.eventNamespace)},_destroy:t.noop,widget:function(){return this.element},option:function(e,i){var s,n,o,a=e;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof e)if(a={},s=e.split("."),e=s.shift(),s.length){for(n=a[e]=t.widget.extend({},this.options[e]),o=0;s.length-1>o;o++)n[s[o]]=n[s[o]]||{},n=n[s[o]];if(e=s.pop(),1===arguments.length)return void 0===n[e]?null:n[e];n[e]=i}else{if(1===arguments.length)return void 0===this.options[e]?null:this.options[e];a[e]=i}return this._setOptions(a),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return"classes"===t&&this._setOptionClasses(e),this.options[t]=e,"disabled"===t&&this._setOptionDisabled(e),this},_setOptionClasses:function(e){var i,s,n;for(i in e)n=this.classesElementLookup[i],e[i]!==this.options.classes[i]&&n&&n.length&&(s=t(n.get()),this._removeClass(n,i),s.addClass(this._classes({element:s,keys:i,classes:e,add:!0})))},_setOptionDisabled:function(t){this._toggleClass(this.widget(),this.widgetFullName+"-disabled",null,!!t),t&&(this._removeClass(this.hoverable,null,"ui-state-hover"),this._removeClass(this.focusable,null,"ui-state-focus"))},enable:function(){return this._setOptions({disabled:!1})},disable:function(){return this._setOptions({disabled:!0})},_classes:function(e){function i(i,o){var a,r;for(r=0;i.length>r;r++)a=n.classesElementLookup[i[r]]||t(),a=e.add?t(t.unique(a.get().concat(e.element.get()))):t(a.not(e.element).get()),n.classesElementLookup[i[r]]=a,s.push(i[r]),o&&e.classes[i[r]]&&s.push(e.classes[i[r]])}var s=[],n=this;return e=t.extend({element:this.element,classes:this.options.classes||{}},e),this._on(e.element,{remove:"_untrackClassesElement"}),e.keys&&i(e.keys.match(/\S+/g)||[],!0),e.extra&&i(e.extra.match(/\S+/g)||[]),s.join(" ")},_untrackClassesElement:function(e){var i=this;t.each(i.classesElementLookup,function(s,n){-1!==t.inArray(e.target,n)&&(i.classesElementLookup[s]=t(n.not(e.target).get()))})},_removeClass:function(t,e,i){return this._toggleClass(t,e,i,!1)},_addClass:function(t,e,i){return this._toggleClass(t,e,i,!0)},_toggleClass:function(t,e,i,s){s="boolean"==typeof s?s:i;var n="string"==typeof t||null===t,o={extra:n?e:i,keys:n?t:e,element:n?this.element:t,add:s};return o.element.toggleClass(this._classes(o),s),this},_on:function(e,i,s){var n,o=this;"boolean"!=typeof e&&(s=i,i=e,e=!1),s?(i=n=t(i),this.bindings=this.bindings.add(i)):(s=i,i=this.element,n=this.widget()),t.each(s,function(s,a){function r(){return e||o.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof a?o[a]:a).apply(o,arguments):void 0}"string"!=typeof a&&(r.guid=a.guid=a.guid||r.guid||t.guid++);var h=s.match(/^([\w:-]*)\s*(.*)$/),l=h[1]+o.eventNamespace,c=h[2];c?n.on(l,c,r):i.on(l,r)})},_off:function(e,i){i=(i||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.off(i).off(i),this.bindings=t(this.bindings.not(e).get()),this.focusable=t(this.focusable.not(e).get()),this.hoverable=t(this.hoverable.not(e).get())},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){this._addClass(t(e.currentTarget),null,"ui-state-hover")},mouseleave:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){this._addClass(t(e.currentTarget),null,"ui-state-focus")},focusout:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-focus")}})},_trigger:function(e,i,s){var n,o,a=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],o=i.originalEvent)for(n in o)n in i||(i[n]=o[n]);return this.element.trigger(i,s),!(t.isFunction(a)&&a.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,o){"string"==typeof n&&(n={effect:n});var a,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),a=!t.isEmptyObject(n),n.complete=o,n.delay&&s.delay(n.delay),a&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,o):s.queue(function(i){t(this)[e](),o&&o.call(s[0]),i()})}}),t.widget,function(){function e(t,e,i){return[parseFloat(t[0])*(u.test(t[0])?e/100:1),parseFloat(t[1])*(u.test(t[1])?i/100:1)]}function i(e,i){return parseInt(t.css(e,i),10)||0}function s(e){var i=e[0];return 9===i.nodeType?{width:e.width(),height:e.height(),offset:{top:0,left:0}}:t.isWindow(i)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}var n,o=Math.max,a=Math.abs,r=/left|center|right/,h=/top|center|bottom/,l=/[\+\-]\d+(\.[\d]+)?%?/,c=/^\w+/,u=/%$/,d=t.fn.position;t.position={scrollbarWidth:function(){if(void 0!==n)return n;var e,i,s=t("
"),o=s.children()[0];return t("body").append(s),e=o.offsetWidth,s.css("overflow","scroll"),i=o.offsetWidth,e===i&&(i=s[0].clientWidth),s.remove(),n=e-i},getScrollInfo:function(e){var i=e.isWindow||e.isDocument?"":e.element.css("overflow-x"),s=e.isWindow||e.isDocument?"":e.element.css("overflow-y"),n="scroll"===i||"auto"===i&&e.widthi?"left":e>0?"right":"center",vertical:0>r?"top":s>0?"bottom":"middle"};l>p&&p>a(e+i)&&(u.horizontal="center"),c>f&&f>a(s+r)&&(u.vertical="middle"),u.important=o(a(e),a(i))>o(a(s),a(r))?"horizontal":"vertical",n.using.call(this,t,u)}),h.offset(t.extend(D,{using:r}))})},t.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,a=s.width,r=t.left-e.collisionPosition.marginLeft,h=n-r,l=r+e.collisionWidth-a-n;e.collisionWidth>a?h>0&&0>=l?(i=t.left+h+e.collisionWidth-a-n,t.left+=h-i):t.left=l>0&&0>=h?n:h>l?n+a-e.collisionWidth:n:h>0?t.left+=h:l>0?t.left-=l:t.left=o(t.left-r,t.left)},top:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollTop:s.offset.top,a=e.within.height,r=t.top-e.collisionPosition.marginTop,h=n-r,l=r+e.collisionHeight-a-n;e.collisionHeight>a?h>0&&0>=l?(i=t.top+h+e.collisionHeight-a-n,t.top+=h-i):t.top=l>0&&0>=h?n:h>l?n+a-e.collisionHeight:n:h>0?t.top+=h:l>0?t.top-=l:t.top=o(t.top-r,t.top)}},flip:{left:function(t,e){var i,s,n=e.within,o=n.offset.left+n.scrollLeft,r=n.width,h=n.isWindow?n.scrollLeft:n.offset.left,l=t.left-e.collisionPosition.marginLeft,c=l-h,u=l+e.collisionWidth-r-h,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];0>c?(i=t.left+d+p+f+e.collisionWidth-r-o,(0>i||a(c)>i)&&(t.left+=d+p+f)):u>0&&(s=t.left-e.collisionPosition.marginLeft+d+p+f-h,(s>0||u>a(s))&&(t.left+=d+p+f))},top:function(t,e){var i,s,n=e.within,o=n.offset.top+n.scrollTop,r=n.height,h=n.isWindow?n.scrollTop:n.offset.top,l=t.top-e.collisionPosition.marginTop,c=l-h,u=l+e.collisionHeight-r-h,d="top"===e.my[1],p=d?-e.elemHeight:"bottom"===e.my[1]?e.elemHeight:0,f="top"===e.at[1]?e.targetHeight:"bottom"===e.at[1]?-e.targetHeight:0,g=-2*e.offset[1];0>c?(s=t.top+p+f+g+e.collisionHeight-r-o,(0>s||a(c)>s)&&(t.top+=p+f+g)):u>0&&(i=t.top-e.collisionPosition.marginTop+p+f+g-h,(i>0||u>a(i))&&(t.top+=p+f+g))}},flipfit:{left:function(){t.ui.position.flip.left.apply(this,arguments),t.ui.position.fit.left.apply(this,arguments)},top:function(){t.ui.position.flip.top.apply(this,arguments),t.ui.position.fit.top.apply(this,arguments)}}}}(),t.ui.position,t.extend(t.expr[":"],{data:t.expr.createPseudo?t.expr.createPseudo(function(e){return function(i){return!!t.data(i,e)}}):function(e,i,s){return!!t.data(e,s[3])}}),t.fn.extend({disableSelection:function(){var t="onselectstart"in document.createElement("div")?"selectstart":"mousedown";return function(){return this.on(t+".ui-disableSelection",function(t){t.preventDefault()})}}(),enableSelection:function(){return this.off(".ui-disableSelection")}});var c="ui-effects-",u="ui-effects-style",d="ui-effects-animated",p=t;t.effects={effect:{}},function(t,e){function i(t,e,i){var s=u[e.type]||{};return null==t?i||!e.def?null:e.def:(t=s.floor?~~t:parseFloat(t),isNaN(t)?e.def:s.mod?(t+s.mod)%s.mod:0>t?0:t>s.max?s.max:t)}function s(i){var s=l(),n=s._rgba=[];return i=i.toLowerCase(),f(h,function(t,o){var a,r=o.re.exec(i),h=r&&o.parse(r),l=o.space||"rgba";return h?(a=s[l](h),s[c[l].cache]=a[c[l].cache],n=s._rgba=a._rgba,!1):e}),n.length?("0,0,0,0"===n.join()&&t.extend(n,o.transparent),s):o[i]}function n(t,e,i){return i=(i+1)%1,1>6*i?t+6*(e-t)*i:1>2*i?e:2>3*i?t+6*(e-t)*(2/3-i):t}var o,a="backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor",r=/^([\-+])=\s*(\d+\.?\d*)/,h=[{re:/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[t[1],t[2],t[3],t[4]]}},{re:/rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[2.55*t[1],2.55*t[2],2.55*t[3],t[4]]}},{re:/#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/,parse:function(t){return[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)]}},{re:/#([a-f0-9])([a-f0-9])([a-f0-9])/,parse:function(t){return[parseInt(t[1]+t[1],16),parseInt(t[2]+t[2],16),parseInt(t[3]+t[3],16)]}},{re:/hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,space:"hsla",parse:function(t){return[t[1],t[2]/100,t[3]/100,t[4]]}}],l=t.Color=function(e,i,s,n){return new t.Color.fn.parse(e,i,s,n)},c={rgba:{props:{red:{idx:0,type:"byte"},green:{idx:1,type:"byte"},blue:{idx:2,type:"byte"}}},hsla:{props:{hue:{idx:0,type:"degrees"},saturation:{idx:1,type:"percent"},lightness:{idx:2,type:"percent"}}}},u={"byte":{floor:!0,max:255},percent:{max:1},degrees:{mod:360,floor:!0}},d=l.support={},p=t("

")[0],f=t.each;p.style.cssText="background-color:rgba(1,1,1,.5)",d.rgba=p.style.backgroundColor.indexOf("rgba")>-1,f(c,function(t,e){e.cache="_"+t,e.props.alpha={idx:3,type:"percent",def:1}}),l.fn=t.extend(l.prototype,{parse:function(n,a,r,h){if(n===e)return this._rgba=[null,null,null,null],this;(n.jquery||n.nodeType)&&(n=t(n).css(a),a=e);var u=this,d=t.type(n),p=this._rgba=[];return a!==e&&(n=[n,a,r,h],d="array"),"string"===d?this.parse(s(n)||o._default):"array"===d?(f(c.rgba.props,function(t,e){p[e.idx]=i(n[e.idx],e)}),this):"object"===d?(n instanceof l?f(c,function(t,e){n[e.cache]&&(u[e.cache]=n[e.cache].slice())}):f(c,function(e,s){var o=s.cache;f(s.props,function(t,e){if(!u[o]&&s.to){if("alpha"===t||null==n[t])return;u[o]=s.to(u._rgba)}u[o][e.idx]=i(n[t],e,!0)}),u[o]&&0>t.inArray(null,u[o].slice(0,3))&&(u[o][3]=1,s.from&&(u._rgba=s.from(u[o])))}),this):e},is:function(t){var i=l(t),s=!0,n=this;return f(c,function(t,o){var a,r=i[o.cache];return r&&(a=n[o.cache]||o.to&&o.to(n._rgba)||[],f(o.props,function(t,i){return null!=r[i.idx]?s=r[i.idx]===a[i.idx]:e})),s}),s},_space:function(){var t=[],e=this;return f(c,function(i,s){e[s.cache]&&t.push(i)}),t.pop()},transition:function(t,e){var s=l(t),n=s._space(),o=c[n],a=0===this.alpha()?l("transparent"):this,r=a[o.cache]||o.to(a._rgba),h=r.slice();return s=s[o.cache],f(o.props,function(t,n){var o=n.idx,a=r[o],l=s[o],c=u[n.type]||{};null!==l&&(null===a?h[o]=l:(c.mod&&(l-a>c.mod/2?a+=c.mod:a-l>c.mod/2&&(a-=c.mod)),h[o]=i((l-a)*e+a,n)))}),this[n](h)},blend:function(e){if(1===this._rgba[3])return this;var i=this._rgba.slice(),s=i.pop(),n=l(e)._rgba;return l(t.map(i,function(t,e){return(1-s)*n[e]+s*t}))},toRgbaString:function(){var e="rgba(",i=t.map(this._rgba,function(t,e){return null==t?e>2?1:0:t});return 1===i[3]&&(i.pop(),e="rgb("),e+i.join()+")"},toHslaString:function(){var e="hsla(",i=t.map(this.hsla(),function(t,e){return null==t&&(t=e>2?1:0),e&&3>e&&(t=Math.round(100*t)+"%"),t});return 1===i[3]&&(i.pop(),e="hsl("),e+i.join()+")"},toHexString:function(e){var i=this._rgba.slice(),s=i.pop();return e&&i.push(~~(255*s)),"#"+t.map(i,function(t){return t=(t||0).toString(16),1===t.length?"0"+t:t}).join("")},toString:function(){return 0===this._rgba[3]?"transparent":this.toRgbaString()}}),l.fn.parse.prototype=l.fn,c.hsla.to=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e,i,s=t[0]/255,n=t[1]/255,o=t[2]/255,a=t[3],r=Math.max(s,n,o),h=Math.min(s,n,o),l=r-h,c=r+h,u=.5*c;return e=h===r?0:s===r?60*(n-o)/l+360:n===r?60*(o-s)/l+120:60*(s-n)/l+240,i=0===l?0:.5>=u?l/c:l/(2-c),[Math.round(e)%360,i,u,null==a?1:a]},c.hsla.from=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e=t[0]/360,i=t[1],s=t[2],o=t[3],a=.5>=s?s*(1+i):s+i-s*i,r=2*s-a;return[Math.round(255*n(r,a,e+1/3)),Math.round(255*n(r,a,e)),Math.round(255*n(r,a,e-1/3)),o]},f(c,function(s,n){var o=n.props,a=n.cache,h=n.to,c=n.from;l.fn[s]=function(s){if(h&&!this[a]&&(this[a]=h(this._rgba)),s===e)return this[a].slice();var n,r=t.type(s),u="array"===r||"object"===r?s:arguments,d=this[a].slice();return f(o,function(t,e){var s=u["object"===r?t:e.idx];null==s&&(s=d[e.idx]),d[e.idx]=i(s,e)}),c?(n=l(c(d)),n[a]=d,n):l(d)},f(o,function(e,i){l.fn[e]||(l.fn[e]=function(n){var o,a=t.type(n),h="alpha"===e?this._hsla?"hsla":"rgba":s,l=this[h](),c=l[i.idx];return"undefined"===a?c:("function"===a&&(n=n.call(this,c),a=t.type(n)),null==n&&i.empty?this:("string"===a&&(o=r.exec(n),o&&(n=c+parseFloat(o[2])*("+"===o[1]?1:-1))),l[i.idx]=n,this[h](l)))})})}),l.hook=function(e){var i=e.split(" ");f(i,function(e,i){t.cssHooks[i]={set:function(e,n){var o,a,r="";if("transparent"!==n&&("string"!==t.type(n)||(o=s(n)))){if(n=l(o||n),!d.rgba&&1!==n._rgba[3]){for(a="backgroundColor"===i?e.parentNode:e;(""===r||"transparent"===r)&&a&&a.style;)try{r=t.css(a,"backgroundColor"),a=a.parentNode}catch(h){}n=n.blend(r&&"transparent"!==r?r:"_default")}n=n.toRgbaString()}try{e.style[i]=n}catch(h){}}},t.fx.step[i]=function(e){e.colorInit||(e.start=l(e.elem,i),e.end=l(e.end),e.colorInit=!0),t.cssHooks[i].set(e.elem,e.start.transition(e.end,e.pos))}})},l.hook(a),t.cssHooks.borderColor={expand:function(t){var e={};return f(["Top","Right","Bottom","Left"],function(i,s){e["border"+s+"Color"]=t}),e}},o=t.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}}(p),function(){function e(e){var i,s,n=e.ownerDocument.defaultView?e.ownerDocument.defaultView.getComputedStyle(e,null):e.currentStyle,o={};if(n&&n.length&&n[0]&&n[n[0]])for(s=n.length;s--;)i=n[s],"string"==typeof n[i]&&(o[t.camelCase(i)]=n[i]);else for(i in n)"string"==typeof n[i]&&(o[i]=n[i]);return o}function i(e,i){var s,o,a={};for(s in i)o=i[s],e[s]!==o&&(n[s]||(t.fx.step[s]||!isNaN(parseFloat(o)))&&(a[s]=o));return a}var s=["add","remove","toggle"],n={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};t.each(["borderLeftStyle","borderRightStyle","borderBottomStyle","borderTopStyle"],function(e,i){t.fx.step[i]=function(t){("none"!==t.end&&!t.setAttr||1===t.pos&&!t.setAttr)&&(p.style(t.elem,i,t.end),t.setAttr=!0)}}),t.fn.addBack||(t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t.effects.animateClass=function(n,o,a,r){var h=t.speed(o,a,r);return this.queue(function(){var o,a=t(this),r=a.attr("class")||"",l=h.children?a.find("*").addBack():a;l=l.map(function(){var i=t(this);return{el:i,start:e(this)}}),o=function(){t.each(s,function(t,e){n[e]&&a[e+"Class"](n[e])})},o(),l=l.map(function(){return this.end=e(this.el[0]),this.diff=i(this.start,this.end),this}),a.attr("class",r),l=l.map(function(){var e=this,i=t.Deferred(),s=t.extend({},h,{queue:!1,complete:function(){i.resolve(e)}});return this.el.animate(this.diff,s),i.promise()}),t.when.apply(t,l.get()).done(function(){o(),t.each(arguments,function(){var e=this.el;t.each(this.diff,function(t){e.css(t,"")})}),h.complete.call(a[0])})})},t.fn.extend({addClass:function(e){return function(i,s,n,o){return s?t.effects.animateClass.call(this,{add:i},s,n,o):e.apply(this,arguments)}}(t.fn.addClass),removeClass:function(e){return function(i,s,n,o){return arguments.length>1?t.effects.animateClass.call(this,{remove:i},s,n,o):e.apply(this,arguments)}}(t.fn.removeClass),toggleClass:function(e){return function(i,s,n,o,a){return"boolean"==typeof s||void 0===s?n?t.effects.animateClass.call(this,s?{add:i}:{remove:i},n,o,a):e.apply(this,arguments):t.effects.animateClass.call(this,{toggle:i},s,n,o)}}(t.fn.toggleClass),switchClass:function(e,i,s,n,o){return t.effects.animateClass.call(this,{add:i,remove:e},s,n,o)}})}(),function(){function e(e,i,s,n){return t.isPlainObject(e)&&(i=e,e=e.effect),e={effect:e},null==i&&(i={}),t.isFunction(i)&&(n=i,s=null,i={}),("number"==typeof i||t.fx.speeds[i])&&(n=s,s=i,i={}),t.isFunction(s)&&(n=s,s=null),i&&t.extend(e,i),s=s||i.duration,e.duration=t.fx.off?0:"number"==typeof s?s:s in t.fx.speeds?t.fx.speeds[s]:t.fx.speeds._default,e.complete=n||i.complete,e}function i(e){return!e||"number"==typeof e||t.fx.speeds[e]?!0:"string"!=typeof e||t.effects.effect[e]?t.isFunction(e)?!0:"object"!=typeof e||e.effect?!1:!0:!0}function s(t,e){var i=e.outerWidth(),s=e.outerHeight(),n=/^rect\((-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto)\)$/,o=n.exec(t)||["",0,i,s,0];return{top:parseFloat(o[1])||0,right:"auto"===o[2]?i:parseFloat(o[2]),bottom:"auto"===o[3]?s:parseFloat(o[3]),left:parseFloat(o[4])||0}}t.expr&&t.expr.filters&&t.expr.filters.animated&&(t.expr.filters.animated=function(e){return function(i){return!!t(i).data(d)||e(i)}}(t.expr.filters.animated)),t.uiBackCompat!==!1&&t.extend(t.effects,{save:function(t,e){for(var i=0,s=e.length;s>i;i++)null!==e[i]&&t.data(c+e[i],t[0].style[e[i]])},restore:function(t,e){for(var i,s=0,n=e.length;n>s;s++)null!==e[s]&&(i=t.data(c+e[s]),t.css(e[s],i))},setMode:function(t,e){return"toggle"===e&&(e=t.is(":hidden")?"show":"hide"),e},createWrapper:function(e){if(e.parent().is(".ui-effects-wrapper"))return e.parent();var i={width:e.outerWidth(!0),height:e.outerHeight(!0),"float":e.css("float")},s=t("

").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),n={width:e.width(),height:e.height()},o=document.activeElement;try{o.id}catch(a){o=document.body}return e.wrap(s),(e[0]===o||t.contains(e[0],o))&&t(o).trigger("focus"),s=e.parent(),"static"===e.css("position")?(s.css({position:"relative"}),e.css({position:"relative"})):(t.extend(i,{position:e.css("position"),zIndex:e.css("z-index")}),t.each(["top","left","bottom","right"],function(t,s){i[s]=e.css(s),isNaN(parseInt(i[s],10))&&(i[s]="auto")}),e.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),e.css(n),s.css(i).show()},removeWrapper:function(e){var i=document.activeElement;return e.parent().is(".ui-effects-wrapper")&&(e.parent().replaceWith(e),(e[0]===i||t.contains(e[0],i))&&t(i).trigger("focus")),e}}),t.extend(t.effects,{version:"1.12.1",define:function(e,i,s){return s||(s=i,i="effect"),t.effects.effect[e]=s,t.effects.effect[e].mode=i,s},scaledDimensions:function(t,e,i){if(0===e)return{height:0,width:0,outerHeight:0,outerWidth:0};var s="horizontal"!==i?(e||100)/100:1,n="vertical"!==i?(e||100)/100:1;return{height:t.height()*n,width:t.width()*s,outerHeight:t.outerHeight()*n,outerWidth:t.outerWidth()*s}},clipToBox:function(t){return{width:t.clip.right-t.clip.left,height:t.clip.bottom-t.clip.top,left:t.clip.left,top:t.clip.top}},unshift:function(t,e,i){var s=t.queue();e>1&&s.splice.apply(s,[1,0].concat(s.splice(e,i))),t.dequeue()},saveStyle:function(t){t.data(u,t[0].style.cssText)},restoreStyle:function(t){t[0].style.cssText=t.data(u)||"",t.removeData(u)},mode:function(t,e){var i=t.is(":hidden");return"toggle"===e&&(e=i?"show":"hide"),(i?"hide"===e:"show"===e)&&(e="none"),e},getBaseline:function(t,e){var i,s;switch(t[0]){case"top":i=0;break;case"middle":i=.5;break;case"bottom":i=1;break;default:i=t[0]/e.height}switch(t[1]){case"left":s=0;break;case"center":s=.5;break;case"right":s=1;break;default:s=t[1]/e.width}return{x:s,y:i}},createPlaceholder:function(e){var i,s=e.css("position"),n=e.position();return e.css({marginTop:e.css("marginTop"),marginBottom:e.css("marginBottom"),marginLeft:e.css("marginLeft"),marginRight:e.css("marginRight")}).outerWidth(e.outerWidth()).outerHeight(e.outerHeight()),/^(static|relative)/.test(s)&&(s="absolute",i=t("<"+e[0].nodeName+">").insertAfter(e).css({display:/^(inline|ruby)/.test(e.css("display"))?"inline-block":"block",visibility:"hidden",marginTop:e.css("marginTop"),marginBottom:e.css("marginBottom"),marginLeft:e.css("marginLeft"),marginRight:e.css("marginRight"),"float":e.css("float")}).outerWidth(e.outerWidth()).outerHeight(e.outerHeight()).addClass("ui-effects-placeholder"),e.data(c+"placeholder",i)),e.css({position:s,left:n.left,top:n.top}),i},removePlaceholder:function(t){var e=c+"placeholder",i=t.data(e);i&&(i.remove(),t.removeData(e))},cleanUp:function(e){t.effects.restoreStyle(e),t.effects.removePlaceholder(e)},setTransition:function(e,i,s,n){return n=n||{},t.each(i,function(t,i){var o=e.cssUnit(i);o[0]>0&&(n[i]=o[0]*s+o[1])}),n}}),t.fn.extend({effect:function(){function i(e){function i(){r.removeData(d),t.effects.cleanUp(r),"hide"===s.mode&&r.hide(),a()}function a(){t.isFunction(h)&&h.call(r[0]),t.isFunction(e)&&e()}var r=t(this);s.mode=c.shift(),t.uiBackCompat===!1||o?"none"===s.mode?(r[l](),a()):n.call(r[0],s,i):(r.is(":hidden")?"hide"===l:"show"===l)?(r[l](),a()):n.call(r[0],s,a)}var s=e.apply(this,arguments),n=t.effects.effect[s.effect],o=n.mode,a=s.queue,r=a||"fx",h=s.complete,l=s.mode,c=[],u=function(e){var i=t(this),s=t.effects.mode(i,l)||o;i.data(d,!0),c.push(s),o&&("show"===s||s===o&&"hide"===s)&&i.show(),o&&"none"===s||t.effects.saveStyle(i),t.isFunction(e)&&e()};return t.fx.off||!n?l?this[l](s.duration,h):this.each(function(){h&&h.call(this)}):a===!1?this.each(u).each(i):this.queue(r,u).queue(r,i)},show:function(t){return function(s){if(i(s))return t.apply(this,arguments);var n=e.apply(this,arguments);return n.mode="show",this.effect.call(this,n) +}}(t.fn.show),hide:function(t){return function(s){if(i(s))return t.apply(this,arguments);var n=e.apply(this,arguments);return n.mode="hide",this.effect.call(this,n)}}(t.fn.hide),toggle:function(t){return function(s){if(i(s)||"boolean"==typeof s)return t.apply(this,arguments);var n=e.apply(this,arguments);return n.mode="toggle",this.effect.call(this,n)}}(t.fn.toggle),cssUnit:function(e){var i=this.css(e),s=[];return t.each(["em","px","%","pt"],function(t,e){i.indexOf(e)>0&&(s=[parseFloat(i),e])}),s},cssClip:function(t){return t?this.css("clip","rect("+t.top+"px "+t.right+"px "+t.bottom+"px "+t.left+"px)"):s(this.css("clip"),this)},transfer:function(e,i){var s=t(this),n=t(e.to),o="fixed"===n.css("position"),a=t("body"),r=o?a.scrollTop():0,h=o?a.scrollLeft():0,l=n.offset(),c={top:l.top-r,left:l.left-h,height:n.innerHeight(),width:n.innerWidth()},u=s.offset(),d=t("
").appendTo("body").addClass(e.className).css({top:u.top-r,left:u.left-h,height:s.innerHeight(),width:s.innerWidth(),position:o?"fixed":"absolute"}).animate(c,e.duration,e.easing,function(){d.remove(),t.isFunction(i)&&i()})}}),t.fx.step.clip=function(e){e.clipInit||(e.start=t(e.elem).cssClip(),"string"==typeof e.end&&(e.end=s(e.end,e.elem)),e.clipInit=!0),t(e.elem).cssClip({top:e.pos*(e.end.top-e.start.top)+e.start.top,right:e.pos*(e.end.right-e.start.right)+e.start.right,bottom:e.pos*(e.end.bottom-e.start.bottom)+e.start.bottom,left:e.pos*(e.end.left-e.start.left)+e.start.left})}}(),function(){var e={};t.each(["Quad","Cubic","Quart","Quint","Expo"],function(t,i){e[i]=function(e){return Math.pow(e,t+2)}}),t.extend(e,{Sine:function(t){return 1-Math.cos(t*Math.PI/2)},Circ:function(t){return 1-Math.sqrt(1-t*t)},Elastic:function(t){return 0===t||1===t?t:-Math.pow(2,8*(t-1))*Math.sin((80*(t-1)-7.5)*Math.PI/15)},Back:function(t){return t*t*(3*t-2)},Bounce:function(t){for(var e,i=4;((e=Math.pow(2,--i))-1)/11>t;);return 1/Math.pow(4,3-i)-7.5625*Math.pow((3*e-2)/22-t,2)}}),t.each(e,function(e,i){t.easing["easeIn"+e]=i,t.easing["easeOut"+e]=function(t){return 1-i(1-t)},t.easing["easeInOut"+e]=function(t){return.5>t?i(2*t)/2:1-i(-2*t+2)/2}})}();var f=t.effects;t.effects.define("blind","hide",function(e,i){var s={up:["bottom","top"],vertical:["bottom","top"],down:["top","bottom"],left:["right","left"],horizontal:["right","left"],right:["left","right"]},n=t(this),o=e.direction||"up",a=n.cssClip(),r={clip:t.extend({},a)},h=t.effects.createPlaceholder(n);r.clip[s[o][0]]=r.clip[s[o][1]],"show"===e.mode&&(n.cssClip(r.clip),h&&h.css(t.effects.clipToBox(r)),r.clip=a),h&&h.animate(t.effects.clipToBox(r),e.duration,e.easing),n.animate(r,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("bounce",function(e,i){var s,n,o,a=t(this),r=e.mode,h="hide"===r,l="show"===r,c=e.direction||"up",u=e.distance,d=e.times||5,p=2*d+(l||h?1:0),f=e.duration/p,g=e.easing,m="up"===c||"down"===c?"top":"left",_="up"===c||"left"===c,v=0,b=a.queue().length;for(t.effects.createPlaceholder(a),o=a.css(m),u||(u=a["top"===m?"outerHeight":"outerWidth"]()/3),l&&(n={opacity:1},n[m]=o,a.css("opacity",0).css(m,_?2*-u:2*u).animate(n,f,g)),h&&(u/=Math.pow(2,d-1)),n={},n[m]=o;d>v;v++)s={},s[m]=(_?"-=":"+=")+u,a.animate(s,f,g).animate(n,f,g),u=h?2*u:u/2;h&&(s={opacity:0},s[m]=(_?"-=":"+=")+u,a.animate(s,f,g)),a.queue(i),t.effects.unshift(a,b,p+1)}),t.effects.define("clip","hide",function(e,i){var s,n={},o=t(this),a=e.direction||"vertical",r="both"===a,h=r||"horizontal"===a,l=r||"vertical"===a;s=o.cssClip(),n.clip={top:l?(s.bottom-s.top)/2:s.top,right:h?(s.right-s.left)/2:s.right,bottom:l?(s.bottom-s.top)/2:s.bottom,left:h?(s.right-s.left)/2:s.left},t.effects.createPlaceholder(o),"show"===e.mode&&(o.cssClip(n.clip),n.clip=s),o.animate(n,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("drop","hide",function(e,i){var s,n=t(this),o=e.mode,a="show"===o,r=e.direction||"left",h="up"===r||"down"===r?"top":"left",l="up"===r||"left"===r?"-=":"+=",c="+="===l?"-=":"+=",u={opacity:0};t.effects.createPlaceholder(n),s=e.distance||n["top"===h?"outerHeight":"outerWidth"](!0)/2,u[h]=l+s,a&&(n.css(u),u[h]=c+s,u.opacity=1),n.animate(u,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("explode","hide",function(e,i){function s(){b.push(this),b.length===u*d&&n()}function n(){p.css({visibility:"visible"}),t(b).remove(),i()}var o,a,r,h,l,c,u=e.pieces?Math.round(Math.sqrt(e.pieces)):3,d=u,p=t(this),f=e.mode,g="show"===f,m=p.show().css("visibility","hidden").offset(),_=Math.ceil(p.outerWidth()/d),v=Math.ceil(p.outerHeight()/u),b=[];for(o=0;u>o;o++)for(h=m.top+o*v,c=o-(u-1)/2,a=0;d>a;a++)r=m.left+a*_,l=a-(d-1)/2,p.clone().appendTo("body").wrap("
").css({position:"absolute",visibility:"visible",left:-a*_,top:-o*v}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:_,height:v,left:r+(g?l*_:0),top:h+(g?c*v:0),opacity:g?0:1}).animate({left:r+(g?0:l*_),top:h+(g?0:c*v),opacity:g?1:0},e.duration||500,e.easing,s)}),t.effects.define("fade","toggle",function(e,i){var s="show"===e.mode;t(this).css("opacity",s?0:1).animate({opacity:s?1:0},{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("fold","hide",function(e,i){var s=t(this),n=e.mode,o="show"===n,a="hide"===n,r=e.size||15,h=/([0-9]+)%/.exec(r),l=!!e.horizFirst,c=l?["right","bottom"]:["bottom","right"],u=e.duration/2,d=t.effects.createPlaceholder(s),p=s.cssClip(),f={clip:t.extend({},p)},g={clip:t.extend({},p)},m=[p[c[0]],p[c[1]]],_=s.queue().length;h&&(r=parseInt(h[1],10)/100*m[a?0:1]),f.clip[c[0]]=r,g.clip[c[0]]=r,g.clip[c[1]]=0,o&&(s.cssClip(g.clip),d&&d.css(t.effects.clipToBox(g)),g.clip=p),s.queue(function(i){d&&d.animate(t.effects.clipToBox(f),u,e.easing).animate(t.effects.clipToBox(g),u,e.easing),i()}).animate(f,u,e.easing).animate(g,u,e.easing).queue(i),t.effects.unshift(s,_,4)}),t.effects.define("highlight","show",function(e,i){var s=t(this),n={backgroundColor:s.css("backgroundColor")};"hide"===e.mode&&(n.opacity=0),t.effects.saveStyle(s),s.css({backgroundImage:"none",backgroundColor:e.color||"#ffff99"}).animate(n,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("size",function(e,i){var s,n,o,a=t(this),r=["fontSize"],h=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],l=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],c=e.mode,u="effect"!==c,d=e.scale||"both",p=e.origin||["middle","center"],f=a.css("position"),g=a.position(),m=t.effects.scaledDimensions(a),_=e.from||m,v=e.to||t.effects.scaledDimensions(a,0);t.effects.createPlaceholder(a),"show"===c&&(o=_,_=v,v=o),n={from:{y:_.height/m.height,x:_.width/m.width},to:{y:v.height/m.height,x:v.width/m.width}},("box"===d||"both"===d)&&(n.from.y!==n.to.y&&(_=t.effects.setTransition(a,h,n.from.y,_),v=t.effects.setTransition(a,h,n.to.y,v)),n.from.x!==n.to.x&&(_=t.effects.setTransition(a,l,n.from.x,_),v=t.effects.setTransition(a,l,n.to.x,v))),("content"===d||"both"===d)&&n.from.y!==n.to.y&&(_=t.effects.setTransition(a,r,n.from.y,_),v=t.effects.setTransition(a,r,n.to.y,v)),p&&(s=t.effects.getBaseline(p,m),_.top=(m.outerHeight-_.outerHeight)*s.y+g.top,_.left=(m.outerWidth-_.outerWidth)*s.x+g.left,v.top=(m.outerHeight-v.outerHeight)*s.y+g.top,v.left=(m.outerWidth-v.outerWidth)*s.x+g.left),a.css(_),("content"===d||"both"===d)&&(h=h.concat(["marginTop","marginBottom"]).concat(r),l=l.concat(["marginLeft","marginRight"]),a.find("*[width]").each(function(){var i=t(this),s=t.effects.scaledDimensions(i),o={height:s.height*n.from.y,width:s.width*n.from.x,outerHeight:s.outerHeight*n.from.y,outerWidth:s.outerWidth*n.from.x},a={height:s.height*n.to.y,width:s.width*n.to.x,outerHeight:s.height*n.to.y,outerWidth:s.width*n.to.x};n.from.y!==n.to.y&&(o=t.effects.setTransition(i,h,n.from.y,o),a=t.effects.setTransition(i,h,n.to.y,a)),n.from.x!==n.to.x&&(o=t.effects.setTransition(i,l,n.from.x,o),a=t.effects.setTransition(i,l,n.to.x,a)),u&&t.effects.saveStyle(i),i.css(o),i.animate(a,e.duration,e.easing,function(){u&&t.effects.restoreStyle(i)})})),a.animate(v,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){var e=a.offset();0===v.opacity&&a.css("opacity",_.opacity),u||(a.css("position","static"===f?"relative":f).offset(e),t.effects.saveStyle(a)),i()}})}),t.effects.define("scale",function(e,i){var s=t(this),n=e.mode,o=parseInt(e.percent,10)||(0===parseInt(e.percent,10)?0:"effect"!==n?0:100),a=t.extend(!0,{from:t.effects.scaledDimensions(s),to:t.effects.scaledDimensions(s,o,e.direction||"both"),origin:e.origin||["middle","center"]},e);e.fade&&(a.from.opacity=1,a.to.opacity=0),t.effects.effect.size.call(this,a,i)}),t.effects.define("puff","hide",function(e,i){var s=t.extend(!0,{},e,{fade:!0,percent:parseInt(e.percent,10)||150});t.effects.effect.scale.call(this,s,i)}),t.effects.define("pulsate","show",function(e,i){var s=t(this),n=e.mode,o="show"===n,a="hide"===n,r=o||a,h=2*(e.times||5)+(r?1:0),l=e.duration/h,c=0,u=1,d=s.queue().length;for((o||!s.is(":visible"))&&(s.css("opacity",0).show(),c=1);h>u;u++)s.animate({opacity:c},l,e.easing),c=1-c;s.animate({opacity:c},l,e.easing),s.queue(i),t.effects.unshift(s,d,h+1)}),t.effects.define("shake",function(e,i){var s=1,n=t(this),o=e.direction||"left",a=e.distance||20,r=e.times||3,h=2*r+1,l=Math.round(e.duration/h),c="up"===o||"down"===o?"top":"left",u="up"===o||"left"===o,d={},p={},f={},g=n.queue().length;for(t.effects.createPlaceholder(n),d[c]=(u?"-=":"+=")+a,p[c]=(u?"+=":"-=")+2*a,f[c]=(u?"-=":"+=")+2*a,n.animate(d,l,e.easing);r>s;s++)n.animate(p,l,e.easing).animate(f,l,e.easing);n.animate(p,l,e.easing).animate(d,l/2,e.easing).queue(i),t.effects.unshift(n,g,h+1)}),t.effects.define("slide","show",function(e,i){var s,n,o=t(this),a={up:["bottom","top"],down:["top","bottom"],left:["right","left"],right:["left","right"]},r=e.mode,h=e.direction||"left",l="up"===h||"down"===h?"top":"left",c="up"===h||"left"===h,u=e.distance||o["top"===l?"outerHeight":"outerWidth"](!0),d={};t.effects.createPlaceholder(o),s=o.cssClip(),n=o.position()[l],d[l]=(c?-1:1)*u+n,d.clip=o.cssClip(),d.clip[a[h][1]]=d.clip[a[h][0]],"show"===r&&(o.cssClip(d.clip),o.css(l,d[l]),d.clip=s,d[l]=n),o.animate(d,{queue:!1,duration:e.duration,easing:e.easing,complete:i})});var f;t.uiBackCompat!==!1&&(f=t.effects.define("transfer",function(e,i){t(this).transfer(e,i)})),t.ui.focusable=function(i,s){var n,o,a,r,h,l=i.nodeName.toLowerCase();return"area"===l?(n=i.parentNode,o=n.name,i.href&&o&&"map"===n.nodeName.toLowerCase()?(a=t("img[usemap='#"+o+"']"),a.length>0&&a.is(":visible")):!1):(/^(input|select|textarea|button|object)$/.test(l)?(r=!i.disabled,r&&(h=t(i).closest("fieldset")[0],h&&(r=!h.disabled))):r="a"===l?i.href||s:s,r&&t(i).is(":visible")&&e(t(i)))},t.extend(t.expr[":"],{focusable:function(e){return t.ui.focusable(e,null!=t.attr(e,"tabindex"))}}),t.ui.focusable,t.fn.form=function(){return"string"==typeof this[0].form?this.closest("form"):t(this[0].form)},t.ui.formResetMixin={_formResetHandler:function(){var e=t(this);setTimeout(function(){var i=e.data("ui-form-reset-instances");t.each(i,function(){this.refresh()})})},_bindFormResetHandler:function(){if(this.form=this.element.form(),this.form.length){var t=this.form.data("ui-form-reset-instances")||[];t.length||this.form.on("reset.ui-form-reset",this._formResetHandler),t.push(this),this.form.data("ui-form-reset-instances",t)}},_unbindFormResetHandler:function(){if(this.form.length){var e=this.form.data("ui-form-reset-instances");e.splice(t.inArray(this,e),1),e.length?this.form.data("ui-form-reset-instances",e):this.form.removeData("ui-form-reset-instances").off("reset.ui-form-reset")}}},"1.7"===t.fn.jquery.substring(0,3)&&(t.each(["Width","Height"],function(e,i){function s(e,i,s,o){return t.each(n,function(){i-=parseFloat(t.css(e,"padding"+this))||0,s&&(i-=parseFloat(t.css(e,"border"+this+"Width"))||0),o&&(i-=parseFloat(t.css(e,"margin"+this))||0)}),i}var n="Width"===i?["Left","Right"]:["Top","Bottom"],o=i.toLowerCase(),a={innerWidth:t.fn.innerWidth,innerHeight:t.fn.innerHeight,outerWidth:t.fn.outerWidth,outerHeight:t.fn.outerHeight};t.fn["inner"+i]=function(e){return void 0===e?a["inner"+i].call(this):this.each(function(){t(this).css(o,s(this,e)+"px")})},t.fn["outer"+i]=function(e,n){return"number"!=typeof e?a["outer"+i].call(this,e):this.each(function(){t(this).css(o,s(this,e,!0,n)+"px")})}}),t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t.ui.keyCode={BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38},t.ui.escapeSelector=function(){var t=/([!"#$%&'()*+,.\/:;<=>?@[\]^`{|}~])/g;return function(e){return e.replace(t,"\\$1")}}(),t.fn.labels=function(){var e,i,s,n,o;return this[0].labels&&this[0].labels.length?this.pushStack(this[0].labels):(n=this.eq(0).parents("label"),s=this.attr("id"),s&&(e=this.eq(0).parents().last(),o=e.add(e.length?e.siblings():this.siblings()),i="label[for='"+t.ui.escapeSelector(s)+"']",n=n.add(o.find(i).addBack(i))),this.pushStack(n))},t.fn.scrollParent=function(e){var i=this.css("position"),s="absolute"===i,n=e?/(auto|scroll|hidden)/:/(auto|scroll)/,o=this.parents().filter(function(){var e=t(this);return s&&"static"===e.css("position")?!1:n.test(e.css("overflow")+e.css("overflow-y")+e.css("overflow-x"))}).eq(0);return"fixed"!==i&&o.length?o:t(this[0].ownerDocument||document)},t.extend(t.expr[":"],{tabbable:function(e){var i=t.attr(e,"tabindex"),s=null!=i;return(!s||i>=0)&&t.ui.focusable(e,s)}}),t.fn.extend({uniqueId:function(){var t=0;return function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++t)})}}(),removeUniqueId:function(){return this.each(function(){/^ui-id-\d+$/.test(this.id)&&t(this).removeAttr("id")})}}),t.widget("ui.accordion",{version:"1.12.1",options:{active:0,animate:{},classes:{"ui-accordion-header":"ui-corner-top","ui-accordion-header-collapsed":"ui-corner-all","ui-accordion-content":"ui-corner-bottom"},collapsible:!1,event:"click",header:"> li > :first-child, > :not(li):even",heightStyle:"auto",icons:{activeHeader:"ui-icon-triangle-1-s",header:"ui-icon-triangle-1-e"},activate:null,beforeActivate:null},hideProps:{borderTopWidth:"hide",borderBottomWidth:"hide",paddingTop:"hide",paddingBottom:"hide",height:"hide"},showProps:{borderTopWidth:"show",borderBottomWidth:"show",paddingTop:"show",paddingBottom:"show",height:"show"},_create:function(){var e=this.options;this.prevShow=this.prevHide=t(),this._addClass("ui-accordion","ui-widget ui-helper-reset"),this.element.attr("role","tablist"),e.collapsible||e.active!==!1&&null!=e.active||(e.active=0),this._processPanels(),0>e.active&&(e.active+=this.headers.length),this._refresh()},_getCreateEventData:function(){return{header:this.active,panel:this.active.length?this.active.next():t()}},_createIcons:function(){var e,i,s=this.options.icons;s&&(e=t(""),this._addClass(e,"ui-accordion-header-icon","ui-icon "+s.header),e.prependTo(this.headers),i=this.active.children(".ui-accordion-header-icon"),this._removeClass(i,s.header)._addClass(i,null,s.activeHeader)._addClass(this.headers,"ui-accordion-icons"))},_destroyIcons:function(){this._removeClass(this.headers,"ui-accordion-icons"),this.headers.children(".ui-accordion-header-icon").remove()},_destroy:function(){var t;this.element.removeAttr("role"),this.headers.removeAttr("role aria-expanded aria-selected aria-controls tabIndex").removeUniqueId(),this._destroyIcons(),t=this.headers.next().css("display","").removeAttr("role aria-hidden aria-labelledby").removeUniqueId(),"content"!==this.options.heightStyle&&t.css("height","")},_setOption:function(t,e){return"active"===t?(this._activate(e),void 0):("event"===t&&(this.options.event&&this._off(this.headers,this.options.event),this._setupEvents(e)),this._super(t,e),"collapsible"!==t||e||this.options.active!==!1||this._activate(0),"icons"===t&&(this._destroyIcons(),e&&this._createIcons()),void 0)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",t),this._toggleClass(null,"ui-state-disabled",!!t),this._toggleClass(this.headers.add(this.headers.next()),null,"ui-state-disabled",!!t)},_keydown:function(e){if(!e.altKey&&!e.ctrlKey){var i=t.ui.keyCode,s=this.headers.length,n=this.headers.index(e.target),o=!1;switch(e.keyCode){case i.RIGHT:case i.DOWN:o=this.headers[(n+1)%s];break;case i.LEFT:case i.UP:o=this.headers[(n-1+s)%s];break;case i.SPACE:case i.ENTER:this._eventHandler(e);break;case i.HOME:o=this.headers[0];break;case i.END:o=this.headers[s-1]}o&&(t(e.target).attr("tabIndex",-1),t(o).attr("tabIndex",0),t(o).trigger("focus"),e.preventDefault())}},_panelKeyDown:function(e){e.keyCode===t.ui.keyCode.UP&&e.ctrlKey&&t(e.currentTarget).prev().trigger("focus")},refresh:function(){var e=this.options;this._processPanels(),e.active===!1&&e.collapsible===!0||!this.headers.length?(e.active=!1,this.active=t()):e.active===!1?this._activate(0):this.active.length&&!t.contains(this.element[0],this.active[0])?this.headers.length===this.headers.find(".ui-state-disabled").length?(e.active=!1,this.active=t()):this._activate(Math.max(0,e.active-1)):e.active=this.headers.index(this.active),this._destroyIcons(),this._refresh()},_processPanels:function(){var t=this.headers,e=this.panels;this.headers=this.element.find(this.options.header),this._addClass(this.headers,"ui-accordion-header ui-accordion-header-collapsed","ui-state-default"),this.panels=this.headers.next().filter(":not(.ui-accordion-content-active)").hide(),this._addClass(this.panels,"ui-accordion-content","ui-helper-reset ui-widget-content"),e&&(this._off(t.not(this.headers)),this._off(e.not(this.panels)))},_refresh:function(){var e,i=this.options,s=i.heightStyle,n=this.element.parent();this.active=this._findActive(i.active),this._addClass(this.active,"ui-accordion-header-active","ui-state-active")._removeClass(this.active,"ui-accordion-header-collapsed"),this._addClass(this.active.next(),"ui-accordion-content-active"),this.active.next().show(),this.headers.attr("role","tab").each(function(){var e=t(this),i=e.uniqueId().attr("id"),s=e.next(),n=s.uniqueId().attr("id");e.attr("aria-controls",n),s.attr("aria-labelledby",i)}).next().attr("role","tabpanel"),this.headers.not(this.active).attr({"aria-selected":"false","aria-expanded":"false",tabIndex:-1}).next().attr({"aria-hidden":"true"}).hide(),this.active.length?this.active.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0}).next().attr({"aria-hidden":"false"}):this.headers.eq(0).attr("tabIndex",0),this._createIcons(),this._setupEvents(i.event),"fill"===s?(e=n.height(),this.element.siblings(":visible").each(function(){var i=t(this),s=i.css("position");"absolute"!==s&&"fixed"!==s&&(e-=i.outerHeight(!0))}),this.headers.each(function(){e-=t(this).outerHeight(!0)}),this.headers.next().each(function(){t(this).height(Math.max(0,e-t(this).innerHeight()+t(this).height()))}).css("overflow","auto")):"auto"===s&&(e=0,this.headers.next().each(function(){var i=t(this).is(":visible");i||t(this).show(),e=Math.max(e,t(this).css("height","").height()),i||t(this).hide()}).height(e))},_activate:function(e){var i=this._findActive(e)[0];i!==this.active[0]&&(i=i||this.active[0],this._eventHandler({target:i,currentTarget:i,preventDefault:t.noop}))},_findActive:function(e){return"number"==typeof e?this.headers.eq(e):t()},_setupEvents:function(e){var i={keydown:"_keydown"};e&&t.each(e.split(" "),function(t,e){i[e]="_eventHandler"}),this._off(this.headers.add(this.headers.next())),this._on(this.headers,i),this._on(this.headers.next(),{keydown:"_panelKeyDown"}),this._hoverable(this.headers),this._focusable(this.headers)},_eventHandler:function(e){var i,s,n=this.options,o=this.active,a=t(e.currentTarget),r=a[0]===o[0],h=r&&n.collapsible,l=h?t():a.next(),c=o.next(),u={oldHeader:o,oldPanel:c,newHeader:h?t():a,newPanel:l};e.preventDefault(),r&&!n.collapsible||this._trigger("beforeActivate",e,u)===!1||(n.active=h?!1:this.headers.index(a),this.active=r?t():a,this._toggle(u),this._removeClass(o,"ui-accordion-header-active","ui-state-active"),n.icons&&(i=o.children(".ui-accordion-header-icon"),this._removeClass(i,null,n.icons.activeHeader)._addClass(i,null,n.icons.header)),r||(this._removeClass(a,"ui-accordion-header-collapsed")._addClass(a,"ui-accordion-header-active","ui-state-active"),n.icons&&(s=a.children(".ui-accordion-header-icon"),this._removeClass(s,null,n.icons.header)._addClass(s,null,n.icons.activeHeader)),this._addClass(a.next(),"ui-accordion-content-active")))},_toggle:function(e){var i=e.newPanel,s=this.prevShow.length?this.prevShow:e.oldPanel;this.prevShow.add(this.prevHide).stop(!0,!0),this.prevShow=i,this.prevHide=s,this.options.animate?this._animate(i,s,e):(s.hide(),i.show(),this._toggleComplete(e)),s.attr({"aria-hidden":"true"}),s.prev().attr({"aria-selected":"false","aria-expanded":"false"}),i.length&&s.length?s.prev().attr({tabIndex:-1,"aria-expanded":"false"}):i.length&&this.headers.filter(function(){return 0===parseInt(t(this).attr("tabIndex"),10)}).attr("tabIndex",-1),i.attr("aria-hidden","false").prev().attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0})},_animate:function(t,e,i){var s,n,o,a=this,r=0,h=t.css("box-sizing"),l=t.length&&(!e.length||t.index()",delay:300,options:{icons:{submenu:"ui-icon-caret-1-e"},items:"> *",menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.element.uniqueId().attr({role:this.options.role,tabIndex:0}),this._addClass("ui-menu","ui-widget ui-widget-content"),this._on({"mousedown .ui-menu-item":function(t){t.preventDefault()},"click .ui-menu-item":function(e){var i=t(e.target),s=t(t.ui.safeActiveElement(this.document[0]));!this.mouseHandled&&i.not(".ui-state-disabled").length&&(this.select(e),e.isPropagationStopped()||(this.mouseHandled=!0),i.has(".ui-menu").length?this.expand(e):!this.element.is(":focus")&&s.closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(e){if(!this.previousFilter){var i=t(e.target).closest(".ui-menu-item"),s=t(e.currentTarget);i[0]===s[0]&&(this._removeClass(s.siblings().children(".ui-state-active"),null,"ui-state-active"),this.focus(e,s))}},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this.element.find(this.options.items).eq(0);e||this.focus(t,i)},blur:function(e){this._delay(function(){var i=!t.contains(this.element[0],t.ui.safeActiveElement(this.document[0]));i&&this.collapseAll(e)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(t){this._closeOnDocumentClick(t)&&this.collapseAll(t),this.mouseHandled=!1}})},_destroy:function(){var e=this.element.find(".ui-menu-item").removeAttr("role aria-disabled"),i=e.children(".ui-menu-item-wrapper").removeUniqueId().removeAttr("tabIndex role aria-haspopup");this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeAttr("role aria-labelledby aria-expanded aria-hidden aria-disabled tabIndex").removeUniqueId().show(),i.children().each(function(){var e=t(this);e.data("ui-menu-submenu-caret")&&e.remove()})},_keydown:function(e){var i,s,n,o,a=!0;switch(e.keyCode){case t.ui.keyCode.PAGE_UP:this.previousPage(e);break;case t.ui.keyCode.PAGE_DOWN:this.nextPage(e);break;case t.ui.keyCode.HOME:this._move("first","first",e);break;case t.ui.keyCode.END:this._move("last","last",e);break;case t.ui.keyCode.UP:this.previous(e);break;case t.ui.keyCode.DOWN:this.next(e);break;case t.ui.keyCode.LEFT:this.collapse(e);break;case t.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(e);break;case t.ui.keyCode.ENTER:case t.ui.keyCode.SPACE:this._activate(e);break;case t.ui.keyCode.ESCAPE:this.collapse(e);break;default:a=!1,s=this.previousFilter||"",o=!1,n=e.keyCode>=96&&105>=e.keyCode?""+(e.keyCode-96):String.fromCharCode(e.keyCode),clearTimeout(this.filterTimer),n===s?o=!0:n=s+n,i=this._filterMenuItems(n),i=o&&-1!==i.index(this.active.next())?this.active.nextAll(".ui-menu-item"):i,i.length||(n=String.fromCharCode(e.keyCode),i=this._filterMenuItems(n)),i.length?(this.focus(e,i),this.previousFilter=n,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter}a&&e.preventDefault()},_activate:function(t){this.active&&!this.active.is(".ui-state-disabled")&&(this.active.children("[aria-haspopup='true']").length?this.expand(t):this.select(t))},refresh:function(){var e,i,s,n,o,a=this,r=this.options.icons.submenu,h=this.element.find(this.options.menus);this._toggleClass("ui-menu-icons",null,!!this.element.find(".ui-icon").length),s=h.filter(":not(.ui-menu)").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var e=t(this),i=e.prev(),s=t("").data("ui-menu-submenu-caret",!0);a._addClass(s,"ui-menu-icon","ui-icon "+r),i.attr("aria-haspopup","true").prepend(s),e.attr("aria-labelledby",i.attr("id"))}),this._addClass(s,"ui-menu","ui-widget ui-widget-content ui-front"),e=h.add(this.element),i=e.find(this.options.items),i.not(".ui-menu-item").each(function(){var e=t(this);a._isDivider(e)&&a._addClass(e,"ui-menu-divider","ui-widget-content")}),n=i.not(".ui-menu-item, .ui-menu-divider"),o=n.children().not(".ui-menu").uniqueId().attr({tabIndex:-1,role:this._itemRole()}),this._addClass(n,"ui-menu-item")._addClass(o,"ui-menu-item-wrapper"),i.filter(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!t.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){if("icons"===t){var i=this.element.find(".ui-menu-icon");this._removeClass(i,null,this.options.icons.submenu)._addClass(i,null,e.submenu)}this._super(t,e)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",t+""),this._toggleClass(null,"ui-state-disabled",!!t)},focus:function(t,e){var i,s,n;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),s=this.active.children(".ui-menu-item-wrapper"),this._addClass(s,null,"ui-state-active"),this.options.role&&this.element.attr("aria-activedescendant",s.attr("id")),n=this.active.parent().closest(".ui-menu-item").children(".ui-menu-item-wrapper"),this._addClass(n,null,"ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),i=e.children(".ui-menu"),i.length&&t&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(e){var i,s,n,o,a,r;this._hasScroll()&&(i=parseFloat(t.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(t.css(this.activeMenu[0],"paddingTop"))||0,n=e.offset().top-this.activeMenu.offset().top-i-s,o=this.activeMenu.scrollTop(),a=this.activeMenu.height(),r=e.outerHeight(),0>n?this.activeMenu.scrollTop(o+n):n+r>a&&this.activeMenu.scrollTop(o+n-a+r))},blur:function(t,e){e||clearTimeout(this.timer),this.active&&(this._removeClass(this.active.children(".ui-menu-item-wrapper"),null,"ui-state-active"),this._trigger("blur",t,{item:this.active}),this.active=null)},_startOpening:function(t){clearTimeout(this.timer),"true"===t.attr("aria-hidden")&&(this.timer=this._delay(function(){this._close(),this._open(t)},this.delay))},_open:function(e){var i=t.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(e.parents(".ui-menu")).hide().attr("aria-hidden","true"),e.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(i)},collapseAll:function(e,i){clearTimeout(this.timer),this.timer=this._delay(function(){var s=i?this.element:t(e&&e.target).closest(this.element.find(".ui-menu"));s.length||(s=this.element),this._close(s),this.blur(e),this._removeClass(s.find(".ui-state-active"),null,"ui-state-active"),this.activeMenu=s},this.delay)},_close:function(t){t||(t=this.active?this.active.parent():this.element),t.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false")},_closeOnDocumentClick:function(e){return!t(e.target).closest(".ui-menu").length},_isDivider:function(t){return!/[^\-\u2014\u2013\s]/.test(t.text())},collapse:function(t){var e=this.active&&this.active.parent().closest(".ui-menu-item",this.element);e&&e.length&&(this._close(),this.focus(t,e))},expand:function(t){var e=this.active&&this.active.children(".ui-menu ").find(this.options.items).first();e&&e.length&&(this._open(e.parent()),this._delay(function(){this.focus(t,e)}))},next:function(t){this._move("next","first",t)},previous:function(t){this._move("prev","last",t)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},_move:function(t,e,i){var s;this.active&&(s="first"===t||"last"===t?this.active["first"===t?"prevAll":"nextAll"](".ui-menu-item").eq(-1):this.active[t+"All"](".ui-menu-item").eq(0)),s&&s.length&&this.active||(s=this.activeMenu.find(this.options.items)[e]()),this.focus(i,s)},nextPage:function(e){var i,s,n;return this.active?(this.isLastItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return i=t(this),0>i.offset().top-s-n}),this.focus(e,i)):this.focus(e,this.activeMenu.find(this.options.items)[this.active?"last":"first"]())),void 0):(this.next(e),void 0)},previousPage:function(e){var i,s,n;return this.active?(this.isFirstItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return i=t(this),i.offset().top-s+n>0}),this.focus(e,i)):this.focus(e,this.activeMenu.find(this.options.items).first())),void 0):(this.next(e),void 0)},_hasScroll:function(){return this.element.outerHeight()",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var e,i,s,n=this.element[0].nodeName.toLowerCase(),o="textarea"===n,a="input"===n; +this.isMultiLine=o||!a&&this._isContentEditable(this.element),this.valueMethod=this.element[o||a?"val":"text"],this.isNewMenu=!0,this._addClass("ui-autocomplete-input"),this.element.attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return e=!0,s=!0,i=!0,void 0;e=!1,s=!1,i=!1;var o=t.ui.keyCode;switch(n.keyCode){case o.PAGE_UP:e=!0,this._move("previousPage",n);break;case o.PAGE_DOWN:e=!0,this._move("nextPage",n);break;case o.UP:e=!0,this._keyEvent("previous",n);break;case o.DOWN:e=!0,this._keyEvent("next",n);break;case o.ENTER:this.menu.active&&(e=!0,n.preventDefault(),this.menu.select(n));break;case o.TAB:this.menu.active&&this.menu.select(n);break;case o.ESCAPE:this.menu.element.is(":visible")&&(this.isMultiLine||this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(e)return e=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),void 0;if(!i){var n=t.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(t){return s?(s=!1,t.preventDefault(),void 0):(this._searchTimeout(t),void 0)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(t){return this.cancelBlur?(delete this.cancelBlur,void 0):(clearTimeout(this.searching),this.close(t),this._change(t),void 0)}}),this._initSource(),this.menu=t(" @@ -64,6 +70,9 @@
+     |     +
+ +
+
+ +
+
+
+ +
+ +
+
+
+
+ + +
+
+ + + + + + + + + + + + {% for image in hubImages %} + + + + + + + + + + {% endfor %} + +
TitleOSDescriptionVerArchSize
{{ image.title or 'NA' }}{{ image.properties['os'] or 'NA' }}{{ image.description or 'NA' }} + {{ image.properties.release or 'NA' }}{{ image.architecture or 'NA' }}{{image.size or 'NA' }}
+
+
+
+
@@ -465,7 +532,7 @@
Key :