Selaa lähdekoodia

Merge branch 'master' of https://github.com/meolu/walle-web

Alenx 6 vuotta sitten
vanhempi
commit
ea37ac814b

+ 9 - 9
migrations/versions/2bca06a823a0_init_walle_database.py

@@ -49,7 +49,7 @@ def create_environments():
               `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '',
               `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '',
               PRIMARY KEY (`id`)
-            ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='';"""
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='';"""
     db.session.execute(sql)
 
 
@@ -77,7 +77,7 @@ def create_menus():
               `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '',
               `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '',
               PRIMARY KEY (`id`)
-            ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='';"""
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='';"""
     db.session.execute(sql)
 
 
@@ -127,7 +127,7 @@ def create_projects():
               `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '',
               `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '',
               PRIMARY KEY (`id`)
-            ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='';"""
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='';"""
     db.session.execute(sql)
 
 
@@ -147,7 +147,7 @@ def create_records():
               `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '',
               `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '',
               PRIMARY KEY (`id`)
-            ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='';"""
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='';"""
     db.session.execute(sql)
 
 
@@ -161,7 +161,7 @@ def create_servers():
               `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '',
               `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '',
               PRIMARY KEY (`id`)
-            ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='';"""
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='';"""
     db.session.execute(sql)
 
 
@@ -174,7 +174,7 @@ def create_spaces():
               `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '',
               `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '',
               PRIMARY KEY (`id`)
-            ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='';"""
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='';"""
     db.session.execute(sql)
 
 
@@ -204,7 +204,7 @@ def create_tasks():
               `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '',
               `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '',
               PRIMARY KEY (`id`)
-            ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='';"""
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='';"""
     db.session.execute(sql)
 
 
@@ -223,7 +223,7 @@ def create_users():
               `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '',
               `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '',
               PRIMARY KEY (`id`)
-            ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='';"""
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='';"""
     db.session.execute(sql)
 
 
@@ -248,7 +248,7 @@ def create_members():
               `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '',
               `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '',
               PRIMARY KEY (`id`)
-            ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='';"""
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='';"""
     db.session.execute(sql)
 
 

+ 34 - 13
walle/config/settings.py

@@ -1,29 +1,50 @@
 # -*- coding: utf-8 -*-
 """Application configuration."""
 import os
+from datetime import timedelta
 
 
 class Config(object):
     """Base configuration."""
 
-    SECRET_KEY = os.environ.get('WALLE_SECRET', 'secret-key')  # TODO: Change me
-    APP_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))  # This directory
+    SECRET_KEY = os.environ.get('WALLE_SECRET', 'secret-key')
+    APP_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
     PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir))
     BCRYPT_LOG_ROUNDS = 13
     ASSETS_DEBUG = False
-    DEBUG_TB_ENABLED = False  # Disable Debug toolbar
+    WTF_CSRF_ENABLED = False
+    DEBUG_TB_ENABLED = False
     DEBUG_TB_INTERCEPT_REDIRECTS = False
-    CACHE_TYPE = 'simple'  # Can be "memcached", "redis", etc.
+
+    # Can be "memcached", "redis", etc.
+    CACHE_TYPE = 'simple'
     SQLALCHEMY_TRACK_MODIFICATIONS = False
     SQLALCHEMY_COMMIT_ON_TEARDOWN = True
 
-    AVATAR_PATH = 'avatar/'
-
     LOGIN_DISABLED = False
-
-
-    LOCAL_SERVER_HOST = '127.0.0.1'
-    LOCAL_SERVER_USER = 'wushuiyong'
-    LOCAL_SERVER_PORT = 22
-
-    CODE_BASE = '/tmp/walle/codebase/'
+    # 设置session的保存时间。
+    PERMANENT_SESSION_LIFETIME = timedelta(days=1)
+
+    # 前端项目部署路径
+    FE_PATH = os.path.abspath(PROJECT_ROOT + '/fe/') + '/'
+    AVATAR_PATH = '/avatar/'
+    UPLOAD_AVATAR = FE_PATH + AVATAR_PATH
+
+    # 邮箱配置
+    MAIL_SERVER = 'smtp.exmail.qq.com'
+    MAIL_PORT = 465
+    MAIL_USE_SSL = True
+    MAIL_USE_TLS = False
+    MAIL_DEFAULT_SENDER = 'service@walle-web.io'
+    MAIL_USERNAME = 'service@walle-web.io'
+    MAIL_PASSWORD = 'Ki9y&3U82'
+
+    # 日志
+    LOG_PATH = os.path.join(PROJECT_ROOT, 'logs')
+    LOG_PATH_ERROR = os.path.join(LOG_PATH, 'error.log')
+    LOG_PATH_INFO = os.path.join(LOG_PATH, 'info.log')
+    LOG_FILE_MAX_BYTES = 100 * 1024 * 1024
+
+    # 轮转数量是 10 个
+    LOG_FILE_BACKUP_COUNT = 10
+    LOG_FORMAT = "%(asctime)s %(thread)d %(message)s"

+ 12 - 40
walle/config/settings_dev.py

@@ -8,56 +8,28 @@
     :created time: 2018-11-24 07:05:35
     :author: wushuiyong@walle-web.io
 """
-from datetime import timedelta
 
 import os
 from walle.config.settings import Config
 
 
 class DevConfig(Config):
-    """Development configuration."""
+    """Production configuration."""
+    ENV = 'dev'
+    DEBUG = True
 
     # 服务启动 @TODO
-    # 跟hosts, nginx配置一致
+    # HOST 修改为与 nginx server_name 一致.
+    # 后续在web hooks与通知中用到此域名.
     HOST = 'dev.admin.walle-web.io'
     PORT = 5000
+    # https True, http False
+    SSL = False
 
-    ENV = 'dev'
-    DEBUG = True
-    WTF_CSRF_ENABLED = False
-    DEBUG_TB_ENABLED = True
-    CACHE_TYPE = 'simple'
-
-    # 数据库配置 @TODO
-    SQLALCHEMY_DATABASE_URI = 'mysql://user:password@localhost/walle_python'
-    PERMANENT_SESSION_LIFETIME = timedelta(days=1)
-
-    # 前端项目部署路径
-    FE_PATH = os.path.abspath(Config.PROJECT_ROOT + '/../walle-fe/')
-    AVATAR_PATH = 'avatar/'
-    UPLOAD_AVATAR = FE_PATH + '/dist/' + AVATAR_PATH
-
-    # 邮箱配置 @TODO
-    MAIL_SERVER = 'smtp.exmail.qq.com'
-    MAIL_PORT = 465
-    MAIL_USE_SSL = True
-    MAIL_USE_TLS = False
-    MAIL_DEFAULT_SENDER = 'service@walle-web.io'
-    MAIL_USERNAME = 'service@walle-web.io'
-    MAIL_PASSWORD = 'Ki9y&3U82'
-
-    LOG_PATH = os.path.join(Config.PROJECT_ROOT, 'logs')
-    LOG_PATH_ERROR = os.path.join(LOG_PATH, 'error.log')
-    LOG_PATH_INFO = os.path.join(LOG_PATH, 'info.log')
-    LOG_PATH_DEBUG = os.path.join(LOG_PATH, 'debug.log')
-    LOG_FILE_MAX_BYTES = 100 * 1024 * 1024
-
-    # 轮转数量是 10 个
-    LOG_FILE_BACKUP_COUNT = 10
-    LOG_FORMAT = "%(asctime)s %(thread)d %(message)s"
-
-    LOCAL_SERVER_HOST = '127.0.0.1'
-    LOCAL_SERVER_USER = 'wushuiyong'
-    LOCAL_SERVER_PORT = 22
+    # 数据库设置 @TODO
+    SQLALCHEMY_DATABASE_URI = 'mysql://user:password@localhost:3306/walle?charset=utf8'
+
+    # 本地代码检出路径(用户查询分支, 编译, 打包) #TODO
+    CODE_BASE = '/tmp/walle/codebase/'
 
     SQLALCHEMY_ECHO = True

+ 16 - 36
walle/config/settings_prod.py

@@ -9,27 +9,23 @@
     :author: wushuiyong@walle-web.io
 """
 
-from datetime import timedelta
-
 import os
 from walle.config.settings import Config
 
 
 class ProdConfig(Config):
     """Production configuration."""
-
-    # 服务启动
-    HOST = '0.0.0.0'
-    PORT = 5000
     ENV = 'prod'
     DEBUG = False
+    SQLALCHEMY_ECHO = False
 
-    # 宿主机(walle部署所在的机器以及用户) @TODO
-    # 建议启动walle服务用户与LOCAL_SERVER_USER一致. 并且做该用户的免密码登录
-    # https://walle-web.io/docs/troubleshooting.html#Authentication-failed
-    LOCAL_SERVER_HOST = '127.0.0.1'
-    LOCAL_SERVER_USER = 'work'
-    LOCAL_SERVER_PORT = 22
+    # 服务启动 @TODO
+    # HOST 修改为与 nginx server_name 一致.
+    # 后续在web hooks与通知中用到此域名.
+    HOST = 'admin.walle-web.io'
+    PORT = 5000
+    # https True, http False
+    SSL = False
 
     # 数据库设置 @TODO
     SQLALCHEMY_DATABASE_URI = 'mysql://user:password@localhost:3306/walle?charset=utf8'
@@ -37,6 +33,14 @@ class ProdConfig(Config):
     # 本地代码检出路径(用户查询分支, 编译, 打包) #TODO
     CODE_BASE = '/tmp/walle/codebase/'
 
+    # 日志存储路径 @TODO
+    # 默认为walle-web项目下logs, 可自定义路径, 需以 / 结尾
+    # LOG_PATH = '/var/logs/walle/'
+    LOG_PATH = os.path.join(Config.PROJECT_ROOT, 'logs')
+    LOG_PATH_ERROR = os.path.join(LOG_PATH, 'error.log')
+    LOG_PATH_INFO = os.path.join(LOG_PATH, 'info.log')
+    LOG_FILE_MAX_BYTES = 100 * 1024 * 1024
+
     # 邮箱配置 @TODO
     MAIL_SERVER = 'smtp.exmail.qq.com'
     MAIL_PORT = 465
@@ -45,27 +49,3 @@ class ProdConfig(Config):
     MAIL_DEFAULT_SENDER = 'service@walle-web.io'
     MAIL_USERNAME = 'service@walle-web.io'
     MAIL_PASSWORD = 'Ki9y&3U82'
-
-    # 日志 @TODO
-    LOG_PATH = os.path.join(Config.PROJECT_ROOT, 'logs')
-    LOG_PATH_ERROR = os.path.join(LOG_PATH, 'error.log')
-    LOG_PATH_INFO = os.path.join(LOG_PATH, 'info.log')
-    LOG_FILE_MAX_BYTES = 100 * 1024 * 1024
-
-    WTF_CSRF_ENABLED = False
-    DEBUG_TB_ENABLED = False
-    CACHE_TYPE = 'simple'
-
-    # 轮转数量是 10 个
-    LOG_FILE_BACKUP_COUNT = 10
-    LOG_FORMAT = "%(asctime)s %(thread)d %(message)s"
-
-    # 设置session的保存时间。
-    PERMANENT_SESSION_LIFETIME = timedelta(days=1)
-
-    # 前端项目部署路径
-    FE_PATH = os.path.abspath(Config.PROJECT_ROOT + '/../walle-fe/') + '/'
-    AVATAR_PATH = 'avatar/'
-    UPLOAD_AVATAR = FE_PATH + '/dist/' + AVATAR_PATH
-
-    SQLALCHEMY_ECHO = False

+ 11 - 40
walle/config/settings_test.py

@@ -15,49 +15,20 @@ from datetime import timedelta
 class TestConfig(Config):
     """Test configuration."""
 
-    # 服务启动 @TODO
-    # 跟hosts, nginx配置一致
-    HOST = 'admin.walle-web.io'
-    PORT = 5000
-
     ENV = 'test'
     TESTING = True
     DEBUG = True
-    WTF_CSRF_ENABLED = False
-    DEBUG_TB_ENABLED = True
-    CACHE_TYPE = 'simple'
-
-    # 数据库配置 @TODO
-    SQLALCHEMY_DATABASE_URI = 'sqlite://'
-    PERMANENT_SESSION_LIFETIME = timedelta(days=1)
-
-    # 前端项目部署路径
-    FE_PATH = os.path.abspath(Config.PROJECT_ROOT + '/../walle-fe/')
-    AVATAR_PATH = 'avatar/'
-    UPLOAD_AVATAR = FE_PATH + '/dist/' + AVATAR_PATH
-
-    # 邮箱配置 @TODO
-    MAIL_SERVER = 'smtp.exmail.qq.com'
-    MAIL_PORT = 465
-    MAIL_USE_SSL = True
-    MAIL_USE_TLS = False
-    MAIL_DEFAULT_SENDER = 'service@walle-web.io'
-    MAIL_USERNAME = 'service@walle-web.io'
-    MAIL_PASSWORD = 'Ki9y&3U82'
-
-    LOG_PATH = os.path.join(Config.PROJECT_ROOT, 'logs')
-    LOG_PATH_ERROR = os.path.join(LOG_PATH, 'error.log')
-    LOG_PATH_INFO = os.path.join(LOG_PATH, 'info.log')
-    LOG_PATH_DEBUG = os.path.join(LOG_PATH, 'debug.log')
-    LOG_FILE_MAX_BYTES = 100 * 1024 * 1024
-
-    # 轮转数量是 10 个
-    LOG_FILE_BACKUP_COUNT = 10
-    LOG_FORMAT = "%(asctime)s %(thread)d %(message)s"
 
+    # 服务启动 @TODO
+    # HOST 修改为与 nginx server_name 一致.
+    # 后续在web hooks与通知中用到此域名.
+    HOST = 'admin.walle-web.io'
+    PORT = 5000
+    # https True, http False
+    SSL = False
 
-    LOCAL_SERVER_HOST = '127.0.0.1'
-    LOCAL_SERVER_USER = 'wushuiyong'
-    LOCAL_SERVER_PORT = 22
+    # 数据库设置 @TODO
+    SQLALCHEMY_DATABASE_URI = 'sqlite://'
 
-    SQLALCHEMY_ECHO = True
+    # 本地代码检出路径(用户查询分支, 编译, 打包) #TODO
+    CODE_BASE = '/tmp/walle/codebase/'

+ 2 - 1
walle/form/project.py

@@ -15,6 +15,7 @@ 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
 
 
 class ProjectForm(Form):
@@ -87,7 +88,7 @@ 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 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,
         }

+ 0 - 2
walle/model/project.py

@@ -127,8 +127,6 @@ class ProjectModel(SurrogatePK, Model):
         db.session.commit()
 
         return project.to_json()
-        self.id = project.id
-        return self.id
 
     def update(self, *args, **kwargs):
         # todo permission_ids need to be formated and checked

+ 1 - 1
walle/model/user.py

@@ -202,7 +202,7 @@ class UserModel(UserMixin, SurrogatePK, Model):
     @classmethod
     def avatar_url(cls, avatar):
         avatar = avatar if avatar else 'default.jpg'
-        return '/' + current_app.config['AVATAR_PATH'] + avatar
+        return current_app.config['AVATAR_PATH'] + avatar
 
     @classmethod
     def fetch_by_uid(cls, uids=None):

+ 7 - 0
walle/service/code.py

@@ -65,6 +65,11 @@ class Code():
     #: 任务部署失败,已终止。请修复错误后,重新部署。
     deploy_fail = 5001
 
+    #: ----------------------- 6xxx 程序中防御性编程相关错误 -----------------
+    #: 6xxx 程序中防御性编程相关错误
+    #: 未知的代码传参,请反馈错误日志予作者修正。
+    sys_params_err = 6001
+
     code_msg = {
         unlogin: '未登录',
         error_pwd: '账号密码错误',
@@ -84,4 +89,6 @@ class Code():
         shell_git_pull_fail: 'git pull 失败,请联系管理员',
 
         deploy_fail: '任务部署失败,已终止。请修复错误后,重新部署。',
+
+        sys_params_err: '未知的代码传参,请反馈错误日志予作者修正'
     }

+ 58 - 46
walle/service/deployer.py

@@ -20,8 +20,9 @@ from walle.service.code import Code
 from walle.service.error import WalleError
 from walle.service.utils import color_clean
 from walle.service.utils import excludes_format
+from walle.service.notice import Notice
 from walle.service.waller import Waller
-
+from flask_login import current_user
 
 class Deployer:
     '''
@@ -56,10 +57,7 @@ class Deployer:
 
     def __init__(self, task_id=None, project_id=None, console=False):
         self.local_codebase = current_app.config.get('CODE_BASE')
-        self.local = Waller(host=current_app.config.get('LOCAL_SERVER_HOST'),
-                            user=current_app.config.get('LOCAL_SERVER_USER'),
-                            port=current_app.config.get('LOCAL_SERVER_PORT'),
-                            )
+        self.localhost = Waller(host='127.0.0.1')
         self.TaskRecord = RecordModel()
 
         if task_id:
@@ -113,25 +111,25 @@ class Deployer:
 
         # 检查 python 版本
         command = 'python --version'
-        result = self.local.run(command, wenv=self.config())
+        result = self.localhost.local(command, wenv=self.config())
 
         # 检查 git 版本
         command = 'git --version'
-        result = self.local.run(command, wenv=self.config())
+        result = self.localhost.local(command, wenv=self.config())
 
         # 检查 目录是否存在
         self.init_repo()
 
         # TODO to be removed
         command = 'mkdir -p %s' % (self.dir_codebase_project)
-        result = self.local.run(command, wenv=self.config())
+        result = self.localhost.local(command, wenv=self.config())
 
         # 用户自定义命令
         command = self.project_info['prev_deploy']
         if command:
             current_app.logger.info(command)
-            with self.local.cd(self.dir_codebase_project):
-                result = self.local.run(command, wenv=self.config())
+            with self.localhost.cd(self.dir_codebase_project):
+                result = self.localhost.local(command, wenv=self.config())
 
     def deploy(self):
         '''
@@ -147,32 +145,32 @@ class Deployer:
         # 如果项目底下有 .git 目录则认为项目完整,可以直接检出代码
         # TODO 不标准
         if os.path.exists(self.dir_codebase_project + '/.git'):
-            with self.local.cd(self.dir_codebase_project):
+            with self.localhost.cd(self.dir_codebase_project):
                 command = 'pwd && git pull'
-                result = self.local.run(command, wenv=self.config())
+                result = self.localhost.local(command, wenv=self.config())
 
         else:
             # 否则当作新项目检出完整代码
-            with self.local.cd(self.dir_codebase_project):
+            with self.localhost.cd(self.dir_codebase_project):
                 command = 'pwd && git clone %s .' % (self.project_info['repo_url'])
                 current_app.logger.info('cd %s  command: %s  ', self.dir_codebase_project, command)
 
-                result = self.local.run(command, wenv=self.config())
+                result = self.localhost.local(command, wenv=self.config())
 
         # copy to a local version
         self.release_version = '%s_%s_%s' % (
             self.project_name, self.task_id, time.strftime('%Y%m%d_%H%M%S', time.localtime(time.time())))
 
-        with self.local.cd(self.local_codebase):
+        with self.localhost.cd(self.local_codebase):
             command = 'cp -rf %s %s' % (self.dir_codebase_project, self.release_version)
             current_app.logger.info('cd %s  command: %s  ', self.dir_codebase_project, command)
 
-            result = self.local.run(command, wenv=self.config())
+            result = self.localhost.local(command, wenv=self.config())
 
         # 更新到指定 commit_id
-        with self.local.cd(self.local_codebase + self.release_version):
+        with self.localhost.cd(self.local_codebase + self.release_version):
             command = 'git reset -q --hard %s' % (self.taskMdl.get('commit_id'))
-            result = self.local.run(command, wenv=self.config())
+            result = self.localhost.local(command, wenv=self.config())
 
             if result.exited != Code.Ok:
                 raise WalleError(Code.shell_git_fail, message=result.stdout)
@@ -195,15 +193,15 @@ class Deployer:
         # 用户自定义命令
         command = self.project_info['post_deploy']
         if command:
-            with self.local.cd(self.local_codebase + self.release_version):
-                result = self.local.run(command, wenv=self.config())
+            with self.localhost.cd(self.local_codebase + self.release_version):
+                result = self.localhost.local(command, wenv=self.config())
 
         # 压缩打包
         self.release_version_tar = '%s.tgz' % (self.release_version)
-        with self.local.cd(self.local_codebase):
+        with self.localhost.cd(self.local_codebase):
             excludes = excludes_format(self.project_info['excludes'])
             command = 'tar zcf  %s %s %s' % (self.release_version_tar, excludes, self.release_version)
-            result = self.local.run(command, wenv=self.config())
+            result = self.localhost.local(command, wenv=self.config())
 
     def prev_release(self, waller):
         '''
@@ -306,16 +304,16 @@ class Deployer:
         errors = []
         #  walle user => walle LOCAL_SERVER_USER
         # show ssh_rsa.pub (maybe not necessary)
-        command = 'whoami'
-        current_app.logger.info(command)
-        result = self.local.run(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')),
-            })
+        # 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
 
@@ -328,7 +326,7 @@ class Deployer:
                     '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' % (
-                    current_app.config.get('LOCAL_SERVER_USER'), server_info['host']),
+                    pwd.getpwuid(os.getuid())[0], server_info['host']),
                 })
 
                 # 检查 webroot 父目录是否存在,是否为软链
@@ -347,9 +345,9 @@ class Deployer:
     def list_tag(self):
         self.init_repo()
 
-        with self.local.cd(self.dir_codebase_project):
+        with self.localhost.cd(self.dir_codebase_project):
             command = 'git tag -l'
-            result = self.local.run(command, pty=False, wenv=self.config())
+            result = self.localhost.local(command, pty=False, wenv=self.config())
             tags = result.stdout.strip()
             tags = tags.split('\n')
             return [color_clean(tag.strip()) for tag in tags]
@@ -359,9 +357,9 @@ class Deployer:
     def list_branch(self):
         self.init_repo()
 
-        with self.local.cd(self.dir_codebase_project):
+        with self.localhost.cd(self.dir_codebase_project):
             command = 'git pull'
-            result = self.local.run(command, wenv=self.config())
+            result = self.localhost.local(command, wenv=self.config())
 
             if result.exited != Code.Ok:
                 raise WalleError(Code.shell_git_pull_fail, message=result.stdout)
@@ -369,7 +367,7 @@ class Deployer:
             current_app.logger.info(self.dir_codebase_project)
 
             command = 'git branch -r'
-            result = self.local.run(command, pty=False, wenv=self.config())
+            result = self.localhost.local(command, pty=False, wenv=self.config())
 
             # if result.exited != Code.Ok:
             #     raise WalleError(Code.shell_run_fail)
@@ -387,12 +385,12 @@ class Deployer:
 
     def list_commit(self, branch):
         self.init_repo()
-        with self.local.cd(self.dir_codebase_project):
+        with self.localhost.cd(self.dir_codebase_project):
             command = 'git checkout %s && git pull' % (branch)
-            self.local.run(command, wenv=self.config())
+            self.localhost.local(command, wenv=self.config())
 
             command = 'git log -50 --pretty="%h #@_@# %an #@_@# %s"'
-            result = self.local.run(command, pty=False, wenv=self.config())
+            result = self.localhost.local(command, pty=False, wenv=self.config())
             current_app.logger.info(result.stdout)
 
             commit_log = result.stdout.strip()
@@ -422,21 +420,21 @@ class Deployer:
             command = 'mkdir -p %s' % (self.dir_codebase_project)
             # TODO remove
             current_app.logger.info(command)
-            self.local.run(command, wenv=self.config())
+            self.localhost.local(command, wenv=self.config())
 
-        with self.local.cd(self.dir_codebase_project):
-            is_git_dir = self.local.run('git status', exception=False, wenv=self.config())
+        with self.localhost.cd(self.dir_codebase_project):
+            is_git_dir = self.localhost.local('git status', exception=False, wenv=self.config())
 
         if is_git_dir.exited != Code.Ok:
             # 否则当作新项目检出完整代码
             # 检查 目录是否存在
             command = 'rm -rf %s' % (self.dir_codebase_project)
-            self.local.run(command, wenv=self.config())
+            self.localhost.local(command, wenv=self.config())
 
             command = 'git clone %s %s' % (self.project_info['repo_url'], self.dir_codebase_project)
             current_app.logger.info('cd %s  command: %s  ', self.dir_codebase_project, command)
 
-            result = self.local.run(command, wenv=self.config())
+            result = self.localhost.local(command, wenv=self.config())
             if result.exited != Code.Ok:
                 raise WalleError(Code.shell_git_init_fail, message=result.stdout)
 
@@ -449,10 +447,24 @@ class Deployer:
             current_app.logger.info('success:%s, status:%s' % (success, status))
             TaskModel().get_by_id(self.task_id).update({'status': status})
 
+        notice_info = {
+            'title': '',
+            'username': current_user.username,
+            'project_name': self.project_info['name'],
+            'task_name': '%s ([%s](%s))' % (self.taskMdl.get('name'), self.task_id, Notice.task_url(project_name=self.project_info['name'], task_id=self.task_id)),
+            'branch': self.taskMdl.get('branch'),
+            'commit': self.taskMdl.get('commit_id'),
+            'is_branch': self.project_info['repo_mode'],
+        }
+        notice = Notice.create(self.project_info['notice_type'])
         if success:
             emit('success', {'event': 'finish', 'data': {'message': '部署完成,辛苦了,为你的努力喝彩!'}}, room=self.task_id)
+            notice_info['title'] = '上线部署成功'
+            notice.deploy_task(project_info=self.project_info, notice_info=notice_info)
         else:
             emit('fail', {'event': 'finish', 'data': {'message': Code.code_msg[Code.deploy_fail]}}, room=self.task_id)
+            notice_info['title'] = '上线部署失败'
+            notice.deploy_task(project_info=self.project_info, notice_info=notice_info)
 
     def walle_deploy(self):
         self.start()

+ 45 - 0
walle/service/notice/__init__.py

@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+"""
+    walle-web
+
+    :copyright: © 2015-2019 walle-web.io
+    :created time: 2018-12-23 20:15:27
+    :author: wushuiyong@walle-web.io
+"""
+from flask import current_app
+from walle.service.error import WalleError, Code
+
+
+
+class Notice():
+    by_dingding = 'dingding'
+
+    by_email = 'email'
+
+    def deploy_task(self, project_info, notice_info):
+        pass
+
+    @classmethod
+    def task_url(cls, project_name, task_id):
+        return '%s//%s/%s/task/deploy/%s' % ('https' if current_app.config.get('SSL') else 'http',
+                                             current_app.config.get('HOST'),
+                                             project_name, task_id)
+
+    @classmethod
+    def create(cls, by):
+        '''
+        usage:
+        create Dingding
+        Notice.create(Notice.by_dingding)
+
+        @param by:
+        @return:
+        '''
+        if by.lower() == cls.by_dingding:
+            from walle.service.notice.dingding import Dingding
+            return Dingding()
+        elif by.lower() == cls.by_email:
+            from walle.service.notice.email import Email
+            return Email()
+        else:
+            return Notice()

+ 47 - 0
walle/service/notice/dingding.py

@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+"""
+    walle-web
+
+    :copyright: © 2015-2019 walle-web.io
+    :created time: 2018-12-23 20:17:14
+    :author: wushuiyong@walle-web.io
+"""
+import json
+
+import requests
+from . import Notice
+
+
+class Dingding(Notice):
+
+    def deploy_task(self, project_info, notice_info):
+        '''
+        上线单新建, 上线完成, 上线失败
+
+        @param hook:
+        @param notice_info:
+            'title',
+            'username',
+            'project_name',
+            'task_name',
+            'branch',
+            'commit',
+            'is_branch',
+        @return:
+        '''
+        data = {
+            "msgtype": "markdown",
+            "markdown": {
+                "title": "上线单通知",
+                "text": u"""#### ![screenshot](http://walle-web.io/dingding.jpg) %s %s  \n> **项目**:%s \n
+                > **任务**:%s \n
+                > **分支**:%s \n
+                > **版本**:%s \n """ % (
+                notice_info['username'], notice_info['title'], notice_info['project_name'], notice_info['task_name'],
+                notice_info['branch'], notice_info['commit'])
+            }
+        }
+        headers = {'Content-Type': 'application/json;charset=UTF-8'}
+        response = requests.post(project_info['notice_hook'], data=json.dumps(data).encode('utf-8'), headers=headers)
+
+        return response.json()['errcode'] == 0

+ 38 - 0
walle/service/notice/email.py

@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+"""
+    walle-web
+
+    :copyright: © 2015-2019 walle-web.io
+    :created time: 2018-12-23 20:18:11
+    :author: wushuiyong@walle-web.io
+"""
+from . import Notice
+from walle.service import emails
+
+
+class Email(Notice):
+
+    def deploy_task(self, project_info, notice_info):
+        '''
+        上线单新建, 上线完成, 上线失败
+
+        @param hook:
+        @param notice_info:
+            'title',
+            'username',
+            'project_name',
+            'task_name',
+            'branch',
+            'commit',
+            'is_branch',
+        @return:
+        '''
+        message = u""" %s %s 
+                <br><br> **项目**:%s
+                <br><br> **任务**:%s
+                <br><br> **分支**:%s
+                <br><br> **版本**:%s
+                <br><br><br><img src='http://walle-web.io/dingding.jpg'> """ % (
+                notice_info['username'], notice_info['title'], notice_info['project_name'], notice_info['task_name'],
+                notice_info['branch'], notice_info['commit'])
+        emails.send_email(project_info['notice_hook'], notice_info['title'], message, '')

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

@@ -0,0 +1,37 @@
+# -*- 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)

+ 18 - 5
walle/service/waller.py

@@ -4,6 +4,8 @@
 # @Created Time : 日  1/ 1 23:43:12 2017
 # @Description:
 
+import os
+import pwd
 from fabric2 import Connection
 from flask import current_app
 from flask_socketio import emit
@@ -13,10 +15,15 @@ from walle.service.code import Code
 from walle.service.utils import say_yes
 
 class Waller(Connection):
+
+    run_mode_sudo = 'sudo'
+    run_mode_remote = 'remote'
+    run_mode_local = 'local'
+
     connections, success, errors = {}, {}, {}
     release_version_tar, release_version = None, None
 
-    def run(self, command, wenv=None, sudo=False, pty=True, exception=True, **kwargs):
+    def run(self, command, wenv=None, run_mode=run_mode_remote, pty=True, exception=True, **kwargs):
         '''
         pty=True/False是直接影响到输出.False较适合在获取文本,True更适合websocket
 
@@ -32,11 +39,14 @@ class Waller(Connection):
         message = 'deploying task_id=%s [%s@%s]$ %s ' % (wenv['task_id'], self.user, self.host, command)
         current_app.logger.info(message)
         try:
-            if sudo:
+            if run_mode == self.run_mode_sudo:
                 result = super(Waller, self).sudo(command, pty=pty, **kwargs)
+            elif run_mode == self.run_mode_local:
+                result = super(Waller, self).local(command, pty=pty, warn=True, watchers=[say_yes()], **kwargs)
             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:
@@ -109,7 +119,7 @@ class Waller(Connection):
             return Result(exited=-1, stderr=error, stdout=error)
 
     def sudo(self, command, wenv=None, **kwargs):
-        return self.run(command, wenv=wenv, sudo=True, **kwargs)
+        return self.run(command, wenv=wenv, run_mode=self.run_mode_sudo, **kwargs)
 
     def get(self, remote, local=None, wenv=None):
         return self.sync(wtype='get', remote=remote, local=local, wenv=wenv)
@@ -117,6 +127,9 @@ class Waller(Connection):
     def put(self, local, remote=None, wenv=None, *args, **kwargs):
         return self.sync(wtype='put', local=local, remote=remote, wenv=wenv, *args, **kwargs)
 
+    def local(self, command, wenv=None, **kwargs):
+        return self.run(command, wenv=wenv, run_mode=self.run_mode_local, **kwargs)
+
     def sync(self, wtype, remote=None, local=None, wenv=None):
         command = 'scp %s %s@%s:%s' % (local, self.user, self.host, remote) if wtype == 'put' \
             else 'scp %s@%s:%s %s' % (self.user, self.host, remote, local)
@@ -127,8 +140,8 @@ class Waller(Connection):
             if wtype == 'put':
                 result = super(Waller, self).put(local=local, remote=remote)
                 current_app.logger.info('put: local %s, remote %s', local, remote)
-                op_user = current_app.config.get('LOCAL_SERVER_USER')
-                op_host = current_app.config.get('LOCAL_SERVER_HOST')
+                op_user = pwd.getpwuid(os.getuid())[0]
+                op_host = '127.0.0.1'
 
             else:
                 result = super(Waller, self).get(remote=remote, local=local)