背景
接触 python 两周有余了。越来越喜欢这门语言,因为做什么都足够快,不像 java 那么笨重。虽然它还是那么容易出错。但是构建一个并不复杂的小工具的时候,它显的效率的多。就像海盗一样:快,狠,准。这里我就不比较 java 和 python 的优劣了,这个话题太大了,以我的水平也怕评论不好。我们就各自使用自己认为最适合项目的语言就好了。刚学 python 的第一周先是学习了一下基本语法,环境,IDE(好在有几年的 java 底子,学的比较快),然后用 pytest 和 allure 做了测试的基础架子,准备测试我们提供的 python 版本的 SDK。之后用 python 重写了一遍环境管理的那一堆 shell 脚本,比 shell 少些了很多代码,之后扩展也好,维护也好都方便了很多 (后悔这么晚才学 python 中~~~)。上周末在家开始学习 django 和 flask, 考虑到底选什么作为建站的框架。后来还是选了 flask,因为够小,够灵活。我暂时的需要可能不需要数据库之类的东西。所以也就没选什么都包办了的 django。于是我拿我们环境管理的需求练手。之前是通过一堆脚本控制的,现在我做一个 web,可以通过在页面上点点点就能管理好这些环境。我想一边练习一边记录其中的步骤,给像我一样刚接触的人一个快速上手的指引吧。由于都是业余时间做,所以更新的会比较慢。
Flask
这里介绍一下 Flask 给像我这种刚刚接触的人把。它给我的感觉就是个十分轻量级的,轻量级到令人发指的 web 框架。用过 Flask 之后才知道 springMVC 到底有多重。它使用起来也很简单,不需要任何 web 容器。 直接按下面的方式写一小段代码就可以了
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!'
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True, port=9537)
当你运行这个脚本后,就可以在浏览器访问了。是不是够简单。同样它也支持像 django 那么大而全的功能,不过这些功能并没有被集成到 Flask 里面。 而是在你需要的时候通过安装相应的模块来实现。例如支持数据库的 orm 框架:Flask-SQLAlchemy 支持数据库迁移,升级等功能的模块:Flask_Migrate。 支持页面表单管理的模块:Flask-WTF。 这些都是需要的时候再安装就可以了。
学习资料
贴一下我找到的学习资料
Flask 官方文档的中文翻译版本:欢迎使用 Flask — Flask 0.10.1 文档
Flask 所有扩展模块的文档:Extensions — Flask Documentation (3.0.x)
一个我觉得比较好的博客:数据库 — flask mega-tutorial 1.1 documentation
开始
OK,我们首先安装 Flask 吧,直接用 pip 安装就好了。暂时我们的功能还很简单,不需要其他的扩展模块。先贴一个图片看看我们的项目结构。
我从上到下开始介绍吧
- env_info 是我们所有环境的配置文件,暂时我没有用数据库来保存这些信息。里面都是 ini 文件,详情不表了。
- lib 是我放置控制 docker 的脚本的地方。如大家所见,都是些 compile,deploy 什么的
- web 是我们 Flask 的根目录
- static 是我们放置静态文件的地方,例如 css,js,图片什么的。注:这是 Flask 的默认设置
- templates 是我们所有模板的文件,大家可以先理解为所有的 html 文件都放在这里。注:这是 Flask 的默认设置
首先我们先在 web 目录的init.py 文件里写一下我们 Flask 的配置:
# coding=utf-8
from flask import Flask
app = Flask(__name__)
# 如果想要使用Flask-WTF的表单,需要一个config文件
app.config.from_object('config')
接着创建一个 urls.py 文件,用来编写我们的路由和启动命令。就如我们上面的例子一样,我们这样写
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True, port=9537)
之后运行 urls.py 就算是我们的启动脚本了。然后写一个首页的路由方法。如下:
@app.route('/')
@app.route('/index', methods=['GET', 'POST'])
def index():
configs = list_all_config()
return render_template('index.html', configs=configs)
我们来解释一下吧。 @app.route, 就是我们的路由方法,可以配置多个, 也就是多个 url 都可以绑定到这一个方法上。 methods 参数设定了方法类型。 可以看到方法里面我们取了所有的配置信息 (就是 env_info 中所有的配置信息)。我们希望将所有的配置信息都展示在首页上。 所以取出信息以后,我们使用 render_template 这个渲染模板的方法,将 index.html 进行渲染并把 configs 传递到模板中 (在 Flask 中页面就是模板)。 好了,现在我们去看看这个 index.html 里是怎么写的吧。
<div class="row clearfix">
{% for config in configs %}
<div class="col-md-4 column">
<h2>
{{ config.name_prefix }}
</h2>
<p>
</p>
<p>
<a class="btn" href="#">查看详情 »</a>
<a class="btn" href="/restart/{{ config.name_prefix }}">部署环境 »</a>
</p>
</div>
{% endfor %}
</div>
这里来看一下 Flask 内置的 Jinjia2 的语法吧。流程控制语句使用{% content %} 来表示, 直接把元素输出到页面上使用{{% element %}}。 上面的代码就是说,我循环一下传递进来的 configs 列表。把每个配置的名字都展示到页面上,然后每个配置都有两个链接,分别是查看配置的详情和部署环境。这个是后话了。 现在如果我们启动 web 服务,使用浏览器访问就会看到所有的配置都显示在了我们的主页上。 但是还有点不够,那就是我们的页面实在太难看了。 没有任何的布局。 但是我还是个前端的小白,让我重头学一遍 css,js 什么的太费时间了。 所以我们可以取巧一下,偷懒一下。来给我们的 web 换个装
bootstrap
换装工具就是 bootstrap 了。 下载地址如下: Bootstrap · The most popular HTML, CSS, and JS library in the world.
我们要把下载下来的文件解压,然后放到 static 目录下,如图:
然后我们在 index.html 加入引用
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 引入 Bootstrap -->
<link href="http://cdn.static.runoob.com/libs/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<!-- HTML5 Shim 和 Respond.js 用于让 IE8 支持 HTML5元素和媒体查询 -->
<!-- 注意: 如果通过 file:// 引入 Respond.js 文件,则该文件无法起效果 -->
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
接着我们使用在线的页面生成工具:http://www.runoob.com/try/bootstrap/layoutit/#
我们用这个工具拖拽好自己的布局之后。就可以点击那个下载。把 HTML 代码弄到我们的页面里。 然后我们就有了如下的页面了
好吧,我不具备什么审美天赋。也没下什么大工夫。看着顺眼就行。
最关键的功能
好了,到了最关键的功能了,那就是部署一个环境。 所以在我们的首页中才会有这样一个链接:
<a class="btn" href="/restart/{{ config.name_prefix }}">重新启动 »</a>
现在我们需要为这个链接创造另一个路由方法。如下:
@app.route('/restart/<name>', methods=['GET', 'POST'])
def restart(name):
workspace = os.path.dirname(os.path.abspath(os.path.dirname(__file__)))
path = workspace + '/boot.sh'
config = workspace + '/env_info/' + name + '.ini'
command = 'bash %s %s' % (path, config)
result = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
jobs = Jobs.jobs
if name in jobs.keys():
jobs[name].kill()
jobs.pop(name)
jobs[name] = result
return render_template('restart.html', name=name)
这里我们看到了一个用法/restart/。 这个<>中的 name 就是页面向我们传递的参数。 所以在 restart 方法里我们才可以使用 name 这个参数。我们根据 name 找到相应的配置文件并使用 subprocess 模块进行调用。由于我们希望这个步骤是异步的,不能再部署过程中页面一直处于卡死的状态。所以使用 Popen 这个函数来调用。 我们把标准输出和标准错误都重定向到管道中。把这个调用后返回的对象保存在一个全局的 dict 中。方便之后取 log 的时候用。 那么现在我们可以在页面上点击链接进行部署了。但是有一个问题,就是我们不知道部署当前是失败了还是成功了。 我们希望能实时的在页面上看到部署的日志, 我们需求页面能实时的显示最新的 log 而不必刷新页面。
Jquery Ajax
我这个前端水平就只能想到这样一个方案了,现在我们去现在一个 Jquery.js 吧。 同样放到 static 目录下。
然后我们再搞一个 log 页面, 在 head 标签中我们引入 Jquery。
<script type=text/javascript src="{{ url_for('static', filename='jquery.js') }}"></script>
然后我们加入下面的一段 JS:
<script type=text/javascript>
$SCRIPT_ROOT = {{ request.script_root|tojson|safe }};
</script>
<script type=text/javascript>
$(function () {
for (var i = 1; i < 10000; i++) {
$.get($SCRIPT_ROOT + '/log/{{ name }}', function (data) {
$("#log").append(data);
})
}
});
</script>
<dev id="log"></dev>
我来解释一下上面这段 JS。 $.get 方法就是动态执行一个请求的方法,后面跟上 url。 $("#log") 是 JS 的选择器,用来定位页面元素,意思是找到 id 为 log 的元素并执行 append 的操作。所有的代码的意思就是循环不停的调用这个 get 方法,并将返回值作为 data 传递给之后的 function,这个 function 用来定位元素并把日志追加到页面上。 那么现在,我们唯一缺的就是再写这么一个路由方法实时读取日志了。如下:
@app.route('/log/<name>')
def update_log(name):
# time.sleep(1)
job = Jobs.jobs[name]
# error = job.stderr.readline()
out = job.stdout.readline()
return '<p>'+out+'</p>'
看过之前 restart 方法的代码,我们知道所有的 Popen 返回的结果对象都放到了一个全局的 dict 里。 现在我们根据页面 AJax 传递 name 把它找出来。然后读取一行日志信息返回。OK,万事大吉。 我不会截动态图~~ 大家见谅。。。
唠叨两句
我知道前端的发展现在已经很厉害了,Jquery 这种玩意都不知道过时了多久,我们公司的前端都已经在用 React.js。可我是没那么多时间去学了。反正我们做的都是给项目内部用的,所以我想不用纠结什么前沿技术了,够用就好。目前我就做了这么多。 大概实现了最主要的几个功能:显示配置,部署环境,实时获取 log。之后加上创建配置和修改配置的功能后就基本上达到可用状态了。额,想想在老东家的时候用 springMVC 为了达到可用状态可是比这多花了不少的时间。所以我现在很喜欢 python,在构建这些工具的时候效率更高,更快。 有个小段子是我最开始请教我司写 SDK 的小伙子的时候,问他你为什么选择用 python,他回答说:为了多活两年。 恩,现在我有点理解他为什么这么说了。 之后我会慢慢的完善这个小网站,就当是练手了,然后我会慢慢的把过程都记录下来发到社区里,一个是对我的学习和工作有个记录,再一个也希望对一些同学有所帮助。毕竟在工作中做测试平台,测试工具什么的可能都要封装成一个 web。由于这纯是我的个人意愿,组织上并没有给我在工作中安排时间去做。 所以我都是抽空搞一搞,进度可能有点慢。希望之后能成熟到给一些非技术人员,例如产品,售前的同学去搭建一个他们需要的环境,而不再需要懂技术的人跑去给他们运行脚本部署。恩~ 保持学习