Alenx 6 years ago
parent
commit
7d4b00b574
72 changed files with 442 additions and 306 deletions
  1. 3 0
      .vscode/settings.json
  2. 19 13
      README.md
  3. 1 1
      admin.sh
  4. 1 1
      fe/index.html
  5. 1 0
      fe/static/css/app.8eef0cb2e9918c4f5edd93ea521bf974.css
  6. BIN
      fe/static/css/app.8eef0cb2e9918c4f5edd93ea521bf974.css.gz
  7. 1 0
      fe/static/js/0.38397690f86c1c129eeb.js
  8. 10 0
      fe/static/js/1.9e1ed1e2e98043e9f4d0.js
  9. BIN
      fe/static/js/1.9e1ed1e2e98043e9f4d0.js.gz
  10. 1 0
      fe/static/js/12.d87dec769d2234a9b8a2.js
  11. 10 0
      fe/static/js/2.0d6643b81fecd1450667.js
  12. BIN
      fe/static/js/2.0d6643b81fecd1450667.js.gz
  13. 1 0
      fe/static/js/3.653d01e6009dfe8108cf.js
  14. BIN
      fe/static/js/3.653d01e6009dfe8108cf.js.gz
  15. 1 0
      fe/static/js/4.9b1856cc3384e2afb25d.js
  16. BIN
      fe/static/js/4.9b1856cc3384e2afb25d.js.gz
  17. 1 0
      fe/static/js/app.d2c23f3f0782c7d5ef58.js
  18. BIN
      fe/static/js/app.d2c23f3f0782c7d5ef58.js.gz
  19. 1 0
      fe/static/js/manifest.8749161ee0b6705c46ed.js
  20. 39 0
      fe/static/js/vendor.bd4d6b7eef4b354ff33d.js
  21. BIN
      fe/static/js/vendor.bd4d6b7eef4b354ff33d.js.gz
  22. 1 1
      gateway/nginx/default.conf
  23. 0 0
      migrations/versions/00adfdca30bf_03_server.py
  24. 23 0
      migrations/versions/0af33c7b8832_06_task_rollback.py
  25. 0 0
      migrations/versions/2bca06a823a0_01_init_walle_database.py
  26. 0 0
      migrations/versions/52a2df18b1d4_02_add_index.py
  27. 0 0
      migrations/versions/91c4d13540c3_05_task_username.py
  28. 0 0
      migrations/versions/9532a372b5aa_04_preject_remove_server.py
  29. 2 2
      tests/test_01_api_environment.py
  30. 1 1
      tests/test_02_api_role.py
  31. 2 2
      tests/test_03_api_user.py
  32. 1 1
      tests/test_04_api_passport.py
  33. 2 2
      tests/test_05_api_space.py
  34. 2 2
      tests/test_06_api_server.py
  35. 2 2
      tests/test_07_api_project.py
  36. 2 2
      tests/test_08_api_task.py
  37. 9 0
      tests/utils.py
  38. 6 7
      walle/api/api.py
  39. 2 3
      walle/api/environment.py
  40. 1 1
      walle/api/group.py
  41. 1 1
      walle/api/passport.py
  42. 3 5
      walle/api/project.py
  43. 2 2
      walle/api/server.py
  44. 2 2
      walle/api/space.py
  45. 15 6
      walle/api/task.py
  46. 4 6
      walle/api/user.py
  47. 6 6
      walle/form/environment.py
  48. 8 10
      walle/form/group.py
  49. 27 27
      walle/form/project.py
  50. 3 4
      walle/form/role.py
  51. 7 9
      walle/form/server.py
  52. 7 11
      walle/form/space.py
  53. 4 5
      walle/form/tag.py
  54. 11 13
      walle/form/task.py
  55. 16 18
      walle/form/user.py
  56. 3 3
      walle/model/database.py
  57. 3 6
      walle/model/environment.py
  58. 0 5
      walle/model/member.py
  59. 1 4
      walle/model/project.py
  60. 6 0
      walle/model/record.py
  61. 0 7
      walle/model/space.py
  62. 34 9
      walle/model/task.py
  63. 3 9
      walle/model/user.py
  64. 4 0
      walle/service/code.py
  65. 110 52
      walle/service/deployer.py
  66. 1 1
      walle/service/notice/dingding.py
  67. 1 1
      walle/service/notice/email.py
  68. 0 37
      walle/service/notice/snotice.py
  69. 0 1
      walle/service/rbac/role.py
  70. 2 2
      walle/service/utils.py
  71. 0 1
      walle/service/waller.py
  72. 12 2
      walle/service/websocket.py

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "python.pythonPath": "/usr/local/opt/python@2/bin/python2.7"
+}

+ 19 - 13
README.md

@@ -6,9 +6,9 @@ Walle 2.0 - [官方主页](https://www.walle-web.io)
 
 walle 让用户代码发布终于可以不只能选择 jenkins!支持各种web代码发布,php、java、python、go等代码的发布、回滚可以通过web来一键完成。walle 一个可自由配置项目,更人性化,高颜值,支持git、多用户、多语言、多项目、多环境同时部署的开源上线部署系统。
 
-功能强大,且免费开源的`walle-web 瓦力`终于更新`2.0`了!占用了我几乎所有业余时间,精力与金钱付出换各位使用收益,望各位喜欢不吝顺手 `star` 以示支持,项目更好亦反馈予你。目前 `2.0` 预览版尚未达到完全企业可用状态,请保持关注,我会在公众号更新(在最下面)。  
+功能强大,且免费开源的`walle-web 瓦力`终于更新`2.0`了!占用了我几乎所有业余时间,精力与金钱付出换各位使用收益,望各位喜欢不吝顺手 `star` 以示支持,项目更好亦反馈予你。目前 `2.0.0` 即将发布,请保持关注,我会在公众号更新(在最下面)。  
 <br>
-老版本已迁移到 [walle 1.x](https://github.com/meolu/walle-web-v1.x) 的同学**务必不要再更新了**,两个版本不兼容,到时我会写个迁移助手,淡定。
+老版本已迁移到 [walle 1.x](https://github.com/meolu/walle-web-v1.x) 的同学**务必不要再更新了**,两个版本不兼容
 
 Feature
 =========================
@@ -19,10 +19,17 @@ Feature
 - 完善的通知机制。邮件、钉钉
 - 全新的UI,我自己都被震憾到了,如丝般流畅
 
+Architecture
+=========================
+![](https://raw.github.com/meolu/docs/master/walle-web.io/docs/2/zh-cn/static/walle-flow-relation.jpg)
+![](https://raw.github.com/meolu/docs/master/walle-web.io/docs/2/zh-cn/static/permission.png)
+
 Preview
 =========================
-![](https://raw.github.com/meolu/walle-web/master/screenshot/projects.png)
-![](https://raw.github.com/meolu/walle-web/master/screenshot/deploy.png)
+![](https://raw.github.com/meolu/docs/master/walle-web.io/docs/2/zh-cn/static/user-list.png)
+![](https://raw.github.com/meolu/docs/master/walle-web.io/docs/2/zh-cn/static/project-list.png)
+![](https://raw.github.com/meolu/docs/master/walle-web.io/docs/2/zh-cn/static/task-list.png)
+![](https://raw.github.com/meolu/docs/master/walle-web.io/docs/2/zh-cn/static/deploy-console.png)
 
 Installation
 =========================
@@ -34,21 +41,20 @@ Roadmap
     - ~~安装文档、前后端代码、Data Migration~~
 - [x] **Alpha** 2018-12-09
     - ~~使用文档、Trouble Shooting、公众号更新~~
-- **Beta** 2018-12-23 :santa:圣诞夜前夕
-    - `1.x`迁移到`2.0`脚本
+- [x] **Beta** 2018-12-23 :santa:圣诞夜前夕
     - ~~钉钉/邮件消息通知~~
-    - 接受官网logo企业的`Trouble Shooting`
+    - ~~接受官网logo企业的`Trouble Shooting`~~
 - **2.0.0**  2018-12-30 :one:元旦前夕
+    - ~~项目检测、复制~~
+    - ~~任务的回滚~~
     - `released tag`、使用文档
     - `Docker` 镜像
-    - 项目配置添加自定义变量
-    - ~~`github` 5000 `star`~~
-- **2.0.1**  2019-01-13
-    - ~~项目检测、复制~~
-    - 任务的回滚
     - Java配置模板
     - PHP配置模板
-    - Python 3.7+兼容
+    - ~~`github` 5000 `star`~~
+- **2.0.1**  2019-01-13
+    - 项目配置添加自定义变量
+    - ~~Python 3.7+兼容~~
 - **2.0.2**  2019-01-20
     - `Dashboard`(全新的玩法,欢迎提issue)
     - 在线PPT介绍

+ 1 - 1
admin.sh

@@ -20,7 +20,7 @@ function init() {
         virtualenv --no-site-packages venv # 注意:安装失败请指定python路径. mac 可能会有用anaconda的python. 请不要mac试用, 麻烦多多
     fi
     echo "安装/更新可能缺少的依赖: mysql-community-devel gcc gcc-c++ python-devel"
-    sudo yum install -y mysql-community-devel gcc gcc-c++ python-devel
+    sudo yum install -y mysql-community-devel gcc gcc-c++ python-devel git
     source ./venv/bin/activate
     pip install -r ./requirements/prod.txt
     if [ $? != "0"  ]; then

File diff suppressed because it is too large
+ 1 - 1
fe/index.html


File diff suppressed because it is too large
+ 1 - 0
fe/static/css/app.8eef0cb2e9918c4f5edd93ea521bf974.css


BIN
fe/static/css/app.8eef0cb2e9918c4f5edd93ea521bf974.css.gz


File diff suppressed because it is too large
+ 1 - 0
fe/static/js/0.38397690f86c1c129eeb.js


File diff suppressed because it is too large
+ 10 - 0
fe/static/js/1.9e1ed1e2e98043e9f4d0.js


BIN
fe/static/js/1.9e1ed1e2e98043e9f4d0.js.gz


File diff suppressed because it is too large
+ 1 - 0
fe/static/js/12.d87dec769d2234a9b8a2.js


File diff suppressed because it is too large
+ 10 - 0
fe/static/js/2.0d6643b81fecd1450667.js


BIN
fe/static/js/2.0d6643b81fecd1450667.js.gz


File diff suppressed because it is too large
+ 1 - 0
fe/static/js/3.653d01e6009dfe8108cf.js


BIN
fe/static/js/3.653d01e6009dfe8108cf.js.gz


File diff suppressed because it is too large
+ 1 - 0
fe/static/js/4.9b1856cc3384e2afb25d.js


BIN
fe/static/js/4.9b1856cc3384e2afb25d.js.gz


File diff suppressed because it is too large
+ 1 - 0
fe/static/js/app.d2c23f3f0782c7d5ef58.js


BIN
fe/static/js/app.d2c23f3f0782c7d5ef58.js.gz


File diff suppressed because it is too large
+ 1 - 0
fe/static/js/manifest.8749161ee0b6705c46ed.js


File diff suppressed because it is too large
+ 39 - 0
fe/static/js/vendor.bd4d6b7eef4b354ff33d.js


BIN
fe/static/js/vendor.bd4d6b7eef4b354ff33d.js.gz


+ 1 - 1
gateway/nginx/default.conf

@@ -6,7 +6,7 @@ server {
     listen       80;
 
     location / {
-        root /opt/walle-web/; # 前端代码
+        root /opt/walle-web/fe; # 前端代码
         try_files $uri $uri/ /index.html;
         add_header access-control-allow-origin *;
     }

migrations/versions/00adfdca30bf_server.py → migrations/versions/00adfdca30bf_03_server.py


+ 23 - 0
migrations/versions/0af33c7b8832_06_task_rollback.py

@@ -0,0 +1,23 @@
+"""06_task_rollback
+
+Revision ID: 0af33c7b8832
+Revises: 91c4d13540c3
+Create Date: 2018-12-31 17:04:39.514132
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+revision = '0af33c7b8832'
+down_revision = '91c4d13540c3'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.alter_column('tasks', 'enable_rollback', new_column_name='is_rollback', existing_type=sa.Integer())
+
+
+def downgrade():
+    op.alter_column('tasks', 'is_rollback', new_column_name='enable_rollback', existing_type=sa.Integer())

migrations/versions/2bca06a823a0_init_walle_database.py → migrations/versions/2bca06a823a0_01_init_walle_database.py


migrations/versions/52a2df18b1d4_add_index.py → migrations/versions/52a2df18b1d4_02_add_index.py


migrations/versions/91c4d13540c3_task_username.py → migrations/versions/91c4d13540c3_05_task_username.py


migrations/versions/9532a372b5aa_preject_remove_server.py → migrations/versions/9532a372b5aa_04_preject_remove_server.py


+ 2 - 2
tests/test_01_api_environment.py

@@ -79,7 +79,7 @@ class TestApiEnv(TestApiBase):
         response = {
             'count': 2,
         }
-        resp = client.get('%s/?%s' % (self.uri_prefix, urllib.urlencode(query)))
+        resp = client.get('%s/?%s' % (self.uri_prefix, urlencode(query)))
         response_success(resp)
         resp_dict = resp_json(resp)
 
@@ -96,7 +96,7 @@ class TestApiEnv(TestApiBase):
         response = {
             'count': 1,
         }
-        resp = client.get('%s/?%s' % (self.uri_prefix, urllib.urlencode(query)))
+        resp = client.get('%s/?%s' % (self.uri_prefix, urlencode(query)))
         response_success(resp)
         resp_dict = resp_json(resp)
 

+ 1 - 1
tests/test_02_api_role.py

@@ -72,7 +72,7 @@ class TestApiRole(TestApiBase):
         #     response = {
         #         'count': 1,
         #     }
-        #     resp = client.get('%s/?%s' % (self.uri_prefix, urllib.urlencode(query)))
+        #     resp = client.get('%s/?%s' % (self.uri_prefix, urlencode(query)))
         #     response_success(resp)
         #     resp_dict = resp_json(resp)
         #

+ 2 - 2
tests/test_03_api_user.py

@@ -106,7 +106,7 @@ class TestApiUser:
         response = {
             'count': 7,
         }
-        resp = client.get('%s/?%s' % (self.uri_prefix, urllib.urlencode(query)))
+        resp = client.get('%s/?%s' % (self.uri_prefix, urlencode(query)))
         response_success(resp)
         resp_dict = resp_json(resp)
 
@@ -123,7 +123,7 @@ class TestApiUser:
         response = {
             'count': 1,
         }
-        resp = client.get('%s/?%s' % (self.uri_prefix, urllib.urlencode(query)))
+        resp = client.get('%s/?%s' % (self.uri_prefix, urlencode(query)))
         response_success(resp)
         resp_dict = resp_json(resp)
 

+ 1 - 1
tests/test_04_api_passport.py

@@ -32,7 +32,7 @@ class TestApiPassport:
         response = {
             'count': 1,
         }
-        resp = client.get('/api/user/?%s' % (urllib.urlencode(query)))
+        resp = client.get('/api/user/?%s' % (urlencode(query)))
         response_success(resp)
         compare_req_resp(response, resp)
 

+ 2 - 2
tests/test_05_api_space.py

@@ -100,7 +100,7 @@ class TestApiSpace(TestApiBase):
         #     response = {
         #         'count': 2,
         #     }
-        #     resp = client.get('%s/?%s' % (self.uri_prefix, urllib.urlencode(query)))
+        #     resp = client.get('%s/?%s' % (self.uri_prefix, urlencode(query)))
         #     response_success(resp)
         #     resp_dict = resp_json(resp)
         #
@@ -124,7 +124,7 @@ class TestApiSpace(TestApiBase):
         #     response = {
         #         'count': 1,
         #     }
-        #     resp = client.get('%s/?%s' % (self.uri_prefix, urllib.urlencode(query)))
+        #     resp = client.get('%s/?%s' % (self.uri_prefix, urlencode(query)))
         #     response_success(resp)
         #     resp_dict = resp_json(resp)
         #     space_data_2 = self.get_list_ids(self.space_data_2)

+ 2 - 2
tests/test_06_api_server.py

@@ -79,7 +79,7 @@ class TestApiServer(TestApiBase):
         response = {
             'count': 2,
         }
-        resp = client.get('%s/?%s' % (self.uri_prefix, urllib.urlencode(query)))
+        resp = client.get('%s/?%s' % (self.uri_prefix, urlencode(query)))
         response_success(resp)
         resp_dict = resp_json(resp)
 
@@ -96,7 +96,7 @@ class TestApiServer(TestApiBase):
         response = {
             'count': 1,
         }
-        resp = client.get('%s/?%s' % (self.uri_prefix, urllib.urlencode(query)))
+        resp = client.get('%s/?%s' % (self.uri_prefix, urlencode(query)))
         response_success(resp)
         resp_dict = resp_json(resp)
 

+ 2 - 2
tests/test_07_api_project.py

@@ -154,7 +154,7 @@ class TestApiProject(TestApiBase):
         response = {
             'count': 2,
         }
-        resp = client.get('%s/?%s' % (self.uri_prefix, urllib.urlencode(query)))
+        resp = client.get('%s/?%s' % (self.uri_prefix, urlencode(query)))
         response_success(resp)
         resp_dict = resp_json(resp)
 
@@ -171,7 +171,7 @@ class TestApiProject(TestApiBase):
         response = {
             'count': 1,
         }
-        resp = client.get('%s/?%s' % (self.uri_prefix, urllib.urlencode(query)))
+        resp = client.get('%s/?%s' % (self.uri_prefix, urlencode(query)))
         response_success(resp)
         resp_dict = resp_json(resp)
 

+ 2 - 2
tests/test_08_api_task.py

@@ -95,7 +95,7 @@ class TestApiTask(TestApiBase):
         response = {
             'count': 2,
         }
-        resp = client.get('%s/?%s' % (self.uri_prefix, urllib.urlencode(query)))
+        resp = client.get('%s/?%s' % (self.uri_prefix, urlencode(query)))
         response_success(resp)
         resp_dict = resp_json(resp)
 
@@ -112,7 +112,7 @@ class TestApiTask(TestApiBase):
         response = {
             'count': 1,
         }
-        resp = client.get('%s/?%s' % (self.uri_prefix, urllib.urlencode(query)))
+        resp = client.get('%s/?%s' % (self.uri_prefix, urlencode(query)))
         response_success(resp)
         resp_dict = resp_json(resp)
 

+ 9 - 0
tests/utils.py

@@ -7,8 +7,17 @@
     :author: wushuiyong@walle-web.io
 """
 import json
+import sys
 from flask import current_app
 
+PY2 = int(sys.version[0]) == 2
+
+if PY2:
+    from urllib import urlencode
+else:
+    from urllib.parse import urlencode
+
+
 def response_success(response):
     assert 200 <= response.status_code < 300, 'Received %d response: %s' % (response.status_code, response.data)
     resp = resp_json(response)

+ 6 - 7
walle/api/api.py

@@ -23,7 +23,6 @@ class ApiResource(Resource):
     actions = None
     action = None
 
-    # TODO 权限验证
     def __init__(self):
         pass
 
@@ -87,7 +86,7 @@ class SecurityResource(ApiResource):
         self.action = 'delete'
         is_allow = AccessRbac.is_allow(action=self.action, controller=self.controller)
         if not is_allow:
-            self.render_json(code=403, message=u'无操作权限')
+            self.render_json(code=403, message='无操作权限')
             # abort(403)
             pass
         pass
@@ -97,7 +96,7 @@ class SecurityResource(ApiResource):
         self.action = 'put'
         is_allow = AccessRbac.is_allow(action=self.action, controller=self.controller)
         if not is_allow:
-            self.render_json(code=403, message=u'无操作权限')
+            self.render_json(code=403, message='无操作权限')
             # abort(403)
             pass
         pass
@@ -115,10 +114,10 @@ class SecurityResource(ApiResource):
 
     def validator(self):
         if not AccessRbac.is_login():
-            return self.render_json(code=1000, message=u'请先登录')
+            return self.render_json(code=1000, message='请先登录')
 
         if not AccessRbac.is_allow(action=self.action, controller=self.controller):
-            return self.render_json(code=1001, message=u'无操作权限')
+            return self.render_json(code=1001, message='无操作权限')
 
 
     @staticmethod
@@ -126,7 +125,7 @@ class SecurityResource(ApiResource):
         @wraps(func)
         def is_enable(*args, **kwargs):
             if current_user.role_info.name != 'super':
-                return ApiResource.render_json(code=403, message=u'无操作权限')
+                return ApiResource.render_json(code=403, message='无操作权限')
             current_app.logger.info("user is login: %s" % (current_user.is_authenticated))
             current_app.logger.info("args: %s kwargs: %s" % (args, kwargs))
             return func(*args, **kwargs)
@@ -138,7 +137,7 @@ class SecurityResource(ApiResource):
         @wraps(func)
         def is_enable(*args, **kwargs):
             if current_user.role_info.name not in ['super', 'master']:
-                return ApiResource.render_json(code=403, message=u'无操作权限')
+                return ApiResource.render_json(code=403, message='无操作权限')
             current_app.logger.info("user is login: %s" % (current_user.is_authenticated))
             current_app.logger.info("args: %s kwargs: %s" % (args, kwargs))
             return func(*args, **kwargs)

+ 2 - 3
walle/api/environment.py

@@ -79,10 +79,9 @@ class EnvironmentAPI(SecurityResource):
         """
         super(EnvironmentAPI, self).post()
 
-        form = EnvironmentForm(request.form, csrf_enabled=False)
+        form = EnvironmentForm(request.form, csrf=False)
         if form.validate_on_submit():
             env_new = EnvironmentModel()
-            # TODO space_id
             env_id = env_new.add(env_name=form.env_name.data, space_id=self.space_id)
             if not env_id:
                 return self.render_json(code=-1)
@@ -100,7 +99,7 @@ class EnvironmentAPI(SecurityResource):
         """
         super(EnvironmentAPI, self).put()
 
-        form = EnvironmentForm(request.form, csrf_enabled=False)
+        form = EnvironmentForm(request.form, csrf=False)
         form.set_env_id(env_id)
         if form.validate_on_submit():
             env = EnvironmentModel(id=env_id)

+ 1 - 1
walle/api/group.py

@@ -84,7 +84,7 @@ class GroupAPI(SecurityResource):
         """
         super(GroupAPI, self).put()
 
-        form = GroupForm(request.form, csrf_enabled=False)
+        form = GroupForm(request.form, csrf=False)
         form.set_group_id(group_id)
         if form.validate_on_submit():
             # pass

+ 1 - 1
walle/api/passport.py

@@ -41,7 +41,7 @@ class PassportAPI(ApiResource):
 
         :return:
         """
-        form = LoginForm(request.form, csrf_enabled=False)
+        form = LoginForm(request.form, csrf=False)
         if form.validate_on_submit():
             user = UserModel.query.filter_by(email=form.email.data).first()
 

+ 3 - 5
walle/api/project.py

@@ -91,7 +91,7 @@ class ProjectAPI(SecurityResource):
             abort(404)
 
     def create(self):
-        form = ProjectForm(request.form, csrf_enabled=False)
+        form = ProjectForm(request.form, csrf=False)
         if form.validate_on_submit():
             # add project
             project = ProjectModel()
@@ -115,9 +115,9 @@ class ProjectAPI(SecurityResource):
         super(ProjectAPI, self).put()
 
         if action and action == 'members':
-            return self.members(project_id, members=json.loads(request.data))
+            return self.members(project_id, members=json.loads(request.data.decode('utf-8')))
 
-        form = ProjectForm(request.form, csrf_enabled=False)
+        form = ProjectForm(request.form, csrf=False)
         form.set_id(project_id)
         if form.validate_on_submit():
             server = ProjectModel().get_by_id(project_id)
@@ -150,8 +150,6 @@ class ProjectAPI(SecurityResource):
         :param members:
         :return:
         """
-        # TODO login for group id
-
         group_model = MemberModel(project_id=project_id)
         ret = group_model.update_project(project_id=project_id, members=members)
 

+ 2 - 2
walle/api/server.py

@@ -69,7 +69,7 @@ class ServerAPI(SecurityResource):
         """
         super(ServerAPI, self).post()
 
-        form = ServerForm(request.form, csrf_enabled=False)
+        form = ServerForm(request.form, csrf=False)
         if form.validate_on_submit():
             server_new = ServerModel()
             data = form.form2dict()
@@ -91,7 +91,7 @@ class ServerAPI(SecurityResource):
         """
         super(ServerAPI, self).put()
 
-        form = ServerForm(request.form, csrf_enabled=False)
+        form = ServerForm(request.form, csrf=False)
         form.set_id(id)
         if form.validate_on_submit():
             server = ServerModel().get_by_id(id)

+ 2 - 2
walle/api/space.py

@@ -81,7 +81,7 @@ class SpaceAPI(SecurityResource):
         """
         super(SpaceAPI, self).post()
 
-        form = SpaceForm(request.form, csrf_enabled=False)
+        form = SpaceForm(request.form, csrf=False)
         # return self.render_json(code=-1, data = form.form2dict())
         if form.validate_on_submit():
             # create space
@@ -119,7 +119,7 @@ class SpaceAPI(SecurityResource):
 
     @permission.upper_master
     def update(self, space_id):
-        form = SpaceForm(request.form, csrf_enabled=False)
+        form = SpaceForm(request.form, csrf=False)
         form.set_id(space_id)
         if form.validate_on_submit():
             space = SpaceModel().get_by_id(space_id)

+ 15 - 6
walle/api/task.py

@@ -39,8 +39,7 @@ class TaskAPI(SecurityResource):
         size = int(request.args.get('size', 10))
         kw = request.values.get('kw', '')
 
-        task_model = TaskModel()
-        task_list, count = task_model.list(page=page, size=size, kw=kw, space_id=self.space_id)
+        task_list, count = TaskModel().list(page=page, size=size, kw=kw, space_id=self.space_id)
         return self.list_json(list=task_list, count=count, enable_create=permission.role_upper_reporter() and current_user.role != SUPER)
 
     def item(self, task_id):
@@ -64,7 +63,7 @@ class TaskAPI(SecurityResource):
         """
         super(TaskAPI, self).post()
 
-        form = TaskForm(request.form, csrf_enabled=False)
+        form = TaskForm(request.form, csrf=False)
         if form.validate_on_submit():
             task_new = TaskModel()
             data = form.form2dict()
@@ -94,7 +93,7 @@ class TaskAPI(SecurityResource):
             return self.update(task_id=task_id)
 
     def update(self, task_id):
-        form = TaskForm(request.form, csrf_enabled=False)
+        form = TaskForm(request.form, csrf=False)
         form.set_id(task_id)
         if form.validate_on_submit():
             task = TaskModel().get_by_id(task_id)
@@ -146,14 +145,24 @@ class TaskAPI(SecurityResource):
         """
 
         task = TaskModel.get_by_id(task_id).to_dict()
-        ex_task = TaskModel().query.filter_by(link_id=task['ex_link_id']).first().to_dict()
+        filters = {
+            TaskModel.link_id == task['ex_link_id'],
+            TaskModel.id < task_id,
+        }
+        ex_task = TaskModel().query.filter(*filters).first()
+
+        if not ex_task:
+            raise WalleError(code=Code.rollback_error)
 
         task['id'] = None
-        task['name'] = task['name'] + u' - 回滚此次上线'
+        task['name'] = task['name'] + ' - 回滚此次上线'
         task['link_id'] = task['ex_link_id']
         task['ex_link_id'] = ''
+        task['is_rollback'] = 1
+        task['status'] = TaskModel.task_default_status(task['project_id'])
 
         # rewrite commit/tag/branch
+        ex_task = ex_task.to_dict()
         task['commit_id'] = ex_task['commit_id']
         task['branch'] = ex_task['branch']
         task['tag'] = ex_task['tag']

+ 4 - 6
walle/api/user.py

@@ -89,13 +89,13 @@ class UserAPI(SecurityResource):
 
     @permission.upper_developer
     def create_user(self):
-        form = RegistrationForm(request.form, csrf_enabled=False)
+        form = RegistrationForm(request.form, csrf=False)
         if form.validate_on_submit():
             user_info = form.form2dict()
             # add user
             user = UserModel().add(user_info)
             # send an email
-            message = u"""Hi, %s
+            message = """Hi, %s
                     <br> <br>Welcome to walle, it cost a lot of time and lock to meet you, enjoy it : )
                     <br><br>name: %s<br>password: %s""" \
                               % (user.username, user.email, form.password.data)
@@ -121,7 +121,7 @@ class UserAPI(SecurityResource):
             else:
                 abort(404)
 
-        form = UserUpdateForm(request.form, csrf_enabled=False)
+        form = UserUpdateForm(request.form, csrf=False)
         if form.validate_on_submit():
             user = UserModel(id=user_id)
             user.update_name_pwd(username=form.username.data, password=form.password.data)
@@ -161,7 +161,7 @@ class UserAPI(SecurityResource):
             },
         }
         ret = []
-        for (key, value) in table.items():
+        for (key, value) in list(table.items()):
             value['key'] = key
             if key in filter:
                 value['value'] = filter[key]
@@ -171,8 +171,6 @@ class UserAPI(SecurityResource):
         return ret
 
     def avatar(self, user_id):
-        # TODO uid
-        # fname = current_user.id + '.jpg'
         random = generate_password_hash(str(user_id))
         fname = random[-10:] + '.jpg'
         current_app.logger.info(fname)

+ 6 - 6
walle/form/environment.py

@@ -10,15 +10,15 @@ try:
     from flask_wtf import FlaskForm  # Try Flask-WTF v0.13+
 except ImportError:
     from flask_wtf import Form as FlaskForm  # Fallback to Flask-WTF v0.12 or older
-from flask_wtf import Form
+from flask_login import current_user
 from walle.model.environment import EnvironmentModel
-from wtforms import TextField
+from wtforms import StringField
 from wtforms import validators, ValidationError
-from flask_login import current_user
 
-class EnvironmentForm(Form):
-    env_name = TextField('env_name', [validators.Length(min=1, max=100)])
-    status = TextField('status', [validators.Length(min=0, max=10)])
+
+class EnvironmentForm(FlaskForm):
+    env_name = StringField('env_name', [validators.Length(min=1, max=100)])
+    status = StringField('status', [validators.Length(min=0, max=10)])
     env_id = None
 
     def set_env_id(self, env_id):

+ 8 - 10
walle/form/group.py

@@ -10,19 +10,18 @@ try:
     from FlaskForm import FlaskForm  # Try Flask-WTF v0.13+
 except ImportError:
     from flask_wtf import Form as FlaskForm  # Fallback to Flask-WTF v0.12 or older
-from flask_wtf import Form
-from wtforms import TextField
-from wtforms import validators, ValidationError
-
-from walle.model.user import UserModel
-from walle.model.tag import TagModel
 import json
+
 from flask import current_app
+from walle.model.tag import TagModel
+from walle.model.user import UserModel
+from wtforms import StringField
+from wtforms import validators, ValidationError
 
 
-class GroupForm(Form):
-    group_name = TextField('group_name', [validators.Length(min=1, max=100)])
-    uid_roles = TextField('uid_roles', [validators.Length(min=1)])
+class GroupForm(FlaskForm):
+    group_name = StringField('group_name', [validators.Length(min=1, max=100)])
+    uid_roles = StringField('uid_roles', [validators.Length(min=1)])
     group_id = None
 
     def set_group_id(self, group_id):
@@ -32,7 +31,6 @@ class GroupForm(Form):
         current_app.logger.info(field.data)
         self.uid_roles = json.loads(field.data)
 
-
         user_ids = [uid_role['user_id'] for uid_role in self.uid_roles]
         roles = [uid_role['role'] for uid_role in self.uid_roles]
         # TODO validator roles

+ 27 - 27
walle/form/project.py

@@ -10,40 +10,39 @@ try:
     from flask_wtf import FlaskForm  # Try Flask-WTF v0.13+
 except ImportError:
     from flask_wtf import Form as FlaskForm  # Fallback to Flask-WTF v0.12 or older
-from flask_wtf import Form
-from wtforms import TextField
-from wtforms import validators, ValidationError
 from flask_login import current_user
 from walle.model.project import ProjectModel
 from walle.service.notice import Notice
+from wtforms import StringField
+from wtforms import validators, ValidationError
 
 
-class ProjectForm(Form):
-    name = TextField('name', [validators.Length(min=1, max=100)])
-    environment_id = TextField('environment_id', [validators.Length(min=1, max=10)])
-    space_id = TextField('space_id', [validators.Length(min=1, max=10)])
-    status = TextField('status', [])
-    excludes = TextField('excludes', [])
-    master = TextField('master', [])
-    server_ids = TextField('server_ids', [validators.Length(min=1)])
-    keep_version_num = TextField('keep_version_num', [])
+class ProjectForm(FlaskForm):
+    name = StringField('name', [validators.Length(min=1, max=100)])
+    environment_id = StringField('environment_id', [validators.Length(min=1, max=10)])
+    space_id = StringField('space_id', [validators.Length(min=1, max=10)])
+    status = StringField('status', [])
+    excludes = StringField('excludes', [])
+    master = StringField('master', [])
+    server_ids = StringField('server_ids', [validators.Length(min=1)])
+    keep_version_num = StringField('keep_version_num', [])
 
-    target_root = TextField('target_root', [validators.Length(min=1, max=200)])
-    target_releases = TextField('target_releases', [validators.Length(min=1, max=200)])
+    target_root = StringField('target_root', [validators.Length(min=1, max=200)])
+    target_releases = StringField('target_releases', [validators.Length(min=1, max=200)])
 
-    task_vars = TextField('task_vars', [])
-    prev_deploy = TextField('prev_deploy', [])
-    post_deploy = TextField('post_deploy', [])
-    prev_release = TextField('prev_release', [])
-    post_release = TextField('post_release', [])
+    task_vars = StringField('task_vars', [])
+    prev_deploy = StringField('prev_deploy', [])
+    post_deploy = StringField('post_deploy', [])
+    prev_release = StringField('prev_release', [])
+    post_release = StringField('post_release', [])
 
-    repo_url = TextField('repo_url', [validators.Length(min=1, max=200)])
-    repo_username = TextField('repo_username', [])
-    repo_password = TextField('repo_password', [])
-    repo_mode = TextField('repo_mode', [validators.Length(min=1, max=50)])
-    notice_type = TextField('notice_type', [])
-    notice_hook = TextField('notice_hook', [])
-    task_audit = TextField('task_audit', [])
+    repo_url = StringField('repo_url', [validators.Length(min=1, max=200)])
+    repo_username = StringField('repo_username', [])
+    repo_password = StringField('repo_password', [])
+    repo_mode = StringField('repo_mode', [validators.Length(min=1, max=50)])
+    notice_type = StringField('notice_type', [])
+    notice_hook = StringField('notice_hook', [])
+    task_audit = StringField('task_audit', [])
 
     id = None
 
@@ -88,7 +87,8 @@ class ProjectForm(Form):
             'repo_password': self.repo_password.data if self.repo_password.data else '',
             'repo_mode': self.repo_mode.data if self.repo_mode.data else '',
 
-            'notice_type': self.notice_type.data if self.notice_type.data in [Notice.by_email, Notice.by_dingding] else '',
+            'notice_type': self.notice_type.data if self.notice_type.data in [Notice.by_email,
+                                                                              Notice.by_dingding] else '',
             'notice_hook': self.notice_hook.data if self.notice_hook.data else '',
             'task_audit': self.task_audit.data if self.task_audit.data else 0,
         }

+ 3 - 4
walle/form/role.py

@@ -10,12 +10,11 @@ try:
     from flask_wtf import FlaskForm  # Try Flask-WTF v0.13+
 except ImportError:
     from flask_wtf import Form as FlaskForm  # Fallback to Flask-WTF v0.12 or older
-from flask_wtf import Form
 
-from wtforms import TextField
+from wtforms import StringField
 from wtforms import validators
 
 
-class RoleAdd(Form):
-    name = TextField('Email Address', [validators.Length(min=6, max=35), validators.InputRequired()])
+class RoleAdd(FlaskForm):
+    name = StringField('Email Address', [validators.Length(min=6, max=35), validators.InputRequired()])
     # password = SelectField('Password', [validators.Length(min=6, max=35))

+ 7 - 9
walle/form/server.py

@@ -10,18 +10,16 @@ try:
     from flask_wtf import FlaskForm  # Try Flask-WTF v0.13+
 except ImportError:
     from flask_wtf import Form as FlaskForm  # Fallback to Flask-WTF v0.12 or older
-from flask_wtf import Form
-from wtforms import TextField
-from wtforms import validators, ValidationError
-
 from walle.model.server import ServerModel
+from wtforms import StringField
+from wtforms import validators, ValidationError
 
 
-class ServerForm(Form):
-    name = TextField('name', [validators.Length(min=1, max=100)])
-    host = TextField('host', [validators.Length(min=1, max=100)])
-    user = TextField('user', [validators.Length(min=1, max=100)])
-    port = TextField('port', [validators.Length(min=1, max=100)])
+class ServerForm(FlaskForm):
+    name = StringField('name', [validators.Length(min=1, max=100)])
+    host = StringField('host', [validators.Length(min=1, max=100)])
+    user = StringField('user', [validators.Length(min=1, max=100)])
+    port = StringField('port', [validators.Length(min=1, max=100)])
     id = None
 
     def set_id(self, id):

+ 7 - 11
walle/form/space.py

@@ -10,17 +10,15 @@ try:
     from flask_wtf import FlaskForm  # Try Flask-WTF v0.13+
 except ImportError:
     from flask_wtf import Form as FlaskForm  # Fallback to Flask-WTF v0.12 or older
-from flask_wtf import Form
-from wtforms import TextField
-from wtforms import validators, ValidationError
-
 from walle.model.space import SpaceModel
+from wtforms import StringField
+from wtforms import validators, ValidationError
 
 
-class SpaceForm(Form):
-    name = TextField('name', [validators.Length(min=1, max=100)])
-    user_id = TextField('user_id', [validators.Length(min=1, max=100)])
-    status = TextField('status', [])
+class SpaceForm(FlaskForm):
+    name = StringField('name', [validators.Length(min=1, max=100)])
+    user_id = StringField('user_id', [validators.Length(min=1, max=100)])
+    status = StringField('status', [])
     id = None
 
     def set_id(self, id):
@@ -39,8 +37,6 @@ class SpaceForm(Form):
     def form2dict(self):
         return {
             'name': self.name.data if self.name.data else '',
-            # TODO g.uid
             'user_id': self.user_id.data if self.user_id.data else '',
-            # TODO default value
             'status': 1,
-        }
+        }

+ 4 - 5
walle/form/tag.py

@@ -10,12 +10,11 @@ try:
     from flask_wtf import FlaskForm  # Try Flask-WTF v0.13+
 except ImportError:
     from flask_wtf import Form as FlaskForm  # Fallback to Flask-WTF v0.12 or older
-from flask_wtf import Form
 
-from wtforms import TextField
+from wtforms import StringField
 from wtforms import validators
 
 
-class TagCreateForm(Form):
-    name = TextField('name', [validators.Length(min=1, max=10)])
-    label = TextField('label', [validators.Length(min=1, max=30)])
+class TagCreateForm(FlaskForm):
+    name = StringField('name', [validators.Length(min=1, max=10)])
+    label = StringField('label', [validators.Length(min=1, max=30)])

+ 11 - 13
walle/form/task.py

@@ -11,24 +11,23 @@ try:
 except ImportError:
     from flask_wtf import Form as FlaskForm  # Fallback to Flask-WTF v0.12 or older
 from flask_login import current_user
-from flask_wtf import Form
 from walle.model.project import ProjectModel
 from walle.model.task import TaskModel
-from wtforms import TextField, IntegerField
+from wtforms import IntegerField, StringField
 from wtforms import validators
 
 
-class TaskForm(Form):
-    name = TextField('name', [validators.Length(min=1)])
+class TaskForm(FlaskForm):
+    name = StringField('name', [validators.Length(min=1)])
     project_id = IntegerField('project_id', [validators.NumberRange(min=1)])
-    servers = TextField('servers', [validators.Length(min=1)])
-    commit_id = TextField('commit_id', [])
+    servers = StringField('servers', [validators.Length(min=1)])
+    commit_id = StringField('commit_id', [])
     status = IntegerField('status', [])
     # TODO 应该增加一个tag/branch其一必填
-    tag = TextField('tag', [])
-    branch = TextField('branch', [])
+    tag = StringField('tag', [])
+    branch = StringField('branch', [])
     file_transmission_mode = IntegerField('file_transmission_mode', [])
-    file_list = TextField('file_list', [])
+    file_list = StringField('file_list', [])
 
     id = None
 
@@ -37,10 +36,10 @@ class TaskForm(Form):
 
     def form2dict(self):
         project_info = ProjectModel(id=self.project_id.data).item()
-        task_status = TaskModel.status_new if project_info['task_audit'] == ProjectModel.task_audit_true else TaskModel.status_pass
+        task_status = TaskModel.status_new if project_info[
+                                                  'task_audit'] == ProjectModel.task_audit_true else TaskModel.status_pass
         return {
             'name': self.name.data if self.name.data else '',
-            # todo
             'user_id': current_user.id,
             'user_name': current_user.username,
             'project_id': self.project_id.data,
@@ -55,6 +54,5 @@ class TaskForm(Form):
             'branch': self.branch.data if self.branch.data else '',
             'file_transmission_mode': self.file_transmission_mode.data if self.file_transmission_mode.data else 0,
             'file_list': self.file_list.data if self.file_list.data else '',
-            'enable_rollback': 1,
-
+            'is_rollback': 0,
         }

+ 16 - 18
walle/form/user.py

@@ -10,28 +10,26 @@ try:
     from flask_wtf import FlaskForm  # Try Flask-WTF v0.13+
 except ImportError:
     from flask_wtf import Form as FlaskForm  # Fallback to Flask-WTF v0.12 or older
-from flask_wtf import Form
-from wtforms import PasswordField, TextField
+import re
+from walle.model.user import UserModel
+from werkzeug.security import generate_password_hash
+from wtforms import PasswordField, StringField
 from wtforms import validators, ValidationError
 from wtforms.validators import Regexp
 
-from walle.model.user import UserModel
-from flask import current_app
-import re
-from werkzeug.security import generate_password_hash
 
 class UserForm(FlaskForm):
-    email = TextField('email', [validators.email()])
+    email = StringField('email', [validators.email()])
     password = PasswordField('Password', [validators.Length(min=6, max=35),
                                           validators.Regexp(regex="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d]{6,}",
-                              message='密码至少6个字符,至少1个大写字母,1个小写字母,1个数字')])
+                                                            message='密码至少6个字符,至少1个大写字母,1个小写字母,1个数字')])
 
-    username = TextField('Username', [validators.Length(min=1, max=50)])
-    role = TextField('role', [])
+    username = StringField('Username', [validators.Length(min=1, max=50)])
+    role = StringField('role', [])
 
     def validate_email(self, field):
-            if UserModel.query.filter_by(email=field.data).first():
-                raise ValidationError('Email already register')
+        if UserModel.query.filter_by(email=field.data).first():
+            raise ValidationError('Email already register')
 
     def form2dict(self):
         return {
@@ -43,19 +41,19 @@ class UserForm(FlaskForm):
 
 
 class RegistrationForm(UserForm):
-
     pass
 
 
-class UserUpdateForm(Form):
+class UserUpdateForm(FlaskForm):
     password = PasswordField('Password', [])
-    username = TextField('username', [])
+    username = StringField('username', [])
+
     def validate_password(self, field):
         if field.data and not re.match("^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d]{6,}", field.data):
             raise ValidationError('密码至少6个字符,至少1个大写字母,1个小写字母,1个数字')
 
 
-class LoginForm(Form):
-    email = TextField('email', [validators.Length(min=6, max=35),
-                                        Regexp(r'^(.+)@(.+)\.(.+)', message='邮箱格式不正确')])
+class LoginForm(FlaskForm):
+    email = StringField('email', [validators.Length(min=6, max=35),
+                                  Regexp(r'^(.+)@(.+)\.(.+)', message='邮箱格式不正确')])
     password = PasswordField('Password', [validators.Length(min=6, max=35)])

+ 3 - 3
walle/model/database.py

@@ -70,8 +70,8 @@ def parse_operator(cls, filter_name_dict):
             return value
 
     binary_expression_list = []
-    for field, op_dict in filter_name_dict.items():
-        for op, op_val in op_dict.items():
+    for field, op_dict in list(filter_name_dict.items()):
+        for op, op_val in list(op_dict.items()):
             op_val = _change_type(cls, field, op_val)
             if op in OPERATOR_FUNC_DICT:
                 binary_expression_list.append(
@@ -99,7 +99,7 @@ class CRUDMixin(object):
 
     def update(self, commit=True, **kwargs):
         """Update specific fields of a record."""
-        for attr, value in kwargs.items():
+        for attr, value in list(kwargs.items()):
             setattr(self, attr, value)
         return commit and self.save() or self
 

+ 3 - 6
walle/model/environment.py

@@ -9,10 +9,9 @@
 from datetime import datetime
 
 from sqlalchemy import String, Integer, DateTime
+from walle import model
 from walle.model.database import db, Model
 from walle.service.extensions import permission
-from walle.service.rbac.role import *
-from walle import model
 
 
 # 环境级别
@@ -44,10 +43,10 @@ class EnvironmentModel(Model):
         if kw:
             query = query.filter(EnvironmentModel.name.like('%' + kw + '%'))
         if space_id:
-            query = query.filter(EnvironmentModel.space_id==space_id)
+            query = query.filter(EnvironmentModel.space_id == space_id)
 
         SpaceModel = model.space.SpaceModel
-        query = query.join(SpaceModel, SpaceModel.id==EnvironmentModel.space_id)
+        query = query.join(SpaceModel, SpaceModel.id == EnvironmentModel.space_id)
         query = query.add_columns(SpaceModel.name)
         count = query.count()
         data = query.order_by(EnvironmentModel.id.desc()).offset(int(size) * int(page)).limit(size).all()
@@ -70,7 +69,6 @@ class EnvironmentModel(Model):
         return data.to_json() if data else []
 
     def add(self, env_name, space_id):
-        # todo permission_ids need to be formated and checked
         env = EnvironmentModel(name=env_name, status=self.status_open, space_id=space_id)
 
         db.session.add(env)
@@ -82,7 +80,6 @@ class EnvironmentModel(Model):
         return env.id
 
     def update(self, env_name, status, env_id=None):
-        # todo permission_ids need to be formated and checked
         role = EnvironmentModel.query.filter_by(id=self.id).first()
         role.name = env_name
         role.status = status

+ 0 - 5
walle/model/member.py

@@ -39,8 +39,6 @@ class MemberModel(SurrogatePK, Model):
     updated_at = db.Column(DateTime, default=current_time, onupdate=current_time)
     group_name = None
 
-    # TODO group id全局化
-
     def spaces(self, user_id=None):
         """
         获取分页列表
@@ -79,9 +77,6 @@ class MemberModel(SurrogatePK, Model):
         if user_id:
             query = query.filter_by(user_id=user_id)
 
-        # if project_id:
-        #     query = query.filter_by(source_id=project_id)
-
         projects = query.all()
         current_app.logger.info(projects)
 

+ 1 - 4
walle/model/project.py

@@ -114,7 +114,7 @@ class ProjectModel(SurrogatePK, Model):
 
         ServerModel = model.server.ServerModel
         server_ids = project_info['server_ids']
-        project_info['servers_info'] = ServerModel.fetch_by_id(map(int, server_ids.split(',')))
+        project_info['servers_info'] = ServerModel.fetch_by_id(list(map(int, server_ids.split(','))))
         return project_info
 
     def add(self, *args, **kwargs):
@@ -127,9 +127,6 @@ class ProjectModel(SurrogatePK, Model):
         return project.to_json()
 
     def update(self, *args, **kwargs):
-        # todo permission_ids need to be formated and checked
-        # a new type to update a model
-
         update_data = dict(*args)
         return super(ProjectModel, self).update(**update_data)
 

+ 6 - 0
walle/model/record.py

@@ -17,6 +17,12 @@ class RecordModel(Model):
     # 表的名字:
     __tablename__ = 'records'
     current_time = datetime.now()
+    #
+    stage_end = 'end'
+    #
+    status_success = 0
+    #
+    status_fail = 1
 
     # 表的结构:
     id = db.Column(Integer, primary_key=True, autoincrement=True)

+ 0 - 7
walle/model/space.py

@@ -72,8 +72,6 @@ class SpaceModel(SurrogatePK, Model):
         return data
 
     def add(self, *args, **kwargs):
-        # todo permission_ids need to be formated and checked
-
         data = dict(*args)
         space = SpaceModel(**data)
         db.session.add(space)
@@ -83,9 +81,6 @@ class SpaceModel(SurrogatePK, Model):
         return self.id
 
     def update(self, *args, **kwargs):
-        # todo permission_ids need to be formated and checked
-        # a new type to update a model
-
         update_data = dict(*args)
         return super(SpaceModel, self).update(**update_data)
 
@@ -107,8 +102,6 @@ class SpaceModel(SurrogatePK, Model):
             'id': self.id,
             'user_id': self.user_id,
             'user_name': uid2name[self.user_id] if uid2name and self.user_id in uid2name else '',
-            # TODO
-            'group_id': 'self.group_id',
             'name': self.name,
             'status': self.status,
             'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'),

+ 34 - 9
walle/model/task.py

@@ -35,6 +35,8 @@ class TaskModel(SurrogatePK, Model):
         status_success: '上线完成',
         status_fail: '上线失败',
     }
+    rollback_count = {}
+    keep_version_num = 3
 
     # 表的结构:
     id = db.Column(Integer, primary_key=True, autoincrement=True)
@@ -52,7 +54,7 @@ class TaskModel(SurrogatePK, Model):
     tag = db.Column(String(100))
     file_transmission_mode = db.Column(Integer)
     file_list = db.Column(Text)
-    enable_rollback = db.Column(Integer)
+    is_rollback = db.Column(Integer)
     created_at = db.Column(DateTime, default=current_time)
     updated_at = db.Column(DateTime, default=current_time, onupdate=current_time)
 
@@ -79,6 +81,7 @@ class TaskModel(SurrogatePK, Model):
         :param kw:
         :return:
         """
+        self.rollback_count.clear()
         query = TaskModel.query.filter(TaskModel.status.notin_([self.status_remove]))
         if kw:
             query = query.filter(TaskModel.name.like('%' + kw + '%'))
@@ -96,7 +99,7 @@ class TaskModel(SurrogatePK, Model):
         if space_id:
             query = query.filter(ProjectModel.space_id == space_id)
 
-        query = query.add_columns(ProjectModel.name, EnvironmentModel.name)
+        query = query.add_columns(ProjectModel.name, EnvironmentModel.name, ProjectModel.keep_version_num)
         count = query.count()
 
         data = query.order_by(TaskModel.id.desc()) \
@@ -104,9 +107,11 @@ class TaskModel(SurrogatePK, Model):
             .all()
         task_list = []
         for p in data:
+            p[0].keep_version_num = p[3]
             item = p[0].to_json()
             item['project_name'] = p[1]
             item['environment_name'] = p[2]
+            # self.keep_version_num = p[3]
             task_list.append(item)
 
         return task_list, count
@@ -125,12 +130,11 @@ class TaskModel(SurrogatePK, Model):
         task = data.to_json()
         ProjectModel = model.project.ProjectModel
         project = ProjectModel().item(task['project_id'])
-        task['project_name'] = project['name'] if project else u'未知项目'
+        task['project_name'] = project['name'] if project else '未知项目'
         task['project_info'] = project
         return task
 
     def add(self, *args, **kwargs):
-        # todo permission_ids need to be formated and checked
         data = dict(*args)
         project = TaskModel(**data)
 
@@ -143,9 +147,6 @@ class TaskModel(SurrogatePK, Model):
         return project.to_json()
 
     def update(self, *args, **kwargs):
-        # todo permission_ids need to be formated and checked
-        # a new type to update a model
-
         update_data = dict(*args)
         return super(TaskModel, self).update(**update_data)
 
@@ -182,7 +183,7 @@ class TaskModel(SurrogatePK, Model):
             'tag': self.tag,
             'file_transmission_mode': self.file_transmission_mode,
             'file_list': self.file_list,
-            'enable_rollback': self.enable_rollback,
+            'is_rollback': self.is_rollback,
             'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
             'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S'),
         }
@@ -191,6 +192,21 @@ class TaskModel(SurrogatePK, Model):
 
     def enable(self):
         is_project_master = self.project_id in session['project_master']
+
+        if self.project_id not in self.rollback_count:
+            self.rollback_count[self.project_id] = 0
+        if self.status in [self.status_doing, self.status_fail, self.status_success]:
+            self.rollback_count[self.project_id] += 1
+
+        current_app.logger.error(self.rollback_count[self.project_id])
+        current_app.logger.error(self.keep_version_num)
+        if self.rollback_count[self.project_id] <= self.keep_version_num \
+            and self.status in [self.status_doing, self.status_fail, self.status_success] \
+            and self.ex_link_id:
+            enable_rollback = True
+        else:
+            enable_rollback = False
+
         return {
             'enable_view': True if self.status in [self.status_doing, self.status_fail, self.status_success] else False,
             'enable_update': (permission.enable_uid(self.user_id) or permission.role_upper_developer() or is_project_master) and (self.status in [self.status_new, self.status_reject]),
@@ -198,5 +214,14 @@ class TaskModel(SurrogatePK, Model):
             'enable_create': False,
             'enable_online': (permission.enable_uid(self.user_id) or permission.role_upper_developer() or is_project_master) and (self.status in [self.status_pass, self.status_fail, self.status_doing]),
             'enable_audit': (permission.role_upper_developer() or is_project_master) and (self.status in [self.status_new]),
-            'enable_block': False,
+            'enable_rollback': enable_rollback
         }
+
+    @classmethod
+    def task_default_status(cls, project_id):
+        ProjectModel = model.project.ProjectModel
+        project_info = ProjectModel.query.filter_by(id=project_id).first()
+        if project_info.task_audit == ProjectModel.task_audit_true:
+            return TaskModel.status_new
+        else:
+            return TaskModel.status_pass

+ 3 - 9
walle/model/user.py

@@ -62,9 +62,6 @@ class UserModel(UserMixin, SurrogatePK, Model):
         return data.to_json() if data else []
 
     def update(self, *args, **kwargs):
-        # todo permission_ids need to be formated and checked
-        # a new type to update a model
-
         update_data = dict(*args)
         return super(UserModel, self).update(**update_data)
 
@@ -74,7 +71,6 @@ class UserModel(UserMixin, SurrogatePK, Model):
         current_app.logger.info(user)
 
     def update_name_pwd(self, username, password=None):
-        # todo permission_ids need to be formated and checked
         user = self.query.filter_by(id=self.id).first()
         if username:
             user.username = username
@@ -177,7 +173,7 @@ class UserModel(UserMixin, SurrogatePK, Model):
         if not spaces and current_user.role != SUPER:
             raise WalleError(Code.space_empty)
 
-        default_space = spaces.keys()[0]
+        default_space = list(spaces.keys())[0]
 
         # 2.第一次登录无空间
         if not current_user.last_space:
@@ -187,7 +183,7 @@ class UserModel(UserMixin, SurrogatePK, Model):
             session['space_info'] = spaces[session['space_id']]
 
         # 3.空间权限有修改(上次登录的空格没有权限了)
-        if current_user.last_space not in spaces.keys():
+        if current_user.last_space not in list(spaces.keys()):
             current_user.last_space = default_space
 
 
@@ -197,7 +193,7 @@ class UserModel(UserMixin, SurrogatePK, Model):
 
         session['space_id'] = current_user.last_space
         session['space_info'] = spaces[current_user.last_space]
-        session['space_list'] = spaces.values()
+        session['space_list'] = list(spaces.values())
 
     @classmethod
     def avatar_url(cls, avatar):
@@ -243,8 +239,6 @@ class UserModel(UserMixin, SurrogatePK, Model):
             'is_email_verified': self.is_email_verified,
             'email': self.email,
             'avatar': self.avatar_url(self.avatar),
-            # TODO 当前登录用户的空间
-            # 'role_id': self.role_id,
             'status': self.status_mapping[self.status],
             'last_space': self.last_space,
             # 'status': self.status,

+ 4 - 0
walle/service/code.py

@@ -41,6 +41,9 @@ class Code():
     #: 表单错误
     form_error = 2001
 
+    #: 不能生成回滚上线单,可能是第一上线,或目标机器上版本库里已失去该版本备份
+    rollback_error = 2002
+
     #: ----------------------- 3xxx shell 相关错误 -----------------
     #: 3xxx shell相关错误
     #: 不知道怎么归类的错误
@@ -80,6 +83,7 @@ class Code():
 
         params_error: '参数错误',
         form_error: '表单错误',
+        rollback_error: '不能生成回滚上线单,可能是第一上线,或目标机器上版本库里已失去该版本备份',
 
         shell_run_fail: '命令运行错误,请联系管理员',
         shell_dir_not_exists: '路径不存在,请联系管理员',

+ 110 - 52
walle/service/deployer.py

@@ -52,7 +52,7 @@ class Deployer:
     dir_release, dir_webroot = None, None
 
     connections, success, errors = {}, {}, {}
-    release_version_tar, release_version = None, None
+    release_version_tar, previous_release_version, release_version = None, None, None
     local = None
 
     def __init__(self, task_id=None, project_id=None, console=False):
@@ -82,9 +82,14 @@ class Deployer:
         # start to deploy
         self.console = console
 
-    def config(self):
-        return {'task_id': self.task_id, 'user_id': self.user_id, 'stage': self.stage, 'sequence': self.sequence,
-                'console': self.console}
+    def config(self, console=None):
+        return {
+            'task_id': self.task_id,
+            'user_id': self.user_id,
+            'stage': self.stage,
+            'sequence': self.sequence,
+            'console': console if console is not None else self.console
+        }
 
     def start(self):
         RecordModel().query.filter_by(task_id=self.task_id).delete()
@@ -109,22 +114,9 @@ class Deployer:
         self.stage = self.stage_prev_deploy
         self.sequence = 1
 
-        # 检查 python 版本
-        command = 'python --version'
-        result = self.localhost.local(command, wenv=self.config())
-
-        # 检查 git 版本
-        command = 'git --version'
-        result = self.localhost.local(command, wenv=self.config())
-
         # 检查 目录是否存在
         self.init_repo()
 
-        # self.init_repo() 函数中已经操作了
-        # TODO to be removed
-        # command = 'mkdir -p %s' % (self.dir_codebase_project)
-        # result = self.localhost.local(command, wenv=self.config())
-
         # 用户自定义命令
         command = self.project_info['prev_deploy']
         if command:
@@ -210,13 +202,6 @@ class Deployer:
             command = 'mkdir -p %s' % (self.project_info['target_releases'])
             result = waller.run(command, wenv=self.config())
 
-        # 用户自定义命令
-        command = self.project_info['prev_release']
-        if command:
-            current_app.logger.info(command)
-            with waller.cd(self.project_info['target_releases']):
-                result = waller.run(command, wenv=self.config())
-
         # TODO md5
         # 传送到版本库 release
         result = waller.put(self.local_codebase + self.release_version_tar,
@@ -226,6 +211,19 @@ class Deployer:
         # 解压
         self.release_untar(waller)
 
+        # 用户自定义命令
+        self.prev_release_custom(waller)
+
+    def prev_release_custom(self, waller):
+        # 用户自定义命令
+        command = self.project_info['prev_release']
+        if command:
+            current_app.logger.info(command)
+            # TODO
+            target_release_version = "%s/%s" % (self.project_info['target_releases'], self.release_version)
+            with waller.cd(target_release_version):
+                result = waller.run(command, wenv=self.config())
+
     def release(self, waller):
         '''
         5.部署代码到目标机器做的任务
@@ -238,6 +236,39 @@ class Deployer:
         self.sequence = 5
 
         with waller.cd(self.project_info['target_releases']):
+            # 0. get previous link
+            command = '[ -L %s ] && readlink %s || echo ""' % (self.project_info['target_root'], self.project_info['target_root'])
+            result = waller.run(command, wenv=self.config(console=False))
+            self.previous_release_version = os.path.basename(result.stdout).strip()
+
+            # 1. create a tmp link dir
+            current_link_tmp_dir = '%s/current-tmp-%s' % (self.project_info['target_releases'], self.task_id)
+            command = 'ln -sfn %s/%s %s' % (
+                self.project_info['target_releases'], self.release_version, current_link_tmp_dir)
+            result = waller.run(command, wenv=self.config())
+
+            # 2. make a soft link from release to tmp link
+
+            # 3. move tmp link to webroot
+            current_link_tmp_dir = '%s/current-tmp-%s' % (self.project_info['target_releases'], self.task_id)
+            command = 'mv -fT %s %s' % (current_link_tmp_dir, self.project_info['target_root'])
+            result = waller.run(command, wenv=self.config())
+
+    def rollback(self, waller):
+        '''
+        5.部署代码到目标机器做的任务
+        - 恢复旧版本
+        :return:
+        '''
+        self.stage = self.stage_release
+        self.sequence = 5
+
+        with waller.cd(self.project_info['target_releases']):
+            # 0. get previous link
+            command = '[ -L %s ] && readlink %s || echo ""' % (self.project_info['target_root'], self.project_info['target_root'])
+            result = waller.run(command, wenv=self.config(console=False))
+            self.previous_release_version = os.path.basename(result.stdout)
+
             # 1. create a tmp link dir
             current_link_tmp_dir = '%s/current-tmp-%s' % (self.project_info['target_releases'], self.task_id)
             command = 'ln -sfn %s/%s %s' % (
@@ -272,12 +303,10 @@ class Deployer:
 
         # 用户自定义命令
         command = self.project_info['post_release']
-        if not command:
-            return None
-
-        current_app.logger.info(command)
-        with waller.cd(self.project_info['target_root']):
-            result = waller.run(command, wenv=self.config())
+        if command:
+            current_app.logger.info(command)
+            with waller.cd(self.project_info['target_root']):
+                result = waller.run(command, wenv=self.config())
 
         # 个性化,用户重启的不一定是NGINX,可能是tomcat, apache, php-fpm等
         # self.post_release_service(waller)
@@ -295,19 +324,6 @@ class Deployer:
 
     def project_detection(self):
         errors = []
-        #  walle user => walle LOCAL_SERVER_USER
-        # show ssh_rsa.pub (maybe not necessary)
-        # command = 'whoami'
-        # current_app.logger.info(command)
-        # result = self.localhost.local(command, exception=False, wenv=self.config())
-        # if result.failed:
-        #     errors.append({
-        #         'title': u'本地免密码登录失败',
-        #         'why': result.stdout,
-        #         'how': u'在宿主机中配置免密码登录,把walle启动用户%s的~/.ssh/ssh_rsa.pub添加到LOCAL_SERVER_USER用户%s的~/.ssh/authorized_keys。了解更多:http://walle-web.io/docs/troubleshooting.html' % (
-        #         pwd.getpwuid(os.getuid())[0], current_app.config.get('LOCAL_SERVER_USER')),
-        #     })
-
         # LOCAL_SERVER_USER => git
 
         # LOCAL_SERVER_USER => target_servers
@@ -316,9 +332,9 @@ class Deployer:
             result = waller.run('id', exception=False, wenv=self.config())
             if result.failed:
                 errors.append({
-                    'title': u'远程目标机器免密码登录失败',
-                    'why': u'远程目标机器:%s 错误:%s' % (server_info['host'], result.stdout),
-                    'how': u'在宿主机中配置免密码登录,把宿主机用户%s的~/.ssh/ssh_rsa.pub添加到远程目标机器用户%s的~/.ssh/authorized_keys。了解更多:http://walle-web.io/docs/troubleshooting.html' % (
+                    'title': '远程目标机器免密码登录失败',
+                    'why': '远程目标机器:%s 错误:%s' % (server_info['host'], result.stdout),
+                    'how': '在宿主机中配置免密码登录,把宿主机用户%s的~/.ssh/ssh_rsa.pub添加到远程目标机器用户%s的~/.ssh/authorized_keys。了解更多:http://walle-web.io/docs/troubleshooting.html' % (
                     pwd.getpwuid(os.getuid())[0], server_info['host']),
                 })
 
@@ -327,9 +343,9 @@ class Deployer:
             result = waller.run(command, exception=False, wenv=self.config())
             if result.stdout == 'false':
                 errors.append({
-                    'title': u'远程目标机器webroot不能是已建好的目录',
-                    'why': u'远程目标机器%s webroot不能是已存在的目录,必须为软链接,你不必新建,walle会自行创建。' % (server_info['host']),
-                    'how': u'手工删除远程目标机器:%s webroot目录:%s' % (server_info['host'], self.project_info['target_root']),
+                    'title': '远程目标机器webroot不能是已建好的目录',
+                    'why': '远程目标机器%s webroot不能是已存在的目录,必须为软链接,你不必新建,walle会自行创建。' % (server_info['host']),
+                    'how': '手工删除远程目标机器:%s webroot目录:%s' % (server_info['host'], self.project_info['target_root']),
                 })
 
         # remote release directory
@@ -411,8 +427,6 @@ class Deployer:
         if not os.path.exists(self.dir_codebase_project):
             # 检查 目录是否存在
             command = 'mkdir -p %s' % (self.dir_codebase_project)
-            # TODO remove
-            current_app.logger.info(command)
             self.localhost.local(command, wenv=self.config())
 
         with self.localhost.cd(self.dir_codebase_project):
@@ -438,7 +452,11 @@ class Deployer:
         if update_status:
             status = TaskModel.status_success if success else TaskModel.status_fail
             current_app.logger.info('success:%s, status:%s' % (success, status))
-            TaskModel().get_by_id(self.task_id).update({'status': status})
+            TaskModel().get_by_id(self.task_id).update({
+                'status': status,
+                'link_id': self.release_version,
+                'ex_link_id': self.previous_release_version,
+            })
 
         notice_info = {
             'title': '',
@@ -475,10 +493,50 @@ class Deployer:
                     self.prev_release(self.connections[host])
                     self.release(self.connections[host])
                     self.post_release(self.connections[host])
+                    RecordModel().save_record(stage=RecordModel.stage_end, sequence=0, user_id=current_user.id,
+                                              task_id=self.task_id, status=RecordModel.status_success, host=host,
+                                              user=server_info['user'], command='')
+                    emit('success', {'event': 'finish', 'data': {'host': host, 'message': host + ' 部署完成!'}}, room=self.task_id)
+                except Exception as e:
+                    is_all_servers_success = False
+                    current_app.logger.error(e)
+                    self.errors[host] = e.message
+                    RecordModel().save_record(stage=RecordModel.stage_end, sequence=0, user_id=current_user.id,
+                                              task_id=self.task_id, status=RecordModel.status_fail, host=host,
+                                              user=server_info['user'], command='')
+                    emit('fail', {'event': 'finish', 'data': {'host': host, 'message': host + Code.code_msg[Code.deploy_fail]}}, room=self.task_id)
+            self.end(is_all_servers_success)
+
+        except Exception as e:
+            self.end(False)
+
+        return {'success': self.success, 'errors': self.errors}
+
+    def walle_rollback(self):
+        self.start()
+
+        try:
+            is_all_servers_success = True
+            self.release_version = self.taskMdl.get('link_id')
+            for server_info in self.servers:
+                host = server_info['host']
+                try:
+                    self.connections[host] = Waller(host=host, user=server_info['user'], port=server_info['port'])
+                    self.prev_release_custom(self.connections[host])
+                    self.release(self.connections[host])
+                    self.post_release(self.connections[host])
+                    RecordModel().save_record(stage=RecordModel.stage_end, sequence=0, user_id=current_user.id,
+                                              task_id=self.task_id, status=RecordModel.status_success, host=host,
+                                              user=server_info['user'], command='')
+                    emit('success', {'event': 'finish', 'data': {'host': host, 'message': host + ' 部署完成!'}}, room=self.task_id)
                 except Exception as e:
                     is_all_servers_success = False
                     current_app.logger.error(e)
                     self.errors[host] = e.message
+                    RecordModel().save_record(stage=RecordModel.stage_end, sequence=0, user_id=current_user.id,
+                                              task_id=self.task_id, status=RecordModel.status_fail, host=host,
+                                              user=server_info['user'], command='')
+                    emit('fail', {'event': 'finish', 'data': {'host': host, 'message': host + Code.code_msg[Code.deploy_fail]}}, room=self.task_id)
             self.end(is_all_servers_success)
 
         except Exception as e:

+ 1 - 1
walle/service/notice/dingding.py

@@ -33,7 +33,7 @@ class Dingding(Notice):
             "msgtype": "markdown",
             "markdown": {
                 "title": "上线单通知",
-                "text": u"""#### ![screenshot](http://walle-web.io/dingding.jpg) %s %s  \n> **项目**:%s \n
+                "text": """#### ![screenshot](http://walle-web.io/dingding.jpg) %s %s  \n> **项目**:%s \n
                 > **任务**:%s \n
                 > **分支**:%s \n
                 > **版本**:%s \n """ % (

+ 1 - 1
walle/service/notice/email.py

@@ -27,7 +27,7 @@ class Email(Notice):
             'is_branch',
         @return:
         '''
-        message = u""" %s %s 
+        message = """ %s %s 
                 <br><br> <strong>项目</strong>:%s
                 <br><br> <strong>任务</strong>:%s
                 <br><br> <strong>分支</strong>:%s

+ 0 - 37
walle/service/notice/snotice.py

@@ -1,37 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-    walle-web
-
-    :copyright: © 2015-2019 walle-web.io
-    :created time: 2018-12-23 20:15:30
-    :author: wushuiyong@walle-web.io
-"""
-# # import . as this
-# from walle.service.error import WalleError, Code
-#
-#
-# class Notice():
-#     by_dingding = 'Dingding'
-#
-#     by_email = 'Eamil'
-#
-#     def deploy_task(self):
-#         pass
-#
-#     @classmethod
-#     def create(cls, by):
-#         '''
-#         usage:
-#         create Dingding
-#         Notice.create(Notice.by_dingding)
-#
-#         @param by:
-#         @return:
-#         '''
-#         if by == cls.by_dingding:
-#             return .dingding.Dingding()
-#         elif by == cls.by_email:
-#             pass
-#             # return .email.Email()
-#         else:
-#             raise WalleError(Code.sys_params_err)

+ 0 - 1
walle/service/rbac/role.py

@@ -146,7 +146,6 @@ class Permission():
         :param uid:
         :return:
         '''
-        # TODO
         return current_user.id == uid
 
     def role_upper_owner(self, role=None):

+ 2 - 2
walle/service/utils.py

@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 """Helper utilities and decorators."""
-from __future__ import print_function
+
 
 import fnmatch
 import sys
@@ -15,7 +15,7 @@ from invoke import Responder
 
 def flash_errors(form, category='warning'):
     """Flash all errors for a form."""
-    for field, errors in form.errors.items():
+    for field, errors in list(form.errors.items()):
         for error in errors:
             flash('{0} - {1}'.format(getattr(form, field).label.text, error), category)
 

+ 0 - 1
walle/service/waller.py

@@ -46,7 +46,6 @@ class Waller(Connection):
             else:
                 result = super(Waller, self).run(command, pty=pty, warn=True, watchers=[say_yes()], **kwargs)
 
-
             if result.failed:
                 exitcode, stdout, stderr = result.exited, '', result.stdout
                 if exception:

+ 12 - 2
walle/service/websocket.py

@@ -12,6 +12,8 @@ from flask_socketio import emit, join_room, Namespace
 from walle.model.record import RecordModel
 from walle.model.task import TaskModel
 from walle.service.deployer import Deployer
+from walle.service.error import Code
+
 
 class WalleSocketIO(Namespace):
     namespace, room, app = None, None, None
@@ -47,7 +49,10 @@ class WalleSocketIO(Namespace):
     def on_deploy(self, message):
         if self.task_info['status'] in [TaskModel.status_pass, TaskModel.status_fail]:
             wi = Deployer(task_id=self.room, console=True)
-            ret = wi.walle_deploy()
+            if self.task_info['is_rollback']:
+                wi.walle_rollback()
+            else:
+                wi.walle_deploy()
         else:
             emit('console', {'event': 'forbidden', 'data': self.task_info}, room=self.room)
 
@@ -98,7 +103,12 @@ class WalleSocketIO(Namespace):
         deployer = Deployer(task_id=self.room)
         for log in deployer.logs():
             log = RecordModel.logs(**log)
-            emit('console', {'event': 'console', 'data': log}, room=self.room)
+            if log['stage'] == RecordModel.stage_end:
+                cmd = 'success' if log['status'] == RecordModel.status_success else 'fail'
+                msg = log['host'] + ' 部署完成!' if log['status'] == RecordModel.status_success else log['host'] + Code.code_msg[Code.deploy_fail]
+                emit(cmd, {'event': 'finish', 'data': {'host': log['host'], 'message': msg}}, room=self.room)
+            else:
+                emit('console', {'event': 'console', 'data': log}, room=self.room)
 
         deployer.end(success=task_info.status == TaskModel.status_success, update_status=False)