def filesystemUpdate(name): """ :param name: :return: """ location = Q("location", "", str) file = os.path.join(__detect_app_dir(name), location) if not os.path.exists(file): raise ServiceException("待操作的文件不存在:%s" % location) # 优先判断是否为删除文件 if Q("del", 0, int) == 1: os.remove(file) LOG.info("删除 %s/%s " % (name, location)) else: content = Q("content") if content is None: raise ServiceException("请输入更新的内容") with open(file, 'w', encoding=ENCODING) as f: f.write(content) LOG.info("更新 %s/%s 的内容" % (name, location)) return jsonify(Result.ok())
def uploadNewVersion(): """ 上传 app 新版本资源 1. 若 app 存在(request.id > 0) 从数据库中获取对应的 app 2. 若 app 不存在(request.id=0) 则 request.name 不能为空 创建新的 app :return: """ file = __detect_file() app = __detect_app() auto_create = app.id is None if auto_create: db.session.add(app) # 保存文件到 attachments 目录 saved_file = getAttachPath(file.filename) LOG.info("上传 %s 到 %s" % (file.filename, saved_file)) file.save(saved_file) resource = Resource.fromFile(saved_file, app) db.session.add(resource) is_update = Q('update', 'true', str).upper() == str(True).upper() name, files = services.load_from_file(saved_file, app, is_update) db.session.commit() return jsonify(Result.ok("%s 应用新版本部署成功" % name, files))
def stats(): """ 查看应用状态,接受的参数为`name=n1,n2,n3` //查询成功返回示例 { "success":true, "data":{ "name1":-1, "name2":0, "name3":1 } } //查询失败返回示例 { "success":false, "message":"无法查询容器状态,请检查 Docker 是否运行" } :return: """ names = Q('names', "", str).split(",") containers = services.list_all_container(True) LOG.info("当前所有容器状态:%s", containers) data = dict( (n, -1 if n not in containers else 1 if containers[n]['stat'] == 'running' else 0) for n in names) return jsonify(Result.ok(data=data))
def operate(name, op): if op not in services.OPERATIONS: raise ServiceException("无效的操作类型:{} (可选:{})".format( op, services.OPERATIONS)) LOG.info("即将对容器 %s 执行 %s 操作...", name, op) services.do_with_container(name, op) return jsonify(Result.ok("{} 执行 {} 操作成功".format(name, op)))
def buildJobs(app, config): if hasattr(config, 'JOBS'): scheduler = APScheduler() scheduler.init_app(app) scheduler.start() else: LOG.info( "JOBS is not defined on Config that Scheduler will not start...")
def heartbeat(data): """ 心跳测试,直接返回参数 :param data: :return: """ LOG.info("heartbeat testing : %s", data) return data
def delete(aid=None): aid = aid if aid is not None else Q('ids', type=int) LOG.debug("客户端请求删除 ID=%d 的应用..." % aid) app = __loadApp(aid) db.session.delete(app) db.session.commit() LOG.info("删除 ID=%d 的应用成功" % aid) return jsonify(Result.ok())
def clean(aid): LOG.debug("客户端请求清空 ID=%s 的应用数据..." % aid) app = __loadApp(aid) app_dir = services.detect_app_dir(app) if os.path.exists(app_dir): shutil.rmtree(app_dir) LOG.info("删除 id={} 的应用数据:{}".format(aid, app_dir)) return jsonify(Result.ok())
def init_docker(config): try: network = docker.setup(config) LOG.info("docker client setup done: \n %s \n default network: %s", docker.version(), network) except Exception as e: LOG.error( "cannot connection to Docker Server , please check your config: %s", str(e)) print(traceback.format_exc()) LOG.error("检测到 Docker 配置有误,请重新配置否则无法正常使用相关的功能")
def delete(aid=None): aid = aid if aid is not None else Q('ids', type=int) LOG.info("客户端请求删除 ID=%d 的资源..." % aid) app = Resource.query.get(aid) if app: db.session.delete(app) db.session.commit() LOG.info("成功删除 ID=%d 的资源" % aid) return jsonify(Result.ok()) else: raise ServiceException("ID=%d 的成功不存在故不能执行删除操作..." % aid)
def checkDocker(): with db.app.app_context(): __tip("开始检测 docker connection...") if docker.client is None: LOG.debug("检测到 docker.client 未实例化,即将进行初始化...") docker.setup(current_app.config) LOG.info("^.^ docker.client 初始化成功 ^.^") else: try: LOG.debug("^.^ docker server 通讯正常 : ping=%s ^.^", docker.client.ping()) except Exception as e: LOG.info("调用 docker.client.ping() 时出错:%s", str(e)) docker.cleanup() __tip()
def add(): """ 录入新的应用 :return: """ ps = request.values name = ps.get('name') version = ps.get('version', default="1.0.0") notEmptyStr(name=name, version=version) id = ps.get('id') app = Application(name=name, id=id, version=version, remark=ps.get('remark')) if id and int(id) > 0: oldApp = Application.query.get(id) if oldApp is None: raise ServiceException("ID=%d 的应用不存在故不能编辑" % id) copyEntityBean(app, oldApp) else: # 判断应用名是否重复 oldApp = Application.query.getOne(name=name) if oldApp: raise ServiceException("应用 %s 已经存在,不能重复录入" % name) db.session.add(app) db.session.commit() op = "录入" if id is 0 else "编辑" LOG.info("%s应用 %s" % (op, app)) return jsonify(Result.ok("应用 %s %s成功(版本=%s)" % (name, op, version), app.id))
def load_from_file(file_path: str, application: Application, update=False, **kwargs): """ 从指定的目录加载文件(用户上传的文件要求为`zip`格式的压缩文件) :param application: :param update: True = 迭代更新,False = 全新部署 :param file_path: :param kwargs: remove 是否移除同名的旧容器,默认 True :return: """ if file_path is None or not os.path.exists(file_path): raise ServiceException("参数 file_path 未定义或者找不到相应的文件:%s" % file_path) if not file_path.endswith(ZIP): raise ServiceException("load_from_file 只支持 %s 结尾的文件" % ZIP) app_dir = detect_app_dir(application) if update: files = update_app_with_zip(file_path, app_dir) return application.name, files unzip_dir, files = unzip(file_path) LOG.info("解压到 %s" % unzip_dir) container_name = application.name for file in [str(f) for f in files]: LOG.debug("processing %s", file) if file.endswith(TAR): LOG.info("检测到 %s 文件 %s,即将导入该镜像..." % (TAR, file)) docker.loadImage(os.path.join(unzip_dir, file)) LOG.info("docker 镜像导入成功") if file.endswith(ZIP): """对于 zip 格式的文件,解压到程序根目录""" unzip(os.path.join(unzip_dir, file), app_dir) LOG.info("解压 %s 到 %s" % (file, app_dir)) if file in ["{}-{}.jar".format(application.name, application.version), "{}.jar".format(application.name)]: """ 对于 {application.name}.jar 、 {application.name}-{version}.jar 的文件,直接复制到 app_dir 通常经过 spring-boot 打包的 jar 可以直接运行 """ copyFileToDir(os.path.join(unzip_dir, file), app_dir) if file == APP_JSON: with open(os.path.join(unzip_dir, file)) as app_json: content = __transform_placeholder(app_json.read(), application) LOG.info("获取并填充 %s :%s" % (APP_JSON, content)) app_ps = json.loads(content) if 'args' not in app_ps: app_ps['args'] = {} if 'image' not in app_ps: raise ServiceException("{} 中必须定义 'image' 属性,否则无法创建容器".format(APP_JSON)) container_name = detect_app_name(app_ps['args'], app_ps['image']) LOG.info("检测到 容器名:%s", container_name) # 判断是否需要移除旧的 container old_container = None if kwargs.pop("remove", True): try: old_container = docker.getContainer(container_name) LOG.info("name={} 的容器已经存在:{}, id={}".format(container_name, old_container, old_container.id)) except Exception: pass if old_container is not None: try: old_container.remove() LOG.info("成功删除name=%s 的容器" % container_name) except Exception as e: raise ServiceException("无法删除 name={} 的容器: {}".format(container_name, str(e))) network = docker.createDefaultNetwork() app_ps['args']['network'] = network.name docker.createContainer(app_ps['image'], container_name, app_ps['cmd'], app_ps['args']) LOG.info("APP 容器 创建成功(image=%s,name=%s, network=%s)" % (app_ps['image'], container_name, network.name)) shutil.rmtree(unzip_dir) return container_name, files
def create_app(config_name=None, customs=None): config = getConfig(config_name, customs) initLogger(config) if config.DOCKER_ABLE: init_docker(config) app = Flask(__name__, static_url_path='', static_folder=config.SERVER_STATIC_DIR) # What it does is prepare the application to work with SQLAlchemy. # However that does not now bind the SQLAlchemy object to your application. # Why doesn’t it do that? Because there might be more than one application created. # >>> from yourapp import create_app # >>> app = create_app() # >>> app.app_context().push() app.app_context().push() app.json_encoder = SQLAlchemyEncoder app.config.from_object(config) config.init_app(app) db.app = app db.init_app(app) try: db.create_all() LOG.info("call db.create_all() success!") except Exception as e: LOG.error("error on try to create all tables", e) buildBlueprint(app) buildJobs(app, config) @app.route('/') def index(): """" 跳转到 static/index.html """ LOG.debug("visit index page %s", config.SERVER_INDEX_PAGE) return app.send_static_file(config.SERVER_INDEX_PAGE) # @app.route('/<path:file_relative_path_to_root>', methods=['GET']) # # @app.route('/static/<path:path>') # def static_resource(path): # return send_from_directory(config.SERVER_STATIC_DIR, path) @app.errorhandler(404) def page_not_found(error): return jsonify(Result.error('[404] Page not found!')), 404 @app.errorhandler(Exception) def global_error_handler(exception): """ 全局的异常处理: 1. 打印到 logger :以便记录异常信息 2. 封装成 Result 对象,以 json 格式返回到 Client :param exception: :return: """ LOG.error("%s\n%s", exception, traceback.format_exc()) return jsonify(Result.error(exception)), 500 @app.errorhandler(ServiceException) def service_error_handler(exception): LOG.error("%s\n%s", exception, traceback.format_exc()) return jsonify(Result.error(exception)), 500 @app.errorhandler(500) def internal_error_handler(exception): LOG.error("[500] %s\n%s", exception, traceback.format_exc()) return jsonify(Result.error(exception)), 500 @app.errorhandler(400) def internal_error_handler(exception): LOG.error("[400] %s\n%s", exception, traceback.format_exc()) return jsonify(Result.error(exception)), 400 # 定位静态文件夹为上级 static,否则无法正常浏览静态资源 # app.static_folder = '../static' return app, config
from buter.logger import LOG from buter.server import create_app app, config = create_app() if __name__ == '__main__': # # 打印 url rule :werkzeug.routing.Rule # 格式参考 Spring Mvc: # Mapped "{[/manage/account/{id}],methods=[GET]}" # onto public T com.zeus.web.controller.AbstractController.get(java.lang.Long) # for rule in app.url_map.iter_rules(): LOG.info("Mapped {:30} methods={:30} onto {}".format( rule.rule, ', '.join(rule.methods), rule.endpoint)) app.run(host=config.SERVER_HOST, port=config.SERVER_PORT, debug=config.DEBUG, use_reloader=config.USE_RELOADER, ssl_context=config.HTTPS)