BlueXIII's Blog

热爱技术,持续学习

0%

引子

SSH连接是我们与服务器交互的主要手段,每个程序员每天都会用到,就像喝白开水一样普通。
就是这样一个简单而又高频的动作,如果配置得当,或许能极大的提高我们工作的幸福感吧。

前不久一个同事刚从Windows切换到macOS平台,问我SecureCRT有没有mac下的版本,我说即然都用上了基于BSD的mac了,应该不再需要SecureCRT这样的东西了才对,SSH如果配置好了,体验应该完爆Windows。于是就有了写下这篇文章的念头,填一下之前的坑。

选择终端GUI工具

首先第一步是选择一个好用的GUI工具,毕竟大多数人都是在DE下工作,极少人会喜欢工作在tty1这样的文字介面下。

Windows平台

Windows平台下的工具比较多,下面捡几个主要的来说一说优缺点:

  • SecureCRT - 老牌的商业软件,同时也提供Mac和Linux版本,功能强大,费用昂贵,但貌似身边所有人都在用盗版,PASS
  • Xshell - 有免费的License,但只允许非商业场景下使用,这个也PASS
  • Putty - 好吧,只能是它了,免费,功能简单,但是也足够用了
  • Cygwin - 差点把这个忘了,严格来说它并不仅仅是一个SSH工具,也值得推荐

macOS平台

macOS平台的终端工具比较好选,公认的只有iTerm2一个:

  • 原生Terminal - 开箱即用,功能上也不过不失,但抛弃它,只是因为有更好的iTerm2。。。
  • iTerm2 - 免费,支持众多的自定义选项,可以完全替代原生Terminal,没得选,就是它了

Ubuntu平台

Ubuntu默认自带一个gnome-terminal,虽然不像iTerm2那样讨喜,但胜在简洁,个人认为是最优的选择吧:

  • Terminator - 支持窗口拆分,但界面丑,不能忍,PASS
  • Guake - 支持下拉式的呼出,但界面更丑,PASS
  • 原生Terminal - 界面简洁,功能够用,最终还是用回了它

iTerm2使用简介

Putty和gnome-terminal的配置都比较简单,记住几个快捷键就可以了,macOS下的iTerm2配置和使用都要麻烦些,这里单独拿出来写一下。

配置呼出快捷键

使用Cmd+,打开设置界面,切换到Keys一栏

在左下方可以设置 呼出/隐藏 iTerm2的快捷键,这里我设置成了Cmd+ESC,瞬间呼出,比Alfred/Spotlight还要方便。
另外还可以在Profile一栏中修改窗口为全屏+半透明,完美。

选中即复制

使用鼠标选中一段文字后,默认就已经复制到剪贴板了,直接用Cmd+V粘帖即可。

Profile设置

使用Cmd+,打开设置界面,切换到Profile一栏
在这里可以配置远程服务器的连接参数

配合sshpass这个命令,可以实现类似SecureCRT的Session管理的功能。
注意,sshpass直接使用明文保存密码(SecureCRT是加密后存在本地),已经不推荐使用,下面会介绍一个更好的方式。

分屏

分屏功能相当好用,例如我们可以在屏幕左侧查看日志,右侧进行常规操作
Cmd+D 垂直分割
Cmd+Shift+D 水平分割

自动完成

输入前几个关键字后,按Cmd+;,会有一个自动补全功能

命令历史记录

使用Cmd+shift+H可以打开命令历史记录

使用Tmux窗口管理工具

GUI工具有了,我们还缺一个通用的窗口管理工具–Tmux

Tmux是一个终端复用器,它可以激活多个终端或窗口, 还可以将屏幕水平或纵向切分成多个窗口。
类似于screen,它可以关闭窗口将程序放在后台运行,需要的时候再重新连接。

其实iTerm2等GUI工具也可以实现类似的功能,但Tmux的好处是:

  1. 它是一个字符终端软件,不需要任何GUI的支持,通用性更好
  2. tmux可以 保持多个会话 ,只要不关机,就可以随时恢复Session

macOS下的安装

1
brew install tmux

Ubuntu下的安装

1
sudo apt-get install tmux

tmux的主要元素分为三层:

  • Session 一组窗口的集合,通常用来概括同一个任务
  • Window 单个可见窗口,和ITerm2中的Tab类似
  • Pane 窗格,被划分成小块的窗口

tmux默认的前置操作是CTRL+b。例如,我们想要新建一个窗体,就需要先在键盘上摁下CTRL+b,松开后再摁下n键。
下面所有的prefix均代表CTRL+b

  • 查看/切换session prefix s
  • 离开Session prefix d
  • 重命名当前Session prefix $
  • 新建窗口 prefix c
  • 切换到上一个活动的窗口 prefix space
  • 关闭一个窗口 prefix &
  • 使用窗口号切换 prefix 窗口号
  • 切换到下一个窗格 prefix o
  • 查看所有窗格的编号 prefix q
  • 垂直拆分出一个新窗格 prefix “
  • 水平拆分出一个新窗格 prefix %
  • 暂时把一个窗体放到最大 prefix z

使用oh my zsh

工具已经齐全,但攘外必先安内,在连接远程服务器之前,我们先优化一下本机的Shell。

Linux系统中已经内置了几种shell,一般默认的是bash

zsh比bash更加好用,且完全兼容bash,但它配置繁琐。幸亏有了oh-my-zsh,让zsh的配置难度大大降低。

首先安装一下zsh,mac系统无需安装,Ubuntu可以通过apt安装

1
sudo apt install zsh

这里是oh my zsh官网

根据官网的介绍,安装相当的简单,只需要一行命令即可

1
2
$ sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"

zsh的配置文件是~/.zshrc,各项配置都有对应的注释,非常清晰,在这里可以修改主题,其实默认的已经很好看了

以下是zsh的一些使用技巧,请尽量记住,因为都很常用:

  • 连按两次Tab会列出所有的补全列表并直接开始选择,补全项可以使用 ctrl+n/p/f/b上下左右切换
  • 命令选项补全。在zsh中只需要键入 tar - 就会列出所有的选项和帮助说明
  • 命令参数补全。键入 kill 就会列出所有的进程名和对应的进程号
  • 更智能的历史命令。在用或者方向上键查找历史命令时,zsh支持限制查找。比如,输入ls,然后再按方向上键,则只会查找用过的ls命令。而此时使用则会仍然按之前的方式查找,忽略 ls
  • 智能跳转,安装了 autojump 之后,zsh 会自动记录你访问过的目录,通过 j 目录名 可以直接进行目录跳转,而且目录名支持模糊匹配和自动补全,例如你访问过 hadoop-1.0.0 目录,输入j hado 即可正确跳转。j --stat 可以看你的历史路径库。
  • 目录浏览和跳转:输入 d,即可列出你在这个会话里访问的目录列表,输入列表前的序号,即可直接跳转。
  • 在当前目录下输入 ..... ,或直接输入当前目录名都可以跳转,你甚至不再需要输入 cd 命令了。在你知道路径的情况下,比如 /usr/local/bin 你可以输入 cd /u/l/b 然后按进行补全快速输入
  • 通配符搜索:ls -l **/*.sh,可以递归显示当前目录下的 shell 文件,文件少时可以代替 find。使用 **/ 来递归搜索
  • 扩展环境变量,输入环境变量然后按 就可以转换成表达的值

使用config记录远程连接

现在开始,我们再看一下如何连接远程服务器。

如果要管理几十台至上百台的主机,那么有没有什么方法能实现类似SecureCRT中Session管理那样的功能呢?

最简单的是使用iTerm2中的profile功能,它可以配置多台主机的连接,并可以使用tag进行归类。
但这种方式首先太依赖于GUI工具,离开了iTerm2就完全用不了。
其次是通用性不够好,配置文件不能跨平台,我们辛辛苦苦在Mac上配好了一份主机清单,但回家扔到Ubuntu下就没法用了。

有没有一种更为通用的方法呢?答案是直接使用openssh自带的config功能

配置文件的路径是:~/.ssh/config,如果不存在,可以新建一个

内容非常简单

1
2
3
4
Host demohost
HostName 192.168.1.1
User username
Port 10022

第一行Host后面,可以为这个连接起一个简单的名字
后面几行记得缩进,HostName后面是IP地址,User后面是用户名,这两项是必填的。如果端口不是标准的22,还可以用Port指定端口。

配置好后,我们只需要输入ssh demohost,即可快速打开SSH连接了。

如果记不清连接名也没有关系,利用zsh的自动补全功能,输入ssh 关键字,再按一下TAB键,会列出所有包含关键字的连接,使用方向键选择后,回车确认即可。

使用ssh-copy-id免密码登录

接着再看一下,如何实现免密码登录远程主机。

之前我们提到过使用sshpass这个工具,可以直接将密码写进命令行中,从而实现非交互式的免密码登录

1
sshpass -p password ssh demouser@92.168.1.1

但这种方式使用明文保存密码,非常不安全,不建议使用。

其实还有一种更好更直接的方式,那就是使用ssh-copy-id建立ssh信任关系,从而免密码登录。

  1. 首先,在本地机器上使用ssh-keygen产生公钥私钥对

    1
    ssh-keygen

    一路回车即可,会在~/.ssh目录下生成公钥私钥对。此命令只需执行一次即可。

  2. 然后,用ssh-copy-id将公钥复制到远程机器中

    1
    ssh-copy-id -i demouser@192.168.1.1

    按提示输入一次密码,ssh-copy-id就会自动将刚才生成的公钥id_rsa.pub追加到远程主机的~/.ssh/authorized_keys后面了,以后的 ssh 以及 sftp 连接,都不用输入密码了。

至此,我们只要输入ssh demousersftp demouser,回车,就可以直接登录远程主机了。

安利一个小工具

最后的最后,再介绍一个很恶搞,但相当有用的小工具,thefuck GitHub页面

当我们敲错了命令,比如忘记加sudo时,只需要输入fuck并大力敲下回车,它就会非常智能的帮助我们更正了,非常有趣。

1
2
3
4
5
6
7
8
9
➜ apt-get install vim
E: Could not open lock file /var/lib/dpkg/lock - open (13: Permission denied)
E: Unable to lock the administration directory (/var/lib/dpkg/), are you root?

➜ fuck
sudo apt-get install vim [enter/↑/↓/ctrl+c]
[sudo] password for nvbn:
Reading package lists... Done
...

参考文档

http://cenalulu.github.io/linux/tmux/
http://harttle.com/2015/11/06/tmux-startup.html
http://wulfric.me/2015/08/iterm2/
http://yijiebuyi.com/blog/e310fc437f32006eb6aa42cad1783587.html
http://wdxtub.com/2016/02/18/oh-my-zsh/

什么是Redmine

Redmine是用Ruby On Rails开发的一款基于WEB的项目管理软件。
它集成了项目管理所需的各项功能,可以同时处理多个项目。
请重点关注 问题甘特图日历 三个功能模块。

我的工作台

点击左上角 我的工作台 按钮

  • 指派给我的问题
    提供一个跨项目的指派给当前用户的问题列表,显示问题的ID,项目,跟踪标类型签和主题。
  • 已报告的问题
    提供一个跨项目的由当前用户报告的问题列表,显示问题的ID,项目,跟踪标签类型和主题。 每一个用户可以通过点击”个性化定制本页”的链接个性化我的工作台。 然后用户可以选择哪些可用的模块被显示:
  • 日历
    提供一个跨项目的每周日历概述
  • 文档
    提供一个跨项目的最近文档概述
  • 最近的新闻
    提供一个跨项目的最近新闻概述
  • 耗时
    提供一个跨项目的关于当前用户最近7天工时的概述
  • 跟踪的问题
    提供一个跨项目的由当前用户跟踪的问题列表

项目概述

点击左上角 项目 按钮,再点击 渠道移动营销 项目

即可查看 项目概述 页面

项目活动


该页面列出了该项目所有活动的历史记录, 这些活动包括:

  • 问题
  • 变更
  • 新闻
  • 文档
  • 文件
  • Wiki编辑记录
  • 帖子
  • 耗时

该页面的右边栏允许你选择具体显示哪类活动

问题跟踪

问题是Redmine的核心业务。 一个问题绑定到一个项目, 由某一用于创建, 可以关联到某一版本, 等等。

  • 查看某一问题
    在问题列表页面点击某一问题的链接, 可以查看该问题的具体描述。

  • 过滤器的应用
    默认情况下, 问题列表显示了所有处于打开状态的问题。 你可以添加过滤器, 点击”应用”链接刷新问题列表, 点击”清除”链接删除设置的滤器。
    可以通过点击”+”号按钮, 为过滤器字段选择多个值。 这时会出现一个选择列表, 按住”ctrl”后, 可选择多个值。

  • 自定义查询
    当刷新页面后, 刚设置的过滤器就会消失, 可以通过点击”保存”链接保存你设置的过滤器, 从而建立自定义查询。
    在新建自定义查询的界面输入自定义查询的名称, 以及过滤器和其他属性的设置。
    点击保存之后, 新建的自定义查询将会出现在问题列表界面的右边栏中。

  • 快捷菜单

    在问题列表的某一个问题上, 点击鼠标右键, 将弹出一个快捷菜单, 用于便捷编辑问题。
    通过快捷菜单可以快修改问题的进度。

  • 新建问题
    要创建新的问题, 需要有新建问题的权限。
    创建问题时, 最重要的字段是跟踪标签字段, 它决定了问题的类型。
    默认情况下, Redmine有三种跟踪标签:
    功能 feature
    缺陷 bug
    支持 support

日历

在问题列表页面, 点击右边栏的”日历”链接, 即可进入日历界面。
日历提供了一个按月份显示的项目预览。 在这里你可以看到一个任务状态的起止日期。
像Redmine提供的其他视图一样, 可以通过设置过滤器从而决定日历图上显示的内容。


注意3种图标的含义

甘特图

在问题列表页面, 点击右边栏的”甘特图”链接, 即可进入甘特图界面。
甘特图显示问题的起止日期以及版本的截至日期

新闻

在”新闻”选项卡下, 你可以发布关于项目的新闻条目, 甚至任何你喜欢的新闻条目。

文档

在这里可以书写不同类型的文档, 默认有两种文档类型:

  • 用户文档
  • 技术文档

管理员可以添加文档类型

Wiki

在这里可以查看或编辑WIKI页面

文件

在这里可以共享项目需要用到的其他资源

参考文档

Redmine 用户手册

qiniu-imgup简介

qiniu-imgup是一个七牛云图片上传工具,目标是简化MarkDown写作中的贴图的繁琐步骤,可以快速将剪贴板中的图片上传至七牛云,并返回MarkDown格式的链接。
适用人群:使用七牛云做为图床的MarkDown编写者。

由于平时工作主要是用macOS 10.12及Ubuntu 16.04两台笔记本,macOS下之前一直在用一个名为iPic的收费软件(30RMB/年,UX做的非常好),Ubuntu下直接找不到类似工具,于是分别针对两个平台各写了一个小工具。

两个版本在思路及实现上的差异还是比较大的,Mac版偏重于用Python原生实现,Linux版则借用了一些第三方系统工具,下面简单做一下分享,有需要的同学可以直接拿去用。

Linux版本的整体思路及部分代码来自 博客 77695的自留地 ,侵删

使用方法介绍

在编写MarkDown文档时,如果使用七牛云做图床,插入一张图片需要以下几个繁琐的步骤:

  1. 截图
  2. 保存为文件
  3. 打开浏览器,在七牛云后台上传图片
  4. 复制图片的HTTP URL
  5. 在编辑器中将URL拼接为MarkDown的链接格式
  6. 粘帖链接

mac版本将整个操作简化为了3个快捷键操作:

  1. cmd+shift+ctrl+4 - 截图到剪贴板
  2. cmd+shift+u - 使用自定义热键,上传并获取MarkDown链接
  3. cmd+v - 粘帖MarkDown链接

linux版本整个操作简化为了2个快捷键操作:

  • alt+shift+s 调用simg截图
  • alt+shift+u 调用uimg上传并自动粘帖MarkDown链接

实现思路

Mac版实现思路

  1. 使用系统自带截图工具,或QQ截图工具,截图至剪贴板
  2. 使用PIL库中的ImageGrab,将剪贴板中的图片保存
  3. 使用qiniu库,生成token,并上传图片
  4. 将七牛云返回的URL拼接为MarkDown格式链接
  5. 使用clipboard库,将链接放到剪贴板中
  6. 使用os.system库,调用osascript发送系统通知,在屏幕右上角提示成功/失败
  7. CMD+V粘帖链接至编辑器

其中2-6步,是由qiniu-imgup来实现的。
可以用Automator工具,将shell脚本封装成一个APP,在系统设置中就可以指定一个快捷键了。

Linux版实现思路

  1. 使用shell脚本,调用第三方截图工具shutter,并将图片保存至临时目录。
  2. 使用qiniu库,生成token,并上传图片
  3. 将七牛云返回的URL拼接为MarkDown格式链接
  4. 直接使用print打印链接至console
  5. 调用libnotify-bin,发送系统通知,在屏幕右上角提示成功/失败
  6. 使用shell脚本,将Python输出结果传递给第三方工具xclip,复制到剪贴板
  7. 使用shell脚本,调用第三方工具xdotool,模拟键盘Ctrl+V操作,直接进行粘帖

其中2-5步,是由qiniu-imgup来实现的。
过程中使用到了第三方工具shutter、xclip及xdotool。
由于使用了xdotool,无须手工Ctrl+V粘帖了,节省了一个操作步骤。

安装与配置

Mac版安装与配置

  1. 安装依赖
    1
    sudo pip3 install -r requirement.txt
  2. 配置config.py,填入七牛云的AK、SK等参数
  3. 使用Automator将shell脚本uimg封装为APP
  4. 在系统设置中(System Preference->Keyboard->Shortcuts->App Shortcuts),为APP分配快捷键

Linux版安装与配置

  1. 依赖及工具安装
    1
    2
    3
    4
    5
    6
    sudo pip3 install qiniu
    sudo apt-get install shutter
    sudo apt-get install xclip
    sudo apt-get install parcellite
    sudo apt-get install xdotool
    sudo apt-get install libnotify-bin
  2. 将程序目录拷贝至/opt/qimg下
  3. 设置环境变量 export PATH=/opt/qimg:$PATH
  4. 在Shutter设置中,将图片自动保存位置设为/tmp/snap_shutter
  5. 在System Setting - Keyboard中,设置快捷键分别指向simg截图和uimg上传

源码

什么是virtualenv

virtualenv是一个用于创建Python虚拟环境的工具。

虚拟环境又是什么呢?它是Python解释器的一个私有副本,在这个环境中可以安装私有包,而不会影响到系统中安装的全局Python解释器。
当一台主机上运行着多个Python应用时,使用虚拟环境可以有效的避免包的混乱和版本的冲突。

virtualenv官网

如果懒得看作者瞎BB,可以跳过下面的章节,直接访问官网:
https://virtualenv.pypa.io

安装

在Linux或Mac中,可以使用pipeasy_install来快速安装virturlenv:

1
sudo pip3 install virtualenv

或:

1
sudo easy_install virutalenv

测试安装是否成功:

1
2
virtualenv --version
15.1.0

创建虚拟环境

下面以macOS为例,创建一个python3的虚拟环境。
假设已经有一个工程目录pyapp,进入工程目录中,执行以下命令:

1
2
3
4
5
6
7
cd pyapp
virtualenv venv

Using base prefix '/Library/Frameworks/Python.framework/Versions/3.5'
New python executable in /Users/foobar/tmp/pyapp/venv/bin/python3.5
Also creating executable in /Users/foobar/tmp/pyapp/venv/bin/python
Installing setuptools, pip, wheel...done.

现在,virtualenv为我们在工程目录中,创建了一个名为venv的子目录,它里面保存了一个全新的虚拟环境,非常简单。

现在可以进入ven/bin目录下,看看里面都有些什么东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cd venv/bin
ls -al
total 160
drwxr-xr-x 16 foobar staff 544 Mar 13 09:44 .
drwxr-xr-x 7 foobar staff 238 Mar 13 09:44 ..
-rw-r--r-- 1 foobar staff 2089 Mar 13 09:44 activate
-rw-r--r-- 1 foobar staff 1031 Mar 13 09:44 activate.csh
-rw-r--r-- 1 foobar staff 2185 Mar 13 09:44 activate.fish
-rw-r--r-- 1 foobar staff 1137 Mar 13 09:44 activate_this.py
-rwxr-xr-x 1 foobar staff 262 Mar 13 09:44 easy_install
-rwxr-xr-x 1 foobar staff 262 Mar 13 09:44 easy_install-3.5
-rwxr-xr-x 1 foobar staff 234 Mar 13 09:44 pip
-rwxr-xr-x 1 foobar staff 234 Mar 13 09:44 pip3
-rwxr-xr-x 1 foobar staff 234 Mar 13 09:44 pip3.5
lrwxr-xr-x 1 foobar staff 9 Mar 13 09:43 python -> python3.5
-rwxr-xr-x 1 foobar staff 2348 Mar 13 09:44 python-config
lrwxr-xr-x 1 foobar staff 9 Mar 13 09:43 python3 -> python3.5
-rwxrwxr-x 1 foobar staff 25384 Mar 13 09:43 python3.5
-rwxr-xr-x 1 foobar staff 241 Mar 13 09:44 wheel

可以看到virtuanenv为我们创建了一个python3的虚拟环境。

但在Ubuntu16.04下,virtualenv默认会创建python2.7的虚拟环境。这时,只要在创建时加入-p python3参数即可解决。

1
virtualenv -p python3 venv

激活和退出虚拟环境

在使用虚拟环境之前,必须将其激活,命令如下:

1
2
3
source venv/bin/activate

. venv/bin/activate

这时,可以看到shell提示符前面加上了(venv)前缀,说明已经工作在虚拟环境之下了

1
(venv)➜pyapp

退出虚拟环境只要执行:

1
deactivate

在虚拟环境中安装依赖

在虚拟环境中,可以像平时一样使用pip安装依赖,操作完全没有区别:

1
2
3
4
5
6
7
pip3 install requests

Collecting requests
Downloading requests-2.13.0-py2.py3-none-any.whl (584kB)
100% |████████████████████████████████| 593kB 13kB/s
Installing collected packages: requests
Successfully installed requests-2.13.0

但依赖会安装到venv/lib/python3.5/site-packages目录下,可以进去看一下:

1
2
cd venv/lib/python3.5/site-packages
ls -al

当然还可以使用-r requirements.txt来批量安装依赖,也是没有问题的:

1
2
3
4
5
6
7
pip3 install -r requirements.txt

Collecting beautifulsoup4 (from -r requirements.txt (line 1))
Downloading beautifulsoup4-4.5.3-py3-none-any.whl (85kB)
100% |████████████████████████████████| 92kB 13kB/s
Installing collected packages: beautifulsoup4
Successfully installed beautifulsoup4-4.5.3

删除虚拟环境

1
rmvirtualenv venv

在PyCharm中使用virtualenv

PyCharm已经提供了对virtualenv的支持,详情可查看官方文档:
https://www.jetbrains.com/help/pycharm/2016.1/creating-virtual-environment.html

首先是设置virtualenv的路径,PyCharm应该会自动发现并配置上,查看一下即可。

位置: CMD+, –> Project –> Project Interpreter,点击右上角的齿轮图标,选择Create VirtualEnv

引言

继续接着上次的话题,上周整理出来的flask-rest脚手架,用于快速构建Restful API。虽然后端的代码量很少,但前端会变得很重,所以总体开发速度并不算太快,不太适合小型项目。
于是又整理了一套flask-adminlte脚手架,目标是用较传统的方式,减少整体的代码量,加快开发速度,以应对一些比较极端的需求。大体估算,开发时间可以做到SpringBoot全家桶的30%左右。

源码

http://git.si-tech.com.cn/guolei/flask-adminlte-handler
https://github.com/xiiiblue/flask-rest-sample

简介

flask-adminlte-handler是一个Python环境下的WEB后台管理系统脚手架,目标是用极少量的代码,快速构建小型WEB应用。请勿在大中型项目中进行尝试。

  1. 使用较传统的重后端+轻前端的方式,降低总体代码量
  2. Web框架使用Flask,默认Jinja模版
  3. ORM框架使用Peewee
  4. 前端套用基于BootStrap的AdminLTE模板

系统截图

  • 登录页

  • 主页

  • 编辑界面

  • 查询界面

第三方依赖

  • peewee
  • pymysql
  • flask
  • flask-script
  • flask-wtf
  • flask-login

环境配置

venv虚拟环境安装配置

1
2
3
sudo pip3 install virtualenv
virtualenv venv
. venv/bin/activate

第三方依赖安装

1
2
pip3 install -r requirements.txt

系统参数配置

  1. 编辑config.py, 修改SECRET_KEY及MySQL数据库相关参数

    1
    2
    3
    4
    5
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret'
    DB_HOST = '127.0.0.1'
    DB_USER = 'foobar'
    DB_PASSWD = 'foobar'
    DB_DATABASE = 'foobar'
  2. 编辑log-app.conf,修改日志路径

    1
    args=('/path/to/log/flask-rest-sample.log','a','utf8')

    数据库初始化

  3. 自动建表
    直接运行python3 models.py

  4. 插入管理员用户(默认admin/admin)

    1
    2
    3
    INSERT INTO `user` (`id`, `username`, `password`, `fullname`, `email`, `phone`, `status`)
    VALUES
    (1, 'admin', 'pbkdf2:sha1:1000$Km1vdx3W$9aa07d3b79ab88aae53e45d26d0d4d4e097a6cd3', '管理员', 'admin@admin.com', '18612341234', 1);

    启动应用

    1
    2
    3
    nohup ./manage.py runserver 2>&1 &

    ./run_app_dev.py (仅限测试)

项目目录结构

  • /app/auth 用户认证相关代码
  • /app/main 主要功能点相关代码
  • /app/static JS、CSS等静态文件
  • /app/template 页面模版
  • /app/models.py Peewee模型
  • /app/utils.py 工具模块
  • /conf 系统参数及日志配置

相关学习文档

引言

cx_Oracle是Python环境下的一个(好像也是唯一的一个)用于操作Oracle的第三方模块。
最近在写某个对帐程序时,不得已要连接一台Oracle库,于是用到了cx_Oracle,总的来说还是比较顺利的,期间遇到几个有意思的小坑写出来分享一下。

RHEL 6.4下安装cx_Oracle

RHEL6.4和cx_Oracle比较搭,安装时应该不会遇到什么挫折,使用RPM安装好instantclient后,直接用pip3安装cx_Oracle即可。

需要注意地方:

  1. 尽量使用RPM方式安装instantclient,安装更方便,而且比zip更好管理。
  2. instantclient的版本选择的是11.2.0.4.0,没有选择12。对于Oracle/WebLogic这类闭源的东西,还是选择次最新版本的比较稳妥。

安装步骤:

  1. 下载Linux版本的instantclient
    这里是 Oracle官网 instantclient下载页面 ,下载以下3个RPM包:
    oracle-instantclient11.2-basic-11.2.0.4.0-1.x86_64.rpm
    oracle-instantclient11.2-devel-11.2.0.4.0-1.x86_64.rpm
    oracle-instantclient11.2-sqlplus-11.2.0.4.0-1.x86_64.rpm

  2. 安装instantclient并设置环境变量

    1
    2
    3
    4
    yum -y install libaio bc flex
    rpm -ivh oracle-instantclient11.2-basic-11.2.0.4.0-1.x86_64.rpm
    rpm -ivh oracle-instantclient11.2-devel-11.2.0.4.0-1.x86_64.rpm
    rpm -ivh oracle-instantclient11.2-sqlplus-11.2.0.4.0-1.x86_64.rpm
  3. 设置环境变量

    1
    2
    3
    4
    5
    echo 'export ORACLE_VERSION="11.2"' >> $HOME/.bashrc
    echo 'export ORACLE_HOME="/usr/lib/oracle/$ORACLE_VERSION/client64/"' >> $HOME/.bashrc
    echo 'export PATH=$PATH:"$ORACLE_HOME/bin"' >> $HOME/.bashrc
    echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"$ORACLE_HOME/lib"' >> $HOME/.bashrc
    source $HOME/.bashrc
  4. 使用pip安装cx_Oracle

    1
    pip3 install cx_Oracle

    macOS 12下安装cx_Oracle

    macOS下,安装cx_Oracle后的编译过程有可能会报错,一般是因为instantclient安装有误造成的。

需要注意的地方:

  1. mac下的instantclient只有zip包一种安装方式,要注意手工建两个软链接。
  2. pip安装前,注意导入LD_LIBRARY_PATH与DYLD_LIBRARY_PATH两个环境变量。

安装过程:

  1. 下载Mac版本的instantclient
    下载以下3个zip包,并unzip解压至同一目录:
    instantclient-basic-macos.x64-11.2.0.4.0.zip
    instantclient-sdk-macos.x64-11.2.0.4.0.zip
    instantclient-sqlplus-macos.x64-11.2.0.4.0.zip

  2. 建立软链接

    1
    2
    3
    cd /path/to/instant
    ln -s libclntsh.dylib.11.2 libclntsh.dylib
    ln -s libocci.dylib.11.2 libocci.dylib
  3. 设置环境变量

    1
    2
    3
    export ORACLE_VERSION="11.2"
    export ORACLE_HOME="/path/to/instantclient_11_2"
    export PATH=$PATH:"$ORACLE_HOME"
  4. 使用pip安装cx_Oracle,注意提前导入DYLD_LIBRARY_PATH与LD_LIBRARY_PATH

    1
    2
    3
    export DYLD_LIBRARY_PATH="$ORACLE_HOME"
    export LD_LIBRARY_PATH="$ORACLE_HOME"
    pip3 install cx_Oracle

    中文乱码问题

    Oracle中文乱码问题存在已久,使用cx_Oracle时也不例外,解决方法还是设置NLS_LANG环境变量。
    有两种方式,一是在系统中设置永久环境变量,二是直接在代码中使用os.environ设置环境变量,这里推荐后者。

  5. 方法一:在Shell中设置环境变量

    1
    export NLS_LANG="SIMPLIFIED CHINESE_CHINA.UTF8"
  6. 方法二:直接在代码中加入:

    1
    2
    import os
    os.environ['NLS_LANG'] = 'SIMPLIFIED CHINESE_CHINA.UTF8'

PyCharm下不识别cx_Oracle问题

安装安成后,在python的console中已经可以import cx_Oracle了,但在PyCharm中却提示找不到cx_Oracle。这是一个比较大的坑,可以详细讲一下处理过程。

首先初步定位到原因,是由于PyCharm中没有定义LD_LIBRARY_PATH与DYLD_LIBRARY_PATH两个环境变量造成的。
PyCharm会自动读取系统中的环境变量设置并导入,但唯独这两个没有导进来。不深究原因,先尝试手工在PyCharm中配置这两个环境变量,总共有两处可以配置:

第一处:
CMD+,打开Preference,找到 Build,Execution,Deployment -> Console -> Python Console ->Enviroment Variables

此处的配置会修复PyCharm中的Console。

第二处:
右上角 Run -> Edit Configurations,添加两条环境变量

此处的配置会修复PyCharm中Ctrl+Shift+R运行代码时的报错。

在以上两处手工添加环境变量:

1
2
DYLD_LIBRARY_PATH=/path/to/instantclient
LD_LIBRARY_PATH/path/to/instantclient

两处的环境变量配置完成后,虽然console中可以正常使用cx_Oracle了,代码也可以正常运行了,但编辑界面中的inspection还是有问题的,提示有Error,并且不能使用自动完成功能。

于是回到之前的问题,为什么明明已经定义了,但PyCharm却没有找到LD_LIBRARY_PATH与DYLD_LIBRARY_PATH?
同时偶然发现,在执行env命令查看已定义的环境变量时,也是找不到LD_LIBRARY_PATH与DYLD_LIBRARY_PATH的。
这貌似已经不是PyCharm自身的问题了,需要从macOS系统来着手了。

于是在stackoverflow上找到了这么一段话:

El Capitan added system integrity protection (SIP), and one side effect of that is that exporting DYLD_LIBRARY_PATH doesn’t work. That could affect running SQL*Plus from a shell script, for example. There are workarounds for the 11g instant client. The installation notes at the bottom of the download page have changed since I last did this, and it now says to hard link the library files to the user’s ~/lib directory to avoid that issue. Fortunately it looks like you don’t need to worry about that with the 12c client - they’ve fixed the way it’s built.

看来根本原因是OSX 10.11之后加入的这个SIP引发的了。

首先想到的最简单的方法,将所有的.dylib.h都拷贝到系统默认的目录就可以了。
但很杯具,/usr/lib/user/include这两个目录也被SIP保护了。因为不想强制关闭SIP,继续再想别的办法。
接着发现/usr/local/lib/user/local/include目录还是可以操作的,于是拷到这两个目录:

1
2
3
cd /path/to/instantclient
cp *.dylib /usr/local/lib
cp ./sdk/include/*.h /usr/local/include

再将cx_Oracle卸载后重新编译安装:

1
2
sudo pip3 uninstall cx_Oracle
sudo pip3 install cx_Oracle

重启PyCharm后,问题解决。

cx_Oracle的简单使用

cx_Oracle的使用上没有什么问题,一切按套路来写就可以了。贴一个简单的示例:

1
2
3
4
5
6
7
8
9
import cx_Oracle
conn = cx_Oracle.connect('username', 'password','host:port/sid')
cursor = conn.cursor()
cursor.execute('select column from table')
result = cursor.fetchall()
for row in result:
print(row)
cursor.close()
conn.close()

更加详细的内容请参考官方文档:
https://cx-oracle.readthedocs.io/en/latest/
https://oracle.github.io/python-cx_Oracle/

参考文档

https://gist.github.com/thom-nic/6011715
http://stackoverflow.com/questions/37711482/how-to-install-oracle-instant-client-on-a-mac

简介

RDS

阿里云关系型数据库(Relational Database Service,简称 RDS)是一种稳定可靠、可弹性伸缩的在线数据库服务。基于阿里云分布式文件系统和高性能存储,RDS 支持 MySQL、SQL Server、PostgreSQL 和 PPAS(Postgre Plus Advanced Server,一种高度兼容 Oracle 的数据库)引擎,并且提供了容灾、备份、恢复、监控、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。

RDS for MySQL 可以认为是云上的MySQL。目前可选的版本有5.5/5.6,部分区域可选择5.7。

DRDS

分布式关系型数据库服务(Distributed Relational Database Service,简称 DRDS)是阿里巴巴致力于解决单机数据库服务瓶颈问题而自主研发推出的分布式数据库产品。DRDS 高度兼容 MySQL 协议和语法,支持自动化水平拆分、在线平滑扩缩容、弹性扩展、透明读写分离,具备数据库全生命周期运维管控能力。DRDS 前身为淘宝 TDDL,是近千核心应用首选组件。

DRDS 可以认为是云上的TDDL中间件。DRDS必须依赖RDS。

相关文档

RDS首页

https://www.aliyun.com/product/rds/mysql

RDS控制台

https://rdsnew.console.aliyun.com

RDS文档

https://help.aliyun.com/product/26090.html

DRDS首页

https://www.aliyun.com/product/drds

DRDS控制台

https://drds.console.aliyun.com/prectrl/home/index

DRDS控制台不在左侧快捷菜单中,可以直接通过链接进入

DRDS文档

https://help.aliyun.com/product/29657.html

RDS初探

  1. 进入控制台,点击右侧的”创建实例”

    创建RDS实例需要几分钟的时间

  2. 按量付费,最低配置的RDS实例,目前为0.324元/小时

    这里选择5.6版本,猜测在工具的支持上BUG会少一些

  3. 在实例列表中可以看到已创建的实例

    点击进入详情页可以进行实例状态查看以及管理

  4. 创建数据库、创建帐号的操作比较简单,不再赘述。

  5. 添加IP白名单
    首先,在数据安全性中,将default分组中的127.0.0.1临时改为0.0.0.0/0,允许所有地址访问

    登录数据库:
    mysql -hrm-xxxxxxxxxxx.mysql.rds.aliyuncs.com -P3306 -utestuser -p
    查询当前IP:
    show processlist;
    在数据安全性中,尽快将default分组中的IP由0.0.0.0/0改回当前的真实的IP。

  6. 登录数据库
    接下来可以用命令行或第三方工具进行登录。RDS不提供root用户。

  7. 其它功能
    另外可以添加只读实例、灾备实例,按需购买即可,猜测原理应该是基于主从复制的。
    另外可以在WEB界面中进行数据迁移、导入等操作。
    WEB控制台中提供了比较完善的日志查询、慢SQL分析、监控与告警功能点,有时间可以慢慢探索。

DRDS初探

  1. 进入控制台,点击右侧的”创建实例”。

  2. 按量付费,共享实例的DRDS,目前价格为0.2元/小时。

    按量后付费的需要提前向帐户中充值

  3. 购买后创建过程非常快,马上就可以在概览中看到。

  4. 进入”详情”页面后,目前DRDS下还没有数据库,可以点击下方链接进行创建。

    请注意: 此步骤依赖于RDS,需要先进行RDS的购买。
    RDS购买时,请注意需要在同一个区域中,尽量不要跨区。

  5. 如果已经购买过RDS,并创建了一个实例,就在左侧的实例清单中就可以选择到了。

  6. 填写基本信息,创建类型这里选择”拆分”。设置好数据库名及密码。

    拆分:即分库分表,将数据按照拆分规则分拆到多个库表中,由 DRDS 代理 SQL 执行。拆分涉及到数据导入导出、SQL 功能/性能测试和改造,对应用功能和性能会有一定的影响。

    非拆分:将已有的 RDS 数据库交由 DRDS 进行代理访问,实现读写分离的功能。无需进行数据导入,无需修改程序代码,修改数据库连接串和用户名密码即可。

  7. 系统会自动在RDS实例上创建8个分库,直接点”下一步”创建成功。

    创建数据库需要几分钟的时间

  8. 创建完成后,可以在DRDS数库列表中看到

    在RDS控制台中也可以看到自动创建的8+1个库,绑定帐号名随机生成,密码与DRDS密码是不同的,估计也是随机生成,所以无法直接登录查看RDS库,只能在DRDS中统一管理。

  9. 点击”管理”,可以查看详情页面

  10. WEB界面登录
    详情页面,点击上方的”登录数据库”按钮,有一个WEB图形化控制台

    但点击登录后,会报错:Access denied for user ‘ xx’@’xx.xx.xx.xx’
    原因未知,有可能是IP限制。官方文档中也未给出示例。

  11. 命令行登录
    可以使用命令行登录
    mysql -hdrdsxxxxxxxxpublic.drds.aliyuncs.com -P3306 -uyourname -p
    版本显示为 5.6.29-TDDL-5.1.27-1217986

  12. 第三方客户端登录
    可以使用第三方客户端登录,如Sequel Pro,也可以正常登录。
    (之前用过的的Cobar/MyCat都是比较挑客户端的)

  13. 在DRDS执行一个简单的建表语句测试

    建一个普通表:

    1
    2
    3
    4
    5
    CREATE TABLE normal_table(
    id int,
    name varchar(30),
    primary key(id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    建一个分库表:

    1
    2
    3
    4
    5
    CREATE TABLE shard_table(
    id int,
    name varchar(30),
    primary key(id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 dbpartition by hash(id) tbpartition by hash(id) tbpartitions 3;

    在WEB控制台中可以查看表的基本信息:

简介

swagger我们已经不陌生了,另外可以借助于swagger-ui,可以实现动态的、可测试的API文档。

本文主要介绍如何使用swagger2markup生成静态的HTML及PDF文档。

生成的文档样式如下:

swagger2markup主页

GitHub主页:
https://github.com/Swagger2Markup/swagger2markup

实现方式

swagger2markup的实现方式比较绕,pom.xml里的依赖也比较多,而且每次构建都生成文档的话,需要耗费较长的时间。
所以不建议直接集成到工程内部,可以将它独立成一个工程出来,专门用于生成文档。

配置及代码

配置及代码不多,就不上传github了,在下面直接贴出来。

实现方式大体是:

  1. 运行单元测试采集远程的swagger.json文件并保存到本地
  2. maven插件将swagger.json转换为asciidoc
  3. 将asciidoc再转为html+pdf。

用到了custom.baseurl这个JVM自定义的参数。默认情况下采集127.0.0.1:8080下的应用。
也可以在mvn后面指定-Dcustom.baseurl=xx.xx.xx.xx来实现自定义的采集,如果微服务较多,可以用start.sh脚本来进行批量采集

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.foo.bar</groupId>
<artifactId>apidocs</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>

<name>apidocs</name>
<description>ApiDocs</description>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>

<custom.baseurl>127.0.0.1:8080</custom.baseurl>
<swagger2markup.version>1.3.1</swagger2markup.version>
<asciidoctor.input.directory>${project.basedir}/src/docs/asciidoc</asciidoctor.input.directory>
<swagger.output.dir>${project.build.directory}/swagger</swagger.output.dir>
<swagger.snippetOutput.dir>${project.build.directory}/asciidoc/snippets</swagger.snippetOutput.dir>
<generated.asciidoc.directory>${project.build.directory}/asciidoc/generated</generated.asciidoc.directory>
<asciidoctor.html.output.directory>${project.build.directory}/asciidoc/html</asciidoctor.html.output.directory>
<asciidoctor.pdf.output.directory>${project.build.directory}/asciidoc/pdf</asciidoctor.pdf.output.directory>
<swagger.input>${swagger.output.dir}/swagger.json</swagger.input>
</properties>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
</parent>

<dependencies>
<!--springboot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--swagger2markup-->
<dependency>
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup-spring-restdocs-ext</artifactId>
<version>${swagger2markup.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-staticdocs</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>io.github.robwin</groupId>
<artifactId>assertj-swagger</artifactId>
<version>0.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
</dependency>
</dependencies>

<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<compilerVersion>${java.version}</compilerVersion>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
<!-- prevents endPosTable exception for maven compile -->
<useIncrementalCompilation>false</useIncrementalCompilation>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<io.springfox.staticdocs.outputDir>${swagger.output.dir}</io.springfox.staticdocs.outputDir>
<io.springfox.staticdocs.snippetsOutputDir>${swagger.snippetOutput.dir}
</io.springfox.staticdocs.snippetsOutputDir>
<custom.baseurl>${custom.baseurl}</custom.baseurl>
</systemPropertyVariables>
</configuration>
</plugin>

<!-- First, use the swagger2markup plugin to generate asciidoc -->
<plugin>
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup-maven-plugin</artifactId>
<version>${swagger2markup.version}</version>
<dependencies>
<dependency>
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup-import-files-ext</artifactId>
<version>${swagger2markup.version}</version>
</dependency>
<dependency>
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup-spring-restdocs-ext</artifactId>
<version>${swagger2markup.version}</version>
</dependency>
</dependencies>
<configuration>
<swaggerInput>${swagger.input}</swaggerInput>
<outputDir>${generated.asciidoc.directory}</outputDir>
<config>
<swagger2markup.markupLanguage>ASCIIDOC</swagger2markup.markupLanguage>
<swagger2markup.pathsGroupedBy>TAGS</swagger2markup.pathsGroupedBy>

<swagger2markup.extensions.dynamicOverview.contentPath>
${project.basedir}/src/docs/asciidoc/extensions/overview
</swagger2markup.extensions.dynamicOverview.contentPath>
<swagger2markup.extensions.dynamicDefinitions.contentPath>
${project.basedir}/src/docs/asciidoc/extensions/definitions
</swagger2markup.extensions.dynamicDefinitions.contentPath>
<swagger2markup.extensions.dynamicPaths.contentPath>${project.basedir}/src/docs/asciidoc/extensions/paths
</swagger2markup.extensions.dynamicPaths.contentPath>
<swagger2markup.extensions.dynamicSecurity.contentPath>
${project.basedir}src/docs/asciidoc/extensions/security/
</swagger2markup.extensions.dynamicSecurity.contentPath>

<swagger2markup.extensions.springRestDocs.snippetBaseUri>${swagger.snippetOutput.dir}
</swagger2markup.extensions.springRestDocs.snippetBaseUri>
<swagger2markup.extensions.springRestDocs.defaultSnippets>true
</swagger2markup.extensions.springRestDocs.defaultSnippets>
</config>
</configuration>
<executions>
<execution>
<phase>test</phase>
<goals>
<goal>convertSwagger2markup</goal>
</goals>
</execution>
</executions>
</plugin>

<!-- Run the generated asciidoc through Asciidoctor to generate
other documentation types, such as PDFs or HTML5 -->
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>1.5.3</version>
<!-- Include Asciidoctor PDF for pdf generation -->
<dependencies>
<dependency>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctorj-pdf</artifactId>
<version>1.5.0-alpha.10.1</version>
</dependency>
<dependency>
<groupId>org.jruby</groupId>
<artifactId>jruby-complete</artifactId>
<version>1.7.21</version>
</dependency>
</dependencies>
<!-- Configure generic document generation settings -->
<configuration>
<sourceDirectory>${asciidoctor.input.directory}</sourceDirectory>
<sourceDocumentName>index.adoc</sourceDocumentName>
<attributes>
<doctype>book</doctype>
<toc>left</toc>
<toclevels>3</toclevels>
<numbered></numbered>
<hardbreaks></hardbreaks>
<sectlinks></sectlinks>
<sectanchors></sectanchors>
<generated>${generated.asciidoc.directory}</generated>
</attributes>
</configuration>
<!-- Since each execution can only handle one backend, run
separate executions for each desired output type -->
<executions>
<execution>
<id>output-html</id>
<phase>test</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html5</backend>
<outputDirectory>${asciidoctor.html.output.directory}</outputDirectory>
</configuration>
</execution>

<execution>
<id>output-pdf</id>
<phase>test</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>pdf</backend>
<outputDirectory>${asciidoctor.pdf.output.directory}</outputDirectory>
</configuration>
</execution>

</executions>
</plugin>

<!-- specify the main class for the manifest -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>io.github.robwin.swagger2markup.petstore.Application</mainClass>
</manifest>
</archive>
</configuration>
</plugin>

<!-- copy dependencies to the lib directory -->
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>

<!-- copy the generated documents -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>2.7</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.outputDirectory}/static/docs</outputDirectory>
<resources>
<resource>
<directory>${asciidoctor.html.output.directory}</directory>
</resource>
<resource>
<directory>${asciidoctor.pdf.output.directory}</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

JUnit测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.sitech.sdtools;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;

import java.io.BufferedWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;

@RunWith(SpringRunner.class)
@AutoConfigureRestDocs(outputDir = "build/asciidoc/snippets")
public class Swagger2MarkupTest {
private RestTemplate restTemplate = new RestTemplate();

@Test
public void createSpringfoxSwaggerJson() throws Exception {
String baseurl = System.getProperty("custom.baseurl");
String swaggerUrl = "http://" + baseurl + "/v2/api-docs";
String outputDir = System.getProperty("io.springfox.staticdocs.outputDir");
System.out.println("swaggerUrl: " + swaggerUrl);
System.out.println("outputDir: " + outputDir);

String swaggerJson = restTemplate.getForObject(swaggerUrl, String.class);

Files.createDirectories(Paths.get(outputDir));
try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(outputDir, "swagger.json"), StandardCharsets.UTF_8)) {
writer.write(swaggerJson);
} catch (Exception e) {
e.printStackTrace();
}
}
}

批量shell脚本

start.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env bash

# 自定义工程名及URL
prjnames=("project1" "project2" "project3")
prjurls=("xx.xx.xx.xx:7477" "xx.xx.xx.xx:23053" "xx.xx.xx.xx:24275")

# 默认参数
datestr=`date +%Y%m%d`
srcdir=./target/asciidoc/html
destdir=./src/docs/html/${datestr}
mkdir ${destdir}

# 循环生成文档
for prjname in ${prjnames[@]}
do
prjurl=${prjurls[${i}]}
echo "文档生成开始 - ${prjname} - ${prjurl}"

mvn clean test -Dcustom.baseurl=${prjurl}
cp ${srcdir}/index.html ${destdir}/${prjname}.html

i=$((i+1))
done

单文档生成步骤

  1. 先本机将需要生成API文档的工程启动,IP及端口指定为127.0.0.1:8080
  2. 在本工程下运行mvn clean test
  3. 在target/asciidoc/html中查看API文档并拷贝出来

批量文档生成步骤

  1. 在DCOS中将所有服务启动
  2. 修改start.sh中的prjurls
  3. 执行./start.sh
  4. src/docs/html/日期/中查看生成的API文档

简介

本文主要介绍如何使用Postman+Newman+Jenkins,实现微服务自动化的回归测试。

所需工具

  • Postman:用于编写测试用例,导出.json格式的测试脚本
  • Newman:命令行方式执行postman导出的测试脚本
  • Jenkins:实现测试的自动化

Postman的下载

官网:
https://www.getpostman.com/

Newman的安装

Newman可以运行Postman生成的.json测试用例,生成.html格式的测试报告。

可以直接使用npm进行安装:

1
npm install newman --global;

更多文档,可以参考GitHub:
https://github.com/postmanlabs/newman

测试过程

  1. 使用Postman编写微服务接口的测试用例,并导出json文件。
  2. Jenkins创建测试项目,使用newman来执行测试脚本,生成测试报告。
  3. 将测试项目与工程构建项目关联,使之在构建发布到测试环境后触发执行。

使用Postman编写测试用例

  1. 根据swagger服务契约,使用Postman工具,传入测试参数并模拟调用。

  2. 调用后,可以点击左侧的文件夹图标,新建一个Collection。

  3. 点击右侧的Save按钮,将本次测试脚本保存到刚才创建的Collection中。

  4. 继续进行其它API的调用,并保存至Collection中。

  5. 点击Collection上的省略号图标,并选择Export,将测试用例导出保存为.json文件。

  6. 上传到服务器相应目录下。

配置Jenkins测试项目

接下来配置Jenkins,用于触发调用newman进行测试。

  1. 创建一个自由风格的Jenkins项目
  2. 在构建一栏,输入以下脚本,用于触发newman测试
    1
    newman run /home/deployer/functest/foobar/foobar.json --reporters cli,html,json,junit --reporter-html-export /home/deployer/functest_report/foobar/foobar_report.html
  3. 可以在主项目的构建配置中,增加构建后操作,构建完成后自动触发执行这个测试项目

查看测试结果

  1. 可以在Jenkins的日志中直接查看结果
  2. 也可以用Web容器将生成的.html测试报告暴露出来,直接通过浏览器访问

自动化回归测试的必要性

Postman、Selenium这样的动化的回归测试真的有必要吗,它能够帮助我们发现多少问题,为了实现它我们要多付出多大的代价?
说到底,这是一个有关”性价比”的问题。

首先从组织结构上来看,如果项目的规模大到了有专门的测试团队,那么无疑会大大降低测试团队的工作量。
但如果是开发人员兼职做测试,那么多花点心思在controller层的集成测试上,可以会用更少的时间,达到差不多的效果。

有时间会再写一下关于实践中单元测试、集成测试的一些取舍,以及手工回归测试与swagger参数示例等方面的内容。

Feign的不恰当的fallback

Feign的坑不少,特别与Hystrix集成之后。
在微服务引入Feign后,上线不久后便发现,对于一个简单的查询类调用,在下游返回正常的”404-资源不存在”这种业务异常时,Feign也做了fallback,最终导致circuit break,引发平台告警。

REST接口的设计

为了解释这个问题,首先还是要从REST接口开始谈起。

REST的一个缺点(也有人认为是优势),它只是一种依赖于HTTP的”风格”,而没有明确的”规范”,所以客户端和服务端之间,要自行达成某种”约定”。
例如返回码,就要硬往HTTP STATUS上靠。

关于返回码,公认的”最佳实践”大概是这样的:

  1. 如果业务处理成功,http status返回200、204等。如果有内容,BODY中直接返回内容(对象或数组都可以),不再有RPC时代的code/message这样的状态描述。如果没有内容,BODY直接空白,No News Is Good News。
  2. 如果业务处理失败,业务逻辑导致的,则http status返回4XX,BODY中返回报错信息,报错信息的统一格式大概是这样的:
    1
    2
    3
    4
    5
    {
    "status": 409, // 冗余字段,把http status再重复一遍
    "code": 888, // 自定义的错误码
    "message": "foobar" // 错误描述
    }
  3. 如果是其它未知错误,抛5XX,认为是服务器内部错误,而不是逻辑错误。

所以对于APP/WEB等客户端来说,很简单,如果发现2XX,则认为成功,直接获取数据。如果非2XX,则是失败,直接取code和message,展现到前台。

但是对于微服务之间的调用,就要区分是”4XX-业务逻辑异常”,还是”5XX-服务器异常”了。。。

REST返回码的选择

下面详细讲一下HTTP STATUS的选择问题。

关于HTTP返回码,看了很多参考(论战),”大概”可以这样选择:

成功: 2XX系列

  • 200 OK // 查询、删除成功用这个
  • 201 CREATED // 新增、修改时用这个。且返回BODY中无任何信息。

业务异常: 4XX系列

  • 400 BAD_REQUEST // 现在有很多人在业务异常时抛这个错。但400要慎重使用。稍后解释。
  • 404 NOT_FOUND // 查询不到结果时用这个
  • 403 FORBIDDEN // 这个也慎重使用。
  • 409 CONFLICT // 业务异常时,可以用这个。

主机异常:5XX系列

  • 500 INTERNAL_SERVER_ERROR // 对于未知异常,统一用这个了
  • 503 SERVICE_UNAVAILABLE // Hystrix异常用这个

什么时候应该Fallback

2XX,成功,这个不用再讨论。
5XX,也相当明确,直接Fallback,这个也不用讨论。
4XX,可以一律认为是业务逻辑异常(或者更精确的说,可以认为4XX中的某几个是业务异常)。这时候,应该是用if/else来处理这个异常,而不应该动用Hystrix来Fallback。

Feign在默认情况下,对于非2XX,都认为是异常。这个地方是有问题的。特别是对于404这种非常容易抛出的业务异常来说,没两下就circuit break了。

Feign的Issue里已经有人提过这个问题,后面的版本中已经提供了一个参数:decode404

可以看一下Feign的代码,位置在:
~/.m2/repository/io/github/openfeign/feign-core/9.5.0/feign-core-9.5.0.jar!/feign/SynchronousMethodHandler.class

所以在Client上可以这样设置:

1
2
3
4
5
@FeignClient(name = "marathon-lb", fallback = FooBarClientFallback.class, decode404 = true)
@RequestMapping(value = "/foo/bar")
public interface FooBarClient {
... ...
}

只需要加入decode404 = true这一个参数,Feign对于2XX和404 ,都不会走Fallback了。
排除404,已经基本上够用了,如果想把409、400等status也加到例外中,可以重写一下Feign的errorDecoder。

关于4XX错误

刚才提到的,如果把2XX,另外加上4XX,全部认为是正常业务逻辑,都不走Fallback,可不可行? 我想最好不要这样很笼统的设置,要看情况。

因为http status不全是服务端给出的,如果服务端与消费者之间隔着一些Nginx、HA、Kong这样的网关,那么情况可能就复杂了,网关也有可能抛出status。

例如当某个微服务宕机之后,Kong网关会直接返回400,这种情况下,很明显是应当Fallback的。

所以,在定义错误码时,要尽量避开400、403这种很溃常见的码,像409这样小众的,差不多可以放心使用。
这样,调用方就可以有针对性的对某几个4XX的status进行单独配置,配置为业务异常。