├── .gitignore ├── README.md ├── WEB API.md ├── control ├── db-schema ├── db-schema-upgrade_v1.0.0_to_v2.0.0.sql └── db-schema.sql ├── docs ├── create_deploy.png ├── create_project.png ├── deploy_progress.png └── deploys.png ├── init.py ├── manage.py ├── pip_requirements.txt └── web ├── __init__.py ├── config.py ├── controller ├── __init__.py ├── api.py ├── deploys.py ├── host.py ├── login.py ├── project.py ├── users.py └── webhooks.py ├── models ├── __init__.py ├── deploys.py ├── hosts.py ├── projects.py ├── rel_user_host.py ├── rel_user_project.py ├── sessions.py └── users.py ├── services ├── __init__.py ├── base.py ├── deploys.py ├── hosts.py ├── projects.py ├── sessions.py └── users.py ├── static ├── css │ └── bootstrap.min.css └── js │ ├── bootstrap.min.js │ ├── jquery.cookie.min.js │ ├── jquery.min.js │ └── pydelo │ ├── account_change_password.js │ ├── api.js │ ├── deploy_create.js │ ├── deploy_progress.js │ ├── deploys.js │ ├── host_create.js │ ├── host_detail.js │ ├── hosts.js │ ├── login.js │ ├── project_create.js │ ├── project_detail.js │ ├── projects.js │ ├── user_create.js │ ├── user_hosts.js │ ├── user_projects.js │ └── users.js ├── templates ├── account_change_password.html ├── blank.html ├── deploy_create.html ├── deploy_progress.html ├── deploys.html ├── host_create.html ├── host_detail.html ├── hosts.html ├── login.html ├── nav.html ├── project_create.html ├── project_detail.html ├── projects.html ├── user_create.html ├── user_hosts.html ├── user_projects.html └── users.html └── utils ├── __init__.py ├── error.py ├── git.py ├── jsonencoder.py ├── localshell.py ├── log.py ├── mysql.py └── remoteshell.py /.gitignore: -------------------------------------------------------------------------------- 1 | test.py 2 | grouplus 3 | grouplus.py 4 | *.swp 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | env/ 17 | env3/ 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *,cover 52 | .hypothesis/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | 61 | # Sphinx documentation 62 | docs/_build/ 63 | 64 | # PyBuilder 65 | target/ 66 | 67 | #Ipython Notebook 68 | .ipynb_checkpoints 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pydelo - A Deploy Tool 2 | ====================== 3 | 这是一个Python语言编写的自动化上线部署系统,只需做很少的配置就可以立即使用。 4 | 系统将整个发布过程分成两个部分:checkout 和 deploy 5 | * checkout 6 | 此部分做代码的检出动作,并且在代码的检出前后可以分别做一些shell操作,如编译动作,配置文件修改等。 7 | * deploy 8 | 此部分做代码的发布动作,通过rsync将代码同步到远端机器的指定目录,在代码的同步前后也可以分别做一些shell操作,如相关服务的stop、start,某些清理工作等。 9 | 10 | Requirements 11 | ------------ 12 | 13 | * Bash(git, rsync, ssh, sshpass) 14 | * MySQL 15 | * Python 16 | * Python site-package(flask, flask-sqlalchemy, pymysql, paramiko) 17 | 18 | That's all. 19 | 20 | Installation 21 | ------------ 22 | ``` 23 | apt-get install rsync sshpass 24 | git clone git@github.com:meanstrong/pydelo.git 25 | cd pydelo 26 | pip install -r pip_requirements.txt # 建议使用virtualenv来部署 27 | mysql -h localhost -u root -p pydelo < db-schema/db-schema.sql # create database and tables 28 | vi web/config.py # set up module config such as mysql connector 29 | python init.py # 添加默认用户、项目数据 30 | 31 | python manage.py # start flask web app 32 | ``` 33 | 34 | Usage 35 | ----- 36 | #### 1.Add project 37 | ![image](https://github.com/meanstrong/pydelo/raw/master/docs/create_project.png) 38 | 39 | #### 2.New deploy 40 | ![image](https://github.com/meanstrong/pydelo/raw/master/docs/create_deploy.png) 41 | 42 | #### 3.Deploy progress 43 | ![image](https://github.com/meanstrong/pydelo/raw/master/docs/deploy_progress.png) 44 | 45 | #### 4.Deploys 46 | ![image](https://github.com/meanstrong/pydelo/raw/master/docs/deploys.png) 47 | 48 | Discussing 49 | ---------- 50 | - email: pmq2008@gmail.com 51 | 52 | 53 | Todo 54 | ---------- 55 | 1. BUG:同一个git项目不能同时有两次部署,因为在部署时会对checkout代码进行reset&clean,这样会相互之间造成影响 56 | 2. rc错误码完善 57 | 3. 权限控制的更合理,对资源的访问权限需要更加细化 58 | 4. API返回的结果中无须返回数据库中的所有字段,应该按需返回 59 | -------------------------------------------------------------------------------- /WEB API.md: -------------------------------------------------------------------------------- 1 | # 基本说明 2 | 3 | ## API验证 4 | 5 | 目前支持两种方式的验证: 6 | 7 | 1. 在调用login登陆后,会返回一个sign值,在以后的api接口调用中添加到cookie即可,例如: 8 | 9 | ```shell 10 | $ curl -d "username=demo&password=123456" http://127.0.0.1:9998/api/users/login 11 | { 12 | "data": { 13 | "sign": "UnbvXIf06L3wtSwHMw3C" 14 | }, 15 | "rc": 0 16 | } 17 | $curl -b "sign=UnbvXIf06L3wtSwHMw3C" http://127.0.0.1:9998/api/projects 18 | { 19 | "data": { 20 | "count": 1, 21 | "projects": [ 22 | { 23 | ... 24 | } 25 | ] 26 | }, 27 | "rc": 0 28 | } 29 | ``` 30 | 31 | 2. 每个用户都会有一个apikey的字段,把该字段放在url的params里面即可,例如: 32 | 33 | ```shell 34 | $ curl http://127.0.0.1:9998/api/projects?apikey=FWi14sULr0CwdYqhyBwQfbpdSEV7M8dp 35 | { 36 | "data": { 37 | "count": 1, 38 | "projects": [ 39 | { 40 | ... 41 | } 42 | ] 43 | }, 44 | "rc": 0 45 | } 46 | ``` 47 | 48 | 49 | # 登陆 50 | 51 | ### 登陆 52 | ``` 53 | path: /api/login 54 | method: POST 55 | params: 56 | data: 57 | username: 用户名 58 | password: 密码 59 | result: 60 | { 61 | rc: 错误码(0表示OK) 62 | msg: 错误信息 63 | data: 64 | { 65 | sign: 放入cookies中sign字符串 66 | } 67 | } 68 | ``` 69 | # 账户 70 | 71 | ### 更新账户密码 72 | 73 | ``` 74 | path: /api/accounts/password 75 | method: PUT 76 | data: 77 | password: 新密码 78 | result: 79 | { 80 | rc: 错误码(0表示OK) 81 | msg: 错误信息 82 | } 83 | ``` 84 | 85 | 86 | 87 | # 部署(deploys) 88 | 89 | ### 获取所有部署 90 | ``` 91 | path: /api/deploys 92 | method: GET 93 | params: 94 | offset: 数据偏移量 95 | limit: 数据个数 96 | result: 97 | { 98 | rc: 错误码(0表示OK) 99 | msg: 错误信息 100 | data: 101 | { 102 | count: 总数量 103 | deploys: 104 | [ 105 | { 106 | branch: 本次部署的git分支名称 107 | comment: 备注 108 | host_id: 本次部署的host id 109 | id: 本次部署id 110 | mode: 本次部署的方式(0:branch的方式;1:tag的方式;2:回滚的方式) 111 | progress: 本次部署的进度(0 ~ 100) 112 | project_id: 本次部署的project id 113 | softln_filename: 本次部署的软连接文件名 114 | status: 本次部署的status(0:失败;1:成功;2:running) 115 | created_at: 本次部署创建时间 116 | updated_at: 本次部署最近更新时间 117 | user_id: 本次部署的user id 118 | version: 本次部署的version(当mode为branch方式时表示commit,当mode为tag时表示tag) 119 | user: { 120 | apikey: us用户的apikey, 121 | created_at: 用户的创建时间, 122 | email: 用户的邮箱地址, 123 | id: 用户id, 124 | name: 用户名称, 125 | password: 用户密码, 126 | phone: 用户电话, 127 | role: 用户角色, 128 | updated_at: 用户最近更新时间 129 | }, 130 | project: { 131 | after_checkout: project代码checkout之后执行的shell 132 | after_deploy: project部署完成之后执行的shell 133 | after_rollback: project回滚完成之后执行的shell 134 | before_checkout: project代码checkout之前执行的shell 135 | before_rollback: project回滚之前执行的shell 136 | checkout_dir: project代码checkout的地址 137 | created_at: project创建时间 138 | deploy_dir: project部署的地址 139 | deploy_history_dir: project部署的历史版本保存地址 140 | id: project id 141 | name: project名称 142 | repo_url: project的git地址 143 | updated_at: project最近更新时间 144 | }, 145 | host: { 146 | created_at: host创建时间, 147 | id: host id, 148 | name: host名称, 149 | ssh_host: host ssh连接的IP地址, 150 | ssh_pass: host ssh连接的用户密码, 151 | ssh_port: host ssh连接的端口号, 152 | ssh_user: host ssh连接的用户, 153 | updated_at: host最近更新时间 154 | } 155 | }, 156 | ] 157 | } 158 | } 159 | ``` 160 | ### 创建部署 161 | ``` 162 | path: /api/deploys 163 | method: POST 164 | params: 165 | project_id: project id 166 | host_id: host id 167 | data: 168 | mode:部署的方式(0:branch的方式;1:tag的方式;2:回滚的方式) 169 | branch:部署的git分支名称(当mode=0时传此值) 170 | tag:部署的git tag名称(当mode=1时传此值) 171 | commit:部署的git分支commit(当mode=0时传此值) 172 | result: 173 | { 174 | rc: 错误码(0表示OK) 175 | msg: 错误信息 176 | data: 177 | { 178 | id: 本次部署的id 179 | } 180 | } 181 | ``` 182 | ### 重新部署 183 | ``` 184 | path: /api/deploys/:id 185 | method: PUT 186 | params: 187 | :id: 某次部署的id 188 | data: 189 | action:部署的方式("redeploy":完全重新部署;"rollback":回滚至此次部署) 190 | result: 191 | { 192 | rc: 错误码(0表示OK) 193 | msg: 错误信息 194 | data: 195 | { 196 | id: 部署的id 197 | } 198 | } 199 | ``` 200 | ### 获取某次部署的详情 201 | ``` 202 | path: /api/deploys/:id 203 | method: GET 204 | params: 205 | :id: 某次部署的id 206 | result: 207 | { 208 | rc: 错误码(0表示OK) 209 | msg: 错误信息 210 | data: 211 | { 212 | branch: 本次部署的git分支名称 213 | comment: 备注 214 | host_id: 本次部署的host id 215 | id: 本次部署id 216 | mode: 本次部署的方式(0:branch的方式;1:tag的方式;2:回滚的方式) 217 | progress: 本次部署的进度(0 ~ 100) 218 | project_id: 本次部署的project id 219 | softln_filename: 本次部署的软连接文件名 220 | status: 本次部署的status(0:失败;1:成功;2:running) 221 | created_at: 本次部署创建时间 222 | updated_at: 本次部署最近更新时间 223 | user_id: 本次部署的user id 224 | version: 本次部署的version(当mode为branch方式时表示commit,当mode为tag时表示tag) 225 | user: { 226 | apikey: us用户的apikey, 227 | created_at: 用户的创建时间, 228 | email: 用户的邮箱地址, 229 | id: 用户id, 230 | name: 用户名称, 231 | password: 用户密码, 232 | phone: 用户电话, 233 | role: 用户角色, 234 | updated_at: 用户最近更新时间 235 | }, 236 | project: { 237 | after_checkout: project代码checkout之后执行的shell 238 | after_deploy: project部署完成之后执行的shell 239 | after_rollback: project回滚完成之后执行的shell 240 | before_checkout: project代码checkout之前执行的shell 241 | before_rollback: project回滚之前执行的shell 242 | checkout_dir: project代码checkout的地址 243 | created_at: project创建时间 244 | deploy_dir: project部署的地址 245 | deploy_history_dir: project部署的历史版本保存地址 246 | id: project id 247 | name: project名称 248 | repo_url: project的git地址 249 | updated_at: project最近更新时间 250 | }, 251 | host: { 252 | created_at: host创建时间, 253 | id: host id, 254 | name: host名称, 255 | ssh_host: host ssh连接的IP地址, 256 | ssh_pass: host ssh连接的用户密码, 257 | ssh_port: host ssh连接的端口号, 258 | ssh_user: host ssh连接的用户, 259 | updated_at: host最近更新时间 260 | } 261 | }, 262 | } 263 | ``` 264 | # 项目(projects) 265 | 266 | ### 获取所有项目 267 | 268 | ``` 269 | path: /api/projects 270 | method: GET 271 | params: 272 | offset: 数据偏移量 273 | limit: 数据个数 274 | result: 275 | { 276 | rc: 错误码(0表示OK) 277 | msg: 错误信息 278 | data: [ 279 | { 280 | after_checkout: project代码checkout之后执行的shell 281 | after_deploy: project部署完成之后执行的shell 282 | after_rollback: project回滚完成之后执行的shell 283 | before_checkout: project代码checkout之前执行的shell 284 | before_rollback: project回滚之前执行的shell 285 | checkout_dir: project代码checkout的地址 286 | created_at: project创建时间 287 | deploy_dir: project部署的地址 288 | deploy_history_dir: project部署的历史版本保存地址 289 | id: project id 290 | name: project名称 291 | repo_url: project的git地址 292 | updated_at: project最近更新时间 293 | }, 294 | ... 295 | ] 296 | } 297 | ``` 298 | 299 | ### 创建项目 300 | 301 | ``` 302 | path: /api/projects 303 | method: POST 304 | params: 305 | after_checkout: project代码checkout之后执行的shell 306 | after_deploy: project部署完成之后执行的shell 307 | after_rollback: project回滚完成之后执行的shell 308 | before_checkout: project代码checkout之前执行的shell 309 | before_rollback: project回滚之前执行的shell 310 | checkout_dir: project代码checkout的地址 311 | deploy_dir: project部署的地址 312 | deploy_history_dir: project部署的历史版本保存地址 313 | name: project名称 314 | repo_url: project的git地址 315 | result: 316 | { 317 | rc: 错误码(0表示OK) 318 | msg: 错误信息 319 | } 320 | ``` 321 | 322 | ### 获取某个项目 323 | 324 | ``` 325 | path: /api/projects/:id 326 | method: GET 327 | params: 328 | :id 项目id 329 | result: 330 | { 331 | rc: 错误码(0表示OK) 332 | msg: 错误信息 333 | data: 334 | { 335 | after_checkout: project代码checkout之后执行的shell 336 | after_deploy: project部署完成之后执行的shell 337 | after_rollback: project回滚完成之后执行的shell 338 | before_checkout: project代码checkout之前执行的shell 339 | before_rollback: project回滚之前执行的shell 340 | checkout_dir: project代码checkout的地址 341 | created_at: project创建时间 342 | deploy_dir: project部署的地址 343 | deploy_history_dir: project部署的历史版本保存地址 344 | id: project id 345 | name: project名称 346 | repo_url: project的git地址 347 | updated_at: project最近更新时间 348 | } 349 | } 350 | ``` 351 | 352 | ### 更新某个项目 353 | 354 | ``` 355 | path: /api/projects/:id 356 | method: PUT 357 | params: 358 | :id 项目id 359 | after_checkout: project代码checkout之后执行的shell 360 | after_deploy: project部署完成之后执行的shell 361 | after_rollback: project回滚完成之后执行的shell 362 | before_checkout: project代码checkout之前执行的shell 363 | before_rollback: project回滚之前执行的shell 364 | checkout_dir: project代码checkout的地址 365 | deploy_dir: project部署的地址 366 | deploy_history_dir: project部署的历史版本保存地址 367 | name: project名称 368 | repo_url: project的git地址 369 | result: 370 | { 371 | rc: 错误码(0表示OK) 372 | msg: 错误信息 373 | } 374 | ``` 375 | 376 | ### 获取某个项目的git分支列表 377 | 378 | ``` 379 | path: /api/projects/:id/branches 380 | method: GET 381 | params: 382 | :id 项目id 383 | offset: 数据偏移量 384 | limit: 数据个数 385 | result: 386 | { 387 | rc: 错误码(0表示OK) 388 | msg: 错误信息 389 | data: 分支列表,如:["master", "dev"] 390 | } 391 | ``` 392 | 393 | ### 获取某个项目的git某个分支的commit列表 394 | 395 | ``` 396 | path: /api/projects/:id/branches/:branch/commits 397 | method: GET 398 | params: 399 | :id 项目id 400 | :branch 分支名称 401 | offset: 数据偏移量 402 | limit: 数据个数 403 | result: 404 | { 405 | rc: 错误码(0表示OK) 406 | msg: 错误信息 407 | data: [ 408 | { 409 | abbreviated_commit 简短commit 410 | author_name 提交commit的author名 411 | subject 提交commit的备注 412 | }, 413 | ... 414 | ] 415 | } 416 | ``` 417 | 418 | ### 获取某个项目的git tag列表 419 | 420 | ``` 421 | path: /api/projects/:id/tags 422 | method: GET 423 | params: 424 | :id 项目id 425 | offset: 数据偏移量 426 | limit: 数据个数 427 | result: 428 | { 429 | rc: 错误码(0表示OK) 430 | msg: 错误信息 431 | data: tag列表,如["v1.0", "v2.0"] 432 | } 433 | ``` 434 | 435 | # 主机(hosts) 436 | 437 | ### 创建主机 438 | 439 | ``` 440 | path: /api/hosts 441 | method: POST 442 | params: 443 | name: host名称, 444 | ssh_host: host ssh连接的IP地址, 445 | ssh_pass: host ssh连接的用户密码, 446 | ssh_port: host ssh连接的端口号, 447 | ssh_user: host ssh连接的用户, 448 | result: 449 | { 450 | rc: 错误码(0表示OK) 451 | msg: 错误信息 452 | } 453 | ``` 454 | 455 | ### 获取所有主机列表 456 | 457 | ``` 458 | path: /api/hosts 459 | method: GET 460 | params: 461 | offset: 数据偏移量 462 | limit: 数据个数 463 | result: 464 | { 465 | rc: 错误码(0表示OK) 466 | msg: 错误信息 467 | data: 468 | [ 469 | { 470 | created_at: host创建时间, 471 | id: host id, 472 | name: host名称, 473 | ssh_host: host ssh连接的IP地址, 474 | ssh_pass: host ssh连接的用户密码, 475 | ssh_port: host ssh连接的端口号, 476 | ssh_user: host ssh连接的用户, 477 | updated_at: host最近更新时间 478 | }, 479 | ... 480 | ] 481 | } 482 | ``` 483 | 484 | ### 获取某个主机 485 | 486 | ``` 487 | path: /api/hosts/:id 488 | method: GET 489 | params: 490 | :id 主机id 491 | offset: 数据偏移量 492 | limit: 数据个数 493 | result: 494 | { 495 | rc: 错误码(0表示OK) 496 | msg: 错误信息 497 | data: { 498 | created_at: host创建时间, 499 | id: host id, 500 | name: host名称, 501 | ssh_host: host ssh连接的IP地址, 502 | ssh_pass: host ssh连接的用户密码, 503 | ssh_port: host ssh连接的端口号, 504 | ssh_user: host ssh连接的用户, 505 | updated_at: host最近更新时间 506 | }, 507 | ... 508 | } 509 | ``` 510 | 511 | ### 更新某个主机 512 | 513 | ``` 514 | path: /api/hosts/:id 515 | method: PUT 516 | params: 517 | name: host名称, 518 | ssh_host: host ssh连接的IP地址, 519 | ssh_pass: host ssh连接的用户密码, 520 | ssh_port: host ssh连接的端口号, 521 | ssh_user: host ssh连接的用户, 522 | result: 523 | { 524 | rc: 错误码(0表示OK) 525 | msg: 错误信息 526 | } 527 | ``` 528 | 529 | # 用户(users) 530 | 531 | ### 创建用户 532 | 533 | ``` 534 | path: /api/users 535 | method: POST 536 | params: 537 | email: 用户的邮箱地址, 538 | name: 用户名称, 539 | phone: 用户电话, 540 | role: 用户角色, 541 | result: 542 | { 543 | rc: 错误码(0表示OK) 544 | msg: 错误信息 545 | } 546 | ``` 547 | 548 | ### 获取所有用户 549 | 550 | ``` 551 | path: /api/users 552 | method: GET 553 | params: 554 | offset: 数据偏移量 555 | limit: 数据个数 556 | result: 557 | { 558 | rc: 错误码(0表示OK) 559 | msg: 错误信息 560 | data: 561 | [ 562 | { 563 | apikey: us用户的apikey, 564 | created_at: 用户的创建时间, 565 | email: 用户的邮箱地址, 566 | id: 用户id, 567 | name: 用户名称, 568 | password: 用户密码, 569 | phone: 用户电话, 570 | role: 用户角色, 571 | updated_at: 用户最近更新时间 572 | }, 573 | ... 574 | ] 575 | } 576 | ``` 577 | 578 | ### 获取某个用户 579 | 580 | ``` 581 | path: /api/users/:id 582 | method: GET 583 | params: 584 | :id 用户id 585 | result: 586 | { 587 | rc: 错误码(0表示OK) 588 | msg: 错误信息 589 | data: 590 | { 591 | apikey: us用户的apikey, 592 | created_at: 用户的创建时间, 593 | email: 用户的邮箱地址, 594 | id: 用户id, 595 | name: 用户名称, 596 | password: 用户密码, 597 | phone: 用户电话, 598 | role: 用户角色, 599 | updated_at: 用户最近更新时间 600 | }, 601 | } 602 | ``` 603 | 604 | ### 获取用户拥有的主机列表 605 | 606 | ``` 607 | path: /api/users/:id/hosts 608 | method: GET 609 | params: 610 | :id 用户id 611 | result: 612 | { 613 | rc: 错误码(0表示OK) 614 | msg: 错误信息 615 | data: 616 | [ 617 | { 618 | created_at: host创建时间, 619 | id: host id, 620 | name: host名称, 621 | ssh_host: host ssh连接的IP地址, 622 | ssh_pass: host ssh连接的用户密码, 623 | ssh_port: host ssh连接的端口号, 624 | ssh_user: host ssh连接的用户, 625 | updated_at: host最近更新时间 626 | }, 627 | ... 628 | ] 629 | } 630 | ``` 631 | 632 | ### 更新用户所拥有的主机列表 633 | 634 | ``` 635 | path: /api/users/:id/hosts 636 | method: PUT 637 | params: 638 | :id 用户id 639 | data: 640 | hosts[] 主机id列表 641 | result: 642 | { 643 | rc: 错误码(0表示OK) 644 | msg: 错误信息 645 | } 646 | ``` 647 | 648 | ### 获取用户拥有的项目列表 649 | 650 | ``` 651 | path: /api/users/:id/projects 652 | method: GET 653 | params: 654 | :id 用户id 655 | result: 656 | { 657 | rc: 错误码(0表示OK) 658 | msg: 错误信息 659 | data: 660 | [ 661 | { 662 | after_checkout: project代码checkout之后执行的shell 663 | after_deploy: project部署完成之后执行的shell 664 | after_rollback: project回滚完成之后执行的shell 665 | before_checkout: project代码checkout之前执行的shell 666 | before_rollback: project回滚之前执行的shell 667 | checkout_dir: project代码checkout的地址 668 | created_at: project创建时间 669 | deploy_dir: project部署的地址 670 | deploy_history_dir: project部署的历史版本保存地址 671 | id: project id 672 | name: project名称 673 | repo_url: project的git地址 674 | updated_at: project最近更新时间 675 | }, 676 | ... 677 | ] 678 | } 679 | ``` 680 | 681 | ### 更新用户所拥有的项目列表 682 | 683 | ``` 684 | path: /api/users/:id/projects 685 | method: PUT 686 | params: 687 | :id 用户id 688 | data: 689 | projects[] 项目id列表 690 | result: 691 | { 692 | rc: 错误码(0表示OK) 693 | msg: 错误信息 694 | } 695 | ``` 696 | 697 | -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | WORKSPACE=$(cd $(dirname $0)/; pwd) 4 | cd $WORKSPACE 5 | 6 | mkdir -p var 7 | 8 | app=pydelo 9 | pidfile=var/$app.pid 10 | logfile=var/$app.log 11 | 12 | function check_pid() { 13 | if [ -f $pidfile ];then 14 | pid=`cat $pidfile` 15 | if [ -n $pid ]; then 16 | running=`ps -p $pid|grep -v "PID TTY" |wc -l` 17 | return $running 18 | fi 19 | fi 20 | return 0 21 | } 22 | 23 | function start() { 24 | source env/bin/activate 25 | check_pid 26 | running=$? 27 | if [ $running -gt 0 ];then 28 | echo -n "$app now is running already, pid=" 29 | cat $pidfile 30 | return 1 31 | fi 32 | 33 | gunicorn -w 2 -b 0.0.0.0:9998 manage:app -D -t 6000 --pid $pidfile --capture-output --error-logfile var/error.log --log-level debug --log-file=$logfile &> $logfile 2>&1 34 | sleep 1 35 | echo -n "$app started..., pid=" 36 | cat $pidfile 37 | } 38 | 39 | function stop() { 40 | pid=`cat $pidfile` 41 | kill $pid 42 | echo "$app quit..." 43 | } 44 | 45 | function kill9() { 46 | pid=`cat $pidfile` 47 | kill -9 $pid 48 | echo "$app stoped..." 49 | } 50 | 51 | function restart() { 52 | stop 53 | sleep 2 54 | start 55 | } 56 | 57 | function status() { 58 | check_pid 59 | running=$? 60 | if [ $running -gt 0 ];then 61 | echo -n "$app now is running, pid=" 62 | cat $pidfile 63 | else 64 | echo "$app is stoped" 65 | fi 66 | } 67 | 68 | function tailf() { 69 | tail -f $logfile 70 | } 71 | 72 | function help() { 73 | echo "$0 start|stop|restart|status|tail|kill9|version|pack" 74 | } 75 | 76 | if [ "$1" == "" ]; then 77 | help 78 | elif [ "$1" == "stop" ];then 79 | stop 80 | elif [ "$1" == "kill9" ];then 81 | kill9 82 | elif [ "$1" == "start" ];then 83 | start 84 | elif [ "$1" == "restart" ];then 85 | restart 86 | elif [ "$1" == "status" ];then 87 | status 88 | elif [ "$1" == "tail" ];then 89 | tailf 90 | elif [ "$1" == "pack" ];then 91 | pack 92 | elif [ "$1" == "version" ];then 93 | show_version 94 | else 95 | help 96 | fi 97 | -------------------------------------------------------------------------------- /db-schema/db-schema-upgrade_v1.0.0_to_v2.0.0.sql: -------------------------------------------------------------------------------- 1 | USE pydelo; 2 | ALTER TABLE `projects` ADD `target_dir` varchar(200) NOT NULL COMMENT '部署文件build后目标目录' AFTER `checkout_dir`; 3 | UPDATE `projects` SET `target_dir` = `checkout_dir`; 4 | ALTER TABLE `projects` DROP COLUMN `before_rollback`; 5 | ALTER TABLE `projects` DROP COLUMN `after_rollback`; 6 | ALTER TABLE `deploys` CHANGE `status` `status` int(1) unsigned NOT NULL COMMENT '0-fail;1-success;2-running;3-waiting'; 7 | 8 | ALTER TABLE `hosts` ADD `ssh_method` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '0-password;1-public key' AFTER `ssh_user`; 9 | -------------------------------------------------------------------------------- /db-schema/db-schema.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE if exists pydelo; 2 | create database pydelo; 3 | use pydelo; 4 | set names utf8; 5 | 6 | DROP TABLE if exists `sessions`; 7 | CREATE TABLE `sessions` ( 8 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 9 | `user_id` int(10) unsigned NOT NULL, 10 | `session` varchar(32) NOT NULL, 11 | `expired` TIMESTAMP NOT NULL COMMENT 'expired time', 12 | `created_at` datetime NOT NULL COMMENT 'create time', 13 | `updated_at` datetime NOT NULL COMMENT 'update time', 14 | PRIMARY KEY (`id`) 15 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 16 | 17 | DROP TABLE if exists `users`; 18 | CREATE TABLE `users` ( 19 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 20 | `name` varchar(32) NOT NULL, 21 | `password` varchar(64) NOT NULL, 22 | `role` int(10) unsigned DEFAULT NULL, 23 | `email` varchar(64) DEFAULT '', 24 | `phone` varchar(16) DEFAULT '', 25 | `apikey` varchar(64) NOT NULL, 26 | `created_at` datetime NOT NULL COMMENT 'create time', 27 | `updated_at` datetime NOT NULL COMMENT 'update time', 28 | PRIMARY KEY (`id`), 29 | UNIQUE KEY `name_key` (`name`) 30 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 31 | 32 | DROP TABLE if exists `projects`; 33 | CREATE TABLE `projects` ( 34 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 35 | `name` varchar(64) NOT NULL COMMENT '项目名称', 36 | `repo_url` varchar(200) NOT NULL COMMENT '项目git库地址', 37 | `checkout_dir` varchar(200) NOT NULL COMMENT '源代码检出目录', 38 | `target_dir` varchar(200) NOT NULL COMMENT '部署文件存放目录', 39 | `deploy_dir` varchar(200) NOT NULL COMMENT '部署机器上的目标目录', 40 | `deploy_history_dir` varchar(200) NOT NULL COMMENT '部署机器上的历史版本目录', 41 | `before_checkout` text , 42 | `after_checkout` text , 43 | `before_deploy` text , 44 | `after_deploy` text , 45 | `created_at` datetime NOT NULL COMMENT 'create time', 46 | `updated_at` datetime NOT NULL COMMENT 'update time', 47 | PRIMARY KEY (`id`) 48 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 49 | 50 | DROP TABLE if exists `hosts`; 51 | CREATE TABLE `hosts` ( 52 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 53 | `name` varchar(64) NOT NULL, 54 | `ssh_host` varchar(32) NOT NULL, 55 | `ssh_port` int(10) unsigned NOT NULL, 56 | `ssh_user` varchar(64) NOT NULL, 57 | `ssh_pass` varchar(100) NOT NULL, 58 | `created_at` datetime NOT NULL COMMENT 'create time', 59 | `updated_at` datetime NOT NULL COMMENT 'update time', 60 | PRIMARY KEY (`id`), 61 | UNIQUE KEY `name` (`name`) 62 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 63 | 64 | DROP TABLE if exists `rel_user_project`; 65 | CREATE TABLE `rel_user_project` ( 66 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 67 | `user_id` int(10) unsigned NOT NULL, 68 | `project_id` int(10) unsigned NOT NULL, 69 | `created_at` datetime NOT NULL COMMENT 'create time', 70 | `updated_at` datetime NOT NULL COMMENT 'update time', 71 | PRIMARY KEY (`id`) 72 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 73 | 74 | DROP TABLE if exists `rel_user_host`; 75 | CREATE TABLE `rel_user_host` ( 76 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 77 | `user_id` int(10) unsigned NOT NULL, 78 | `host_id` int(10) unsigned NOT NULL, 79 | `created_at` datetime NOT NULL COMMENT 'create time', 80 | `updated_at` datetime NOT NULL COMMENT 'update time', 81 | PRIMARY KEY (`id`) 82 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 83 | 84 | DROP TABLE if exists `deploys`; 85 | CREATE TABLE `deploys` ( 86 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 87 | `user_id` int(10) unsigned NOT NULL, 88 | `project_id` int(10) unsigned NOT NULL, 89 | `host_id` int(10) unsigned NOT NULL, 90 | `mode` int(10) unsigned NOT NULL COMMENT '0:branch;1:tag;2:rollback', 91 | `branch` varchar(32) NOT NULL, 92 | `version` varchar(32) NOT NULL, 93 | `progress` int(10) unsigned NOT NULL COMMENT '', 94 | `status` int(1) unsigned NOT NULL COMMENT '0-fail;1-success;2-running;3-waiting', 95 | `softln_filename` varchar(64) NOT NULL, 96 | `comment` text , 97 | `created_at` datetime NOT NULL COMMENT 'create time', 98 | `updated_at` datetime NOT NULL COMMENT 'update time', 99 | PRIMARY KEY (`id`) 100 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 101 | -------------------------------------------------------------------------------- /docs/create_deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meanstrong/pydelo/74ba65b4150479ff6a4b8aec3c83401d24022056/docs/create_deploy.png -------------------------------------------------------------------------------- /docs/create_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meanstrong/pydelo/74ba65b4150479ff6a4b8aec3c83401d24022056/docs/create_project.png -------------------------------------------------------------------------------- /docs/deploy_progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meanstrong/pydelo/74ba65b4150479ff6a4b8aec3c83401d24022056/docs/deploy_progress.png -------------------------------------------------------------------------------- /docs/deploys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meanstrong/pydelo/74ba65b4150479ff6a4b8aec3c83401d24022056/docs/deploys.png -------------------------------------------------------------------------------- /init.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | from hashlib import md5 4 | from web.services.users import users 5 | from web.services.projects import projects 6 | 7 | users.create(name="root", 8 | password=md5("123456".encode("utf-8")).hexdigest().upper(), 9 | apikey=''.join(random.choice(string.letters+string.digits) for _ 10 | in range(32)), 11 | role=1) 12 | users.create(name="demo", 13 | password=md5("123456".encode("utf-8")).hexdigest().upper(), 14 | apikey=''.join(random.choice(string.letters+string.digits) for _ 15 | in range(32))) 16 | projects.create(name="pydelo", 17 | repo_url="https://github.com/meanstrong/pydelo", 18 | checkout_dir="/data/home/rocky/pydelo/test/checkout", 19 | target_dir="/data/home/rocky/pydelo/test/target", 20 | deploy_dir="/data/home/rocky/pydelo/test/deploy", 21 | deploy_history_dir="/data/home/rocky/pydelo/history") 22 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from web import app 3 | 4 | __author__ = 'Rocky Peng' 5 | 6 | if __name__ == '__main__': 7 | app.run(host='0.0.0.0', port=app.config["PORT"], debug=app.config["DEBUG"]) 8 | -------------------------------------------------------------------------------- /pip_requirements.txt: -------------------------------------------------------------------------------- 1 | flask==0.10.1 2 | jinja2==2.8 3 | werkzeug==0.11.8 4 | flask-sqlalchemy==2.1 5 | paramiko==1.16.0 6 | pymysql==0.7.2 7 | -------------------------------------------------------------------------------- /web/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from flask import Flask 3 | from flask.ext.sqlalchemy import SQLAlchemy 4 | 5 | from web.utils.jsonencoder import JSONEncoder 6 | from web.utils.log import Logger 7 | 8 | __author__ = 'Rocky Peng' 9 | 10 | 11 | app = Flask(__name__) 12 | app.config.from_object("web.config") 13 | Logger.DEBUG_MODE = app.config["DEBUG"] 14 | app.config['SQLALCHEMY_DATABASE_URI'] = ('mysql+pymysql://{0}:{1}@{2}:{3}/{4}' 15 | ).format(app.config["DB_USER"], 16 | app.config["DB_PASS"], 17 | app.config["DB_HOST"], 18 | app.config["DB_PORT"], 19 | app.config["DB_NAME"]) 20 | app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True 21 | app.json_encoder = JSONEncoder 22 | db = SQLAlchemy(app) 23 | db_session = db.session 24 | 25 | 26 | from .controller import api, webhooks, login, deploys, project, host, users 27 | -------------------------------------------------------------------------------- /web/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | __author__ = 'Rocky Peng' 3 | 4 | # -- app config -- 5 | DEBUG = True 6 | PORT = 9998 7 | 8 | # -- mysql config -- 9 | DB_HOST = "127.0.0.1" 10 | DB_PORT = 3306 11 | DB_USER = "root" 12 | DB_PASS = "abc$#@!8008CBA" 13 | DB_NAME = "pydelo" 14 | 15 | # -- web app config -- 16 | # deploy host上保存的最大历史版本数量 17 | MAX_DEPLOY_HISTORY = 30 18 | -------------------------------------------------------------------------------- /web/controller/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | __author__ = 'Rocky Peng' 3 | -------------------------------------------------------------------------------- /web/controller/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import traceback 3 | import json 4 | import time 5 | import random 6 | import string 7 | import sys 8 | from hashlib import md5 9 | 10 | from flask import request, jsonify, g 11 | 12 | from web import db 13 | from web import app 14 | from web.services.users import users 15 | from web.services.hosts import hosts 16 | from web.services.deploys import deploys 17 | from web.services.projects import projects 18 | from web.utils.error import Error 19 | from .login import authorize 20 | 21 | from web.utils.log import Logger 22 | logger = Logger("web.controller.api") 23 | if sys.version_info > (3,): 24 | string.letters = string.ascii_letters 25 | 26 | __author__ = 'Rocky Peng' 27 | 28 | 29 | @app.errorhandler(Error) 30 | def error(err): 31 | return jsonify(dict(rc=err.rc, msg=err.msg)) 32 | 33 | 34 | @app.route("/api/accounts/password", methods=["PUT"]) 35 | @authorize 36 | def api_update_accounts(): 37 | password = request.form.get("password") 38 | password = md5(password.encode("utf-8")).hexdigest().upper() 39 | users.update(g.user, password=password) 40 | return jsonify(dict(rc=0)) 41 | 42 | 43 | @app.route("/api/login", methods=["POST"]) 44 | def api_user_login(): 45 | username = request.form.get("username") 46 | password = request.form.get("password") 47 | sign = users.login(username, password) 48 | return jsonify(dict(rc=0, data=dict(sign=sign))) 49 | 50 | 51 | @app.route("/api/deploys", methods=["GET"]) 52 | @authorize 53 | def api_deploys(): 54 | deploys.session_commit() 55 | offset = request.args.get("offset", None, type=int) 56 | limit = request.args.get("limit", None, type=int) 57 | if g.user.role == g.user.ROLE["ADMIN"]: 58 | return jsonify( 59 | dict(rc=0, 60 | data=dict(deploys=deploys.all( 61 | offset, 62 | limit, 63 | order_by="created_at", 64 | desc=True), 65 | count=deploys.count()))) 66 | else: 67 | return jsonify( 68 | dict(rc=0, 69 | data=dict(deploys=deploys.find(user_id=g.user.id) 70 | .order_by(db.desc("created_at")).limit(limit) 71 | .offset(offset).all(), 72 | count=deploys.count(user_id=g.user.id)))) 73 | 74 | 75 | @app.route("/api/deploys", methods=["POST"]) 76 | @authorize 77 | def api_post_deploy(): 78 | project_id = request.args.get("project_id") 79 | host_id = request.args.get("host_id") 80 | mode = request.form.get("mode", type=int) 81 | branch = request.form.get("branch") if mode == 0 else "" 82 | tag = request.form.get("tag") 83 | commit = request.form.get("commit") if mode == 0 else tag 84 | deploy = deploys.create( 85 | user_id=g.user.id, 86 | project_id=project_id, 87 | host_id=host_id, 88 | mode=mode, 89 | status=3, 90 | branch=branch, 91 | version=commit, 92 | softln_filename=time.strftime("%Y%m%d-%H%M%S") + "-" + commit, 93 | ) 94 | deploys.deploy(deploy) 95 | return jsonify(dict(rc=0, data=dict(id=deploy.id))) 96 | 97 | 98 | @app.route("/api/deploys/", methods=["PUT"]) 99 | @authorize 100 | def update_deploy_by_id(id): 101 | action = request.form.get("action") 102 | deploy = deploys.get(id) 103 | if action == "redeploy": 104 | new_deploy = deploys.create( 105 | user_id=deploy.user_id, 106 | project_id=deploy.project_id, 107 | host_id=deploy.host_id, 108 | mode=deploy.mode, 109 | status=3, 110 | branch=deploy.branch, 111 | version=deploy.version, 112 | softln_filename=deploy.softln_filename) 113 | deploys.deploy(new_deploy) 114 | return jsonify(dict(rc=0, data=dict(id=new_deploy.id))) 115 | elif action == "rollback": 116 | new_deploy = deploys.create( 117 | user_id=deploy.user_id, 118 | project_id=deploy.project_id, 119 | host_id=deploy.host_id, 120 | mode=2, 121 | status=3, 122 | branch=deploy.branch, 123 | version=deploy.version, 124 | softln_filename=deploy.softln_filename) 125 | deploys.rollback(new_deploy) 126 | return jsonify(dict(rc=0, data=dict(id=new_deploy.id))) 127 | else: 128 | raise Error(10000, msg=None) 129 | 130 | 131 | @app.route("/api/deploys/", methods=["GET"]) 132 | @authorize 133 | def get_deploy_progress_by_id(id): 134 | deploys.session_commit() 135 | deploy = deploys.get(id) 136 | return jsonify(dict(rc=0, data=deploy)) 137 | 138 | 139 | @app.route("/api/projects", methods=["GET"]) 140 | @authorize 141 | def api_projects(): 142 | offset = request.args.get("offset", None, type=int) 143 | limit = request.args.get("limit", None, type=int) 144 | data = users.get_user_projects(g.user, offset=offset, limit=limit, 145 | order_by="name") 146 | return jsonify(dict(rc=0, data=data)) 147 | 148 | 149 | @app.route("/api/projects", methods=["POST"]) 150 | @authorize 151 | def api_create_project(): 152 | projects.create(**request.form.to_dict()) 153 | return jsonify(dict(rc=0)) 154 | 155 | 156 | @app.route("/api/projects/", methods=["GET"]) 157 | @authorize 158 | def api_get_project_by_id(id): 159 | return jsonify(dict(rc=0, data=projects.get(id))) 160 | 161 | 162 | @app.route("/api/projects/", methods=["PUT"]) 163 | @authorize 164 | def api_update_project_by_id(id): 165 | projects.update(projects.get(id), **request.form.to_dict()) 166 | return jsonify(dict(rc=0)) 167 | 168 | 169 | @app.route("/api/projects//branches", methods=["GET"]) 170 | @authorize 171 | def api_project_branches(id): 172 | project = projects.get(id) 173 | projects.git_clone(project) 174 | return jsonify(dict(rc=0, data=projects.git_branch(project))) 175 | 176 | 177 | @app.route("/api/projects//tags", methods=["GET"]) 178 | @authorize 179 | def api_project_tags(id): 180 | project = projects.get(id) 181 | projects.git_clone(project) 182 | return jsonify(dict(rc=0, data=projects.git_tag(project))) 183 | 184 | 185 | @app.route("/api/projects//branches//commits", methods=["GET"]) 186 | @authorize 187 | def api_project_branch_commits(id, branch): 188 | project = projects.get(id) 189 | projects.git_clone(project) 190 | return jsonify(dict(rc=0, 191 | data=projects.git_branch_commit_log(project, branch))) 192 | 193 | 194 | # 获取所有hosts 195 | @app.route("/api/hosts", methods=["GET"]) 196 | @authorize 197 | def api_hosts(): 198 | offset = request.args.get("offset", None, type=int) 199 | limit = request.args.get("limit", None, type=int) 200 | data = users.get_user_hosts(g.user, offset=offset, limit=limit) 201 | return jsonify(dict(rc=0, data=data)) 202 | 203 | 204 | # 获取某个host 205 | @app.route("/api/hosts/", methods=["GET"]) 206 | @authorize 207 | def api_get_host_by_id(id): 208 | return jsonify(dict(rc=0, data=hosts.get(id))) 209 | 210 | 211 | # 更新某个host 212 | @app.route("/api/hosts/", methods=["PUT"]) 213 | @authorize 214 | def api_update_host_by_id(id): 215 | hosts.update(hosts.get(id), **request.form.to_dict()) 216 | return jsonify(dict(rc=0)) 217 | 218 | 219 | # 新建host 220 | @app.route("/api/hosts", methods=["POST"]) 221 | @authorize 222 | def create_hosts(): 223 | hosts.create(**request.form.to_dict()) 224 | return jsonify(dict(rc=0)) 225 | 226 | 227 | @app.route("/api/users", methods=["POST"]) 228 | @authorize 229 | def create_users(): 230 | apikey = ''.join( 231 | random.choice(string.letters+string.digits) for _ in range(32)) 232 | user_params = request.form.to_dict() 233 | user_params["password"] = \ 234 | md5(user_params["password"].encode("utf-8")).hexdigest().upper() 235 | users.create(apikey=apikey, **user_params) 236 | return jsonify(dict(rc=0)) 237 | 238 | 239 | @app.route("/api/users", methods=["GET"]) 240 | @authorize 241 | def api_users(): 242 | offset = request.args.get("offset", None, type=int) 243 | limit = request.args.get("limit", None, type=int) 244 | return jsonify(dict(rc=0, 245 | data=dict(users=users.all(offset, limit), 246 | count=users.count()))) 247 | 248 | 249 | @app.route("/api/users/", methods=["GET"]) 250 | @authorize 251 | def api_get_user_by_id(id): 252 | return jsonify(dict(rc=0, data=users.get(id))) 253 | 254 | 255 | @app.route("/api/users//hosts", methods=["GET"]) 256 | @authorize 257 | def api_get_user_hosts_by_id(id): 258 | user = users.get(id) 259 | data = users.get_user_hosts(user) 260 | return jsonify(dict(rc=0, data=data)) 261 | 262 | 263 | @app.route("/api/users//hosts", methods=["PUT"]) 264 | @authorize 265 | def api_update_user_hosts_by_id(id): 266 | user = users.get(id) 267 | user.hosts = [] 268 | for host in request.form.getlist("hosts[]"): 269 | user.hosts.append(hosts.get(int(host))) 270 | users.save(user) 271 | return jsonify(dict(rc=0)) 272 | 273 | 274 | @app.route("/api/users//projects", methods=["GET"]) 275 | @authorize 276 | def api_get_user_projects_by_id(id): 277 | user = users.get(id) 278 | data = users.get_user_projects(user) 279 | return jsonify(dict(rc=0, data=data)) 280 | 281 | 282 | @app.route("/api/users//projects", methods=["PUT"]) 283 | @authorize 284 | def api_update_user_projects_by_id(id): 285 | user = users.get(id) 286 | user.projects = [] 287 | for project in request.form.getlist("projects[]"): 288 | user.projects.append(projects.get(int(project))) 289 | users.save(user) 290 | return jsonify(dict(rc=0)) 291 | -------------------------------------------------------------------------------- /web/controller/deploys.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from flask import render_template 3 | 4 | from web import app 5 | from .login import authorize 6 | 7 | __author__ = 'Rocky Peng' 8 | 9 | 10 | @app.route("/deploys", methods=["GET"]) 11 | @authorize 12 | def deploys(): 13 | return render_template("deploys.html") 14 | 15 | 16 | @app.route("/deploy/create", methods=["GET"]) 17 | @authorize 18 | def deploys_new(): 19 | return render_template("deploy_create.html") 20 | 21 | 22 | @app.route("/deploys//progress", methods=["GET"]) 23 | @authorize 24 | def deploy_progress(id): 25 | return render_template("deploy_progress.html") 26 | -------------------------------------------------------------------------------- /web/controller/host.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from flask import render_template 3 | 4 | from web import app 5 | from .login import authorize 6 | 7 | __author__ = 'Rocky Peng' 8 | 9 | 10 | @app.route("/hosts", methods=["GET"]) 11 | @authorize 12 | def hosts(): 13 | return render_template("hosts.html") 14 | 15 | 16 | @app.route("/hosts/", methods=["GET"]) 17 | @authorize 18 | def hosts_id(id): 19 | return render_template("host_detail.html") 20 | 21 | 22 | @app.route("/host/create", methods=["GET"]) 23 | @authorize 24 | def host_creation(): 25 | return render_template("host_create.html") 26 | 27 | 28 | @app.route("/host//group", methods=["GET"]) 29 | @authorize 30 | def host_group(id): 31 | return render_template("host_detail.html") 32 | -------------------------------------------------------------------------------- /web/controller/login.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import collections 3 | from datetime import datetime 4 | from hashlib import md5 5 | import random 6 | import string 7 | from flask import request, Response, redirect, url_for, render_template, g 8 | 9 | from web import app 10 | from web.services.users import users 11 | from web.services.sessions import sessions 12 | 13 | from functools import wraps 14 | 15 | __author__ = 'Rocky Peng' 16 | 17 | 18 | def authorize(func): 19 | @wraps(func) 20 | def decorator(*args, **kargs): 21 | apikey = request.args.get("apikey") 22 | sign = request.cookies.get('sign') 23 | if users.is_login(sign, apikey): 24 | g.user = users.first(apikey=apikey) or \ 25 | users.get(sessions.first(session=sign).user_id) 26 | if g.user is not None: 27 | return func(*args, **kargs) 28 | return redirect(url_for('login', next=request.path)) 29 | return decorator 30 | 31 | 32 | @app.route("/", methods=["GET"]) 33 | @authorize 34 | def index(): 35 | return redirect(url_for('deploys')) 36 | 37 | 38 | @app.route("/login", methods=["GET"]) 39 | def login(): 40 | return render_template("login.html") 41 | 42 | 43 | @app.route("/account/change_password", methods=["GET"]) 44 | @authorize 45 | def change_password(): 46 | return render_template("account_change_password.html") 47 | 48 | 49 | @app.route("/logout") 50 | @authorize 51 | def logout(): 52 | users.logout(g.user) 53 | resp = redirect(url_for('login')) 54 | resp.set_cookie("sign", "", expires=0) 55 | return resp 56 | -------------------------------------------------------------------------------- /web/controller/project.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from flask import render_template, jsonify 4 | 5 | from web import app 6 | from .login import authorize 7 | 8 | __author__ = 'Rocky Peng' 9 | 10 | 11 | @app.route("/projects", methods=["GET"]) 12 | @authorize 13 | def projects(): 14 | return render_template("projects.html") 15 | 16 | 17 | @app.route("/projects/", methods=["GET"]) 18 | @authorize 19 | def detail(id): 20 | return render_template("project_detail.html") 21 | 22 | 23 | @app.route("/project/create", methods=["GET"]) 24 | @authorize 25 | def project_create(): 26 | return render_template("project_create.html") 27 | -------------------------------------------------------------------------------- /web/controller/users.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from flask import render_template 3 | 4 | from web import app 5 | from .login import authorize 6 | 7 | __author__ = 'Rocky Peng' 8 | 9 | 10 | @app.route("/users", methods=["GET"]) 11 | @authorize 12 | def users(): 13 | return render_template("users.html") 14 | 15 | 16 | @app.route("/users/", methods=["GET"]) 17 | @authorize 18 | def users_id(id): 19 | return render_template("host_detail.html") 20 | 21 | 22 | @app.route("/users/create", methods=["GET"]) 23 | @authorize 24 | def users_creation(): 25 | return render_template("user_create.html") 26 | 27 | 28 | @app.route("/users//hosts", methods=["GET"]) 29 | @authorize 30 | def users_hosts(id): 31 | return render_template("user_hosts.html") 32 | 33 | 34 | @app.route("/users//projects", methods=["GET"]) 35 | @authorize 36 | def users_projects(id): 37 | return render_template("user_projects.html") 38 | -------------------------------------------------------------------------------- /web/controller/webhooks.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import time 3 | 4 | from flask import request, jsonify, g 5 | 6 | from web import app 7 | from web.services.users import users 8 | from web.services.hosts import hosts 9 | from web.services.deploys import deploys 10 | from web.services.projects import projects 11 | from .login import authorize 12 | from web.utils.log import Logger 13 | logger = Logger("web.controllers.webhooks") 14 | 15 | 16 | # 添加git的webhook像这样:http://10.169.123.172:9998/api/webhooks/push_events?apikey=FWi14sULr0CwdYqhyBwQfbpdSEV7M8dp&project_id=9&host_id=1 17 | @app.route('/api/webhooks/push_events', methods=["POST"]) 18 | @authorize 19 | def webhooks_push_events(): 20 | project_id = request.args.get("project_id") 21 | host_id = request.args.get("host_id") 22 | data = request.json 23 | branch = data["ref"].split("/", 2)[-1] 24 | version = data["after"][:7] 25 | logger.debug(repr(data)) 26 | # 只针对dev分支进行deploy 27 | if data["ref"] == "refs/heads/dev" and data["total_commits_count"] > 0: 28 | deploy = deploys.create( 29 | user_id=g.user.id, 30 | project_id=project_id, 31 | host_id=host_id, 32 | mode=0, 33 | status=3, 34 | branch=branch, 35 | version=version, 36 | softln_filename=time.strftime("%Y%m%d-%H%M%S") + "-" + version, 37 | ) 38 | deploys.deploy(deploy) 39 | return jsonify({"rc": 0}), 200 40 | else: 41 | return jsonify({"rc": 0}), 204 42 | 43 | 44 | @app.route('/api/webhooks/tag_push_events', methods=["POST"]) 45 | @authorize 46 | def webhooks_tag_push_events(): 47 | project_id = request.args.get("project_id") 48 | host_id = request.args.get("host_id") 49 | data = request.json 50 | tag = data["ref"].split("/", 2)[-1] 51 | logger.debug(repr(data)) 52 | # 只针对tag push 53 | if data["object_kind"] == "tag_push" and data["total_commits_count"] > 0: 54 | deploy = deploys.create( 55 | user_id=g.user.id, 56 | project_id=project_id, 57 | host_id=host_id, 58 | mode=1, 59 | status=3, 60 | branch="", 61 | version=tag, 62 | softln_filename=time.strftime("%Y%m%d-%H%M%S") + "-" + tag, 63 | ) 64 | deploys.deploy(deploy) 65 | return jsonify({"rc": 0}), 200 66 | else: 67 | return jsonify({"rc": 0}), 204 68 | -------------------------------------------------------------------------------- /web/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meanstrong/pydelo/74ba65b4150479ff6a4b8aec3c83401d24022056/web/models/__init__.py -------------------------------------------------------------------------------- /web/models/deploys.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | # -*- coding:utf-8 -*- 3 | from web import db 4 | from web.utils.jsonencoder import JsonSerializer 5 | 6 | __author__ = 'Rocky Peng' 7 | 8 | 9 | class Deploys(JsonSerializer, db.Model): 10 | id = db.Column(db.Integer, primary_key=True) 11 | user_id = db.Column(db.Integer, db.ForeignKey("users.id")) 12 | project_id = db.Column(db.Integer, db.ForeignKey("projects.id")) 13 | host_id = db.Column(db.Integer, db.ForeignKey("hosts.id")) 14 | mode = db.Column(db.Integer) 15 | branch = db.Column(db.String(32)) 16 | version = db.Column(db.String(32)) 17 | progress = db.Column(db.Integer, default=0) 18 | status = db.Column(db.Integer, default=0) 19 | softln_filename = db.Column(db.String(64)) 20 | comment = db.Column(db.Text, default="") 21 | created_at = db.Column(db.DateTime, default=db.func.now()) 22 | updated_at = db.Column(db.DateTime, default=db.func.now(), 23 | onupdate=db.func.now()) 24 | 25 | user = db.relationship("Users", 26 | backref=db.backref("deploys", lazy="dynamic")) 27 | project = db.relationship("Projects", 28 | backref=db.backref("deploys", lazy="dynamic")) 29 | host = db.relationship("Hosts", 30 | backref=db.backref("deploys", lazy="dynamic")) 31 | -------------------------------------------------------------------------------- /web/models/hosts.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | # -*- coding:utf-8 -*- 3 | from web import db 4 | from web.utils.jsonencoder import JsonSerializer 5 | 6 | __author__ = 'Rocky Peng' 7 | 8 | 9 | rel_user_host = db.Table( 10 | "rel_user_host", 11 | db.Column("id", db.Integer, primary_key=True), 12 | db.Column("user_id", db.Integer, db.ForeignKey("users.id")), 13 | db.Column("host_id", db.Integer, db.ForeignKey("hosts.id")), 14 | db.Column("created_at", db.DateTime, default=db.func.now()), 15 | db.Column("updated_at", db.DateTime, default=db.func.now(), 16 | onupdate=db.func.now()), 17 | ) 18 | 19 | 20 | class Hosts(JsonSerializer, db.Model): 21 | __json_hidden__ = ["deploys", "users"] 22 | 23 | id = db.Column(db.Integer, primary_key=True) 24 | name = db.Column(db.String(64)) 25 | ssh_host = db.Column(db.String(32)) 26 | ssh_port = db.Column(db.Integer) 27 | ssh_user = db.Column(db.String(64)) 28 | ssh_method = db.Column(db.Integer()) 29 | ssh_pass = db.Column(db.String(100)) 30 | created_at = db.Column(db.DateTime, default=db.func.now()) 31 | updated_at = db.Column(db.DateTime, default=db.func.now(), 32 | onupdate=db.func.now()) 33 | 34 | users = db.relationship("Users", secondary=rel_user_host, 35 | backref=db.backref("hosts", lazy="dynamic")) 36 | -------------------------------------------------------------------------------- /web/models/projects.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | # -*- coding:utf-8 -*- 3 | from web import db 4 | from web.utils.jsonencoder import JsonSerializer 5 | 6 | __author__ = 'Rocky Peng' 7 | 8 | 9 | rel_user_project = db.Table( 10 | "rel_user_project", 11 | db.Column("id", db.Integer, primary_key=True), 12 | db.Column("user_id", db.Integer, db.ForeignKey("users.id")), 13 | db.Column("project_id", db.Integer, db.ForeignKey("projects.id")), 14 | db.Column("created_at", db.DateTime, default=db.func.now()), 15 | db.Column("updated_at", db.DateTime, default=db.func.now(), 16 | onupdate=db.func.now()), 17 | ) 18 | 19 | 20 | class Projects(JsonSerializer, db.Model): 21 | __json_hidden__ = ["deploys", "users"] 22 | 23 | id = db.Column(db.Integer, primary_key=True) 24 | name = db.Column(db.String(64), unique=True) 25 | repo_url = db.Column(db.String(200)) 26 | checkout_dir = db.Column(db.String(200)) 27 | target_dir = db.Column(db.String(200)) 28 | deploy_dir = db.Column(db.String(200)) 29 | deploy_history_dir = db.Column(db.String(200)) 30 | before_checkout = db.Column(db.Text, default="") 31 | after_checkout = db.Column(db.Text, default="") 32 | before_deploy = db.Column(db.Text, default="") 33 | after_deploy = db.Column(db.Text, default="") 34 | created_at = db.Column(db.DateTime, default=db.func.now()) 35 | updated_at = db.Column(db.DateTime, default=db.func.now(), 36 | onupdate=db.func.now()) 37 | 38 | users = db.relationship("Users", secondary=rel_user_project, 39 | backref=db.backref("projects", lazy="dynamic")) 40 | -------------------------------------------------------------------------------- /web/models/rel_user_host.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | # -*- coding:utf-8 -*- 3 | from web import db 4 | from web.utils.jsonencoder import JsonSerializer 5 | 6 | __author__ = 'Rocky Peng' 7 | 8 | 9 | class RelUserHost(): 10 | pass 11 | -------------------------------------------------------------------------------- /web/models/rel_user_project.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | # -*- coding:utf-8 -*- 3 | from web import db 4 | from web.utils.jsonencoder import JsonSerializer 5 | 6 | __author__ = 'Rocky Peng' 7 | 8 | 9 | class RelUserProject(JsonSerializer, db.Model): 10 | 11 | id = db.Column(db.Integer, primary_key=True) 12 | user_id = db.Column(db.Integer, db.ForeignKey("users.id")) 13 | project_id = db.Column(db.Integer, db.ForeignKey("projects.id")) 14 | created_at = db.Column(db.DateTime, default=db.func.now()) 15 | updated_at = db.Column(db.DateTime, default=db.func.now(), 16 | onupdate=db.func.now()) 17 | 18 | user = db.relationship("Users", 19 | backref=db.backref("projects", lazy="dynamic")) 20 | project = db.relationship("Projects", 21 | backref=db.backref("users", lazy="dynamic")) 22 | -------------------------------------------------------------------------------- /web/models/sessions.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | # -*- coding:utf-8 -*- 3 | from web import db 4 | from web.utils.jsonencoder import JsonSerializer 5 | 6 | __author__ = 'Rocky Peng' 7 | 8 | 9 | class Sessions(JsonSerializer, db.Model): 10 | id = db.Column(db.Integer, primary_key=True) 11 | user_id = db.Column(db.Integer, db.ForeignKey("users.id")) 12 | session = db.Column(db.String(32)) 13 | expired = db.Column(db.Integer) 14 | created_at = db.Column(db.DateTime, default=db.func.now()) 15 | updated_at = db.Column(db.DateTime, default=db.func.now(), 16 | onupdate=db.func.now()) 17 | 18 | user = db.relationship("Users", 19 | backref=db.backref("sessions", lazy="dynamic")) 20 | -------------------------------------------------------------------------------- /web/models/users.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | # -*- coding:utf-8 -*- 3 | from web import db 4 | from web.utils.jsonencoder import JsonSerializer 5 | 6 | __author__ = 'Rocky Peng' 7 | 8 | 9 | class Users(JsonSerializer, db.Model): 10 | __json_hidden__ = ["deploys", "sessions", "hosts", "projects"] 11 | 12 | ROLE = dict(ADMIN=1, NORMAL=2) 13 | 14 | id = db.Column(db.Integer, primary_key=True) 15 | name = db.Column(db.String(32)) 16 | password = db.Column(db.String(64)) 17 | role = db.Column(db.Integer, default=2) 18 | email = db.Column(db.String(64)) 19 | phone = db.Column(db.String(16)) 20 | apikey = db.Column(db.String(64)) 21 | created_at = db.Column(db.DateTime, default=db.func.now()) 22 | updated_at = db.Column(db.DateTime, default=db.func.now(), 23 | onupdate=db.func.now()) 24 | -------------------------------------------------------------------------------- /web/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meanstrong/pydelo/74ba65b4150479ff6a4b8aec3c83401d24022056/web/services/__init__.py -------------------------------------------------------------------------------- /web/services/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | # -*- coding:utf-8 -*- 3 | from web import db 4 | from web import db_session 5 | from web.utils.log import Logger 6 | logger = Logger("web.services.base") 7 | __author__ = 'Rocky Peng' 8 | 9 | 10 | class Base(object): 11 | __model__ = None 12 | 13 | def __init__(self, session=None): 14 | self.session = session or db_session 15 | 16 | def save(self, model): 17 | self.session.add(model) 18 | self.session.commit() 19 | return model 20 | 21 | def find(self, **kargs): 22 | query = self.session.query(self.__model__).filter_by(**kargs) 23 | return query 24 | 25 | def first(self, **kargs): 26 | return self.session.query(self.__model__).filter_by(**kargs).first() 27 | 28 | def get(self, id): 29 | self.session.expire_all() 30 | return self.session.query(self.__model__).get(id) 31 | 32 | def get_or_404(self, id): 33 | self.session.query(self.__model__).get_or_404(id) 34 | 35 | def count(self, **kargs): 36 | return self.session.query(self.__model__).filter_by(**kargs).count() 37 | 38 | def all(self, offset=None, limit=None, order_by=None, desc=False): 39 | query = self.session.query(self.__model__) 40 | if order_by is not None: 41 | if desc: 42 | query = query.order_by(db.desc(order_by)) 43 | else: 44 | query = query.order_by(order_by) 45 | if offset is not None: 46 | query = query.offset(offset) 47 | if limit is not None: 48 | query = query.limit(limit) 49 | return query.all() 50 | 51 | def create(self, **kargs): 52 | return self.save(self.__model__(**kargs)) 53 | 54 | def update(self, model, **kargs): 55 | for k, v in kargs.items(): 56 | setattr(model, k, v) 57 | self.save(model) 58 | return model 59 | 60 | def session_commit(self): 61 | self.session.commit() 62 | 63 | def __del__(self): 64 | logger.info("session close.") 65 | self.session.close() 66 | -------------------------------------------------------------------------------- /web/services/deploys.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | # -*- coding:utf-8 -*- 3 | import traceback 4 | import os 5 | import threading 6 | 7 | from web import db 8 | from web.models.deploys import Deploys 9 | from .base import Base 10 | from web.utils.git import Git 11 | from web.utils.localshell import LocalShell 12 | from web.utils.remoteshell import RemoteShell 13 | from web.utils.error import Error 14 | import web.config as config 15 | from web.utils.log import Logger 16 | logger = Logger("web.deploys.deploys") 17 | __author__ = 'Rocky Peng' 18 | 19 | 20 | class DeploysService(Base): 21 | __model__ = Deploys 22 | 23 | def deploy(self, deploy): 24 | if self.count(status=2, project_id=deploy.project_id): 25 | logger.debug("deploy thread wait in quene") 26 | return 27 | first_deploy = self.first(status=3, project_id=deploy.project_id) 28 | if first_deploy.mode == 0 or first_deploy.mode == 1: 29 | t = threading.Thread(target=deploy_thread, 30 | args=(deploy.project_id,), 31 | name="pydelo-deploy[%d]" % deploy.id) 32 | t.start() 33 | elif first_deploy.mode == 2: 34 | t = threading.Thread(target=rollback_thread, 35 | args=(deploy.project_id,), 36 | name="pydelo-deploy[%d]" % deploy.id) 37 | t.start() 38 | 39 | def rollback(self, deploy): 40 | if self.find(status=2, project_id=deploy.project_id).count(): 41 | logger.debug("deploy thread wait in quene") 42 | return 43 | t = threading.Thread(target=rollback_thread, args=(deploy.project_id,), 44 | name="pydelo-deploy[%d]" % deploy.id) 45 | t.start() 46 | 47 | def append_comment(self, deploy, comment): 48 | sql = ("UPDATE {table} SET comment = CONCAT(comment, :comment) where " 49 | "id = {id}").format(table=self.__model__.__tablename__, 50 | id=deploy.id) 51 | self.session.execute(sql, {"comment": comment}) 52 | self.session.commit() 53 | 54 | deploys = DeploysService() 55 | 56 | 57 | def rollback_thread(project_id): 58 | deploys = DeploysService() 59 | deploy = deploys.first(project_id=project_id, status=3) 60 | logger.info("deploy thread start: %d" % deploy.id) 61 | ssh = RemoteShell(host=deploy.host.ssh_host, 62 | port=deploy.host.ssh_port, 63 | user=deploy.host.ssh_user, 64 | passwd=deploy.host.ssh_pass) 65 | try: 66 | # before rollback 67 | deploys.append_comment(deploy, "before rollback:\n") 68 | logger.debug("before rollback:") 69 | before_deploy = deploy.project.before_deploy.replace("\r", "").replace( 70 | "\n", " && ") 71 | if before_deploy: 72 | rc, stdout, stderr = ssh.exec_command( 73 | "WORKSPACE='{0}' && cd $WORKSPACE && {1}".format( 74 | deploy.project.deploy_dir, before_deploy)) 75 | if rc: 76 | raise Error(11000) 77 | deploys.append_comment(deploy, "OK!\n") 78 | deploys.update(deploy, progress=33) 79 | # rollback 80 | deploys.append_comment(deploy, "rollback:\n") 81 | logger.debug("rollback:") 82 | rc, stdout, stderr = ssh.exec_command("ln -snf {0} {1}".format( 83 | os.path.join(deploy.project.deploy_history_dir, 84 | deploy.softln_filename), 85 | deploy.project.deploy_dir)) 86 | if rc: 87 | raise Error(11001) 88 | deploys.append_comment(deploy, "OK!\n") 89 | deploys.update(deploy, progress=67) 90 | 91 | # after rollback 92 | deploys.append_comment(deploy, "after rollback:\n") 93 | logger.debug("after rollback:") 94 | after_deploy = deploy.project.after_deploy.replace("\r", "").replace( 95 | "\n", " && ") 96 | if after_deploy: 97 | rc, stdout, stderr = ssh.exec_command( 98 | "WORKSPACE='{0}' && cd $WORKSPACE && {1}".format( 99 | deploy.project.deploy_dir, after_deploy)) 100 | if rc: 101 | raise Error(11002) 102 | deploys.append_comment(deploy, "OK!\n") 103 | except Exception as err: 104 | traceback.print_exc() 105 | deploys.append_comment(deploy, repr(err)) 106 | deploys.update(deploy, status=0) 107 | else: 108 | deploys.update(deploy, progress=100, status=1) 109 | finally: 110 | logger.info("deploy thread end: %d" % deploy.id) 111 | ssh.close() 112 | deploy = deploys.first(project_id=deploy.project_id, status=3) 113 | if deploy: 114 | deploys.deploy(deploy) 115 | 116 | 117 | def deploy_thread(project_id): 118 | deploys = DeploysService() 119 | deploy = deploys.first(project_id=project_id, status=3) 120 | if not deploy: 121 | logger.info("no deploy wait in quene.") 122 | return 123 | logger.info("deploy thread start: {}".format(deploy.id)) 124 | ssh = RemoteShell(host=deploy.host.ssh_host, 125 | port=deploy.host.ssh_port, 126 | user=deploy.host.ssh_user, 127 | passwd=deploy.host.ssh_pass) 128 | try: 129 | deploys.update(deploy, progress=0, status=2) 130 | # before checkout 131 | git = Git(deploy.project.checkout_dir, deploy.project.repo_url) 132 | before_checkout = deploy.project.before_checkout.replace( 133 | "\r", "").replace("\n", " && ") 134 | logger.debug("before_checkout"+before_checkout) 135 | deploys.append_comment(deploy, "before checkout:\n") 136 | cmd = "mkdir -p {0} && rm -rf {1}/*".format( 137 | deploy.project.target_dir, deploy.project.target_dir) 138 | LocalShell.check_call(cmd, shell=True) 139 | if before_checkout: 140 | cmd = "WORKSPACE='{0}' && cd $WORKSPACE && {1}".format( 141 | deploy.project.checkout_dir, before_checkout) 142 | LocalShell.check_call(cmd, shell=True) 143 | deploys.append_comment(deploy, "OK!\n") 144 | deploys.update(deploy, progress=17) 145 | # checkout 146 | deploys.append_comment(deploy, "checkout:\n") 147 | git.clone() 148 | if deploy.mode == 0: 149 | git.checkout_branch(deploy.branch, deploy.version) 150 | else: 151 | git.checkout_tag(deploy.version) 152 | deploys.append_comment(deploy, "OK!\n") 153 | deploys.update(deploy, progress=33) 154 | # after checkout 155 | after_checkout = deploy.project.after_checkout.replace( 156 | "\r", "").replace("\n", " && ") 157 | deploys.append_comment(deploy, "after checkout:\n") 158 | if after_checkout: 159 | cmd = "WORKSPACE='{0}' && cd $WORKSPACE && {1}".format( 160 | deploy.project.checkout_dir, after_checkout) 161 | LocalShell.check_call(cmd, shell=True) 162 | deploys.append_comment(deploy, "OK!\n") 163 | deploys.update(deploy, progress=50) 164 | # before deploy 165 | deploys.append_comment(deploy, "before deploy:\n") 166 | ssh.check_call( 167 | "mkdir -p {0}".format( 168 | os.path.join(deploy.project.deploy_history_dir, 169 | deploy.softln_filename))) 170 | 171 | logger.debug("before deploy:") 172 | ssh.check_call( 173 | ("WORKSPACE='{0}' && cd $WORKSPACE && ls -1t | tail -n +{1} | " 174 | "xargs rm -rf").format(deploy.project.deploy_history_dir, 175 | config.MAX_DEPLOY_HISTORY)) 176 | before_deploy = deploy.project.before_deploy.replace("\r", "").replace( 177 | "\n", " && ") 178 | if before_deploy: 179 | ssh.check_call( 180 | "WORKSPACE='{0}' && cd $WORKSPACE && {1}".format( 181 | deploy.project.deploy_dir, before_deploy)) 182 | deploys.append_comment(deploy, "OK!\n") 183 | deploys.update(deploy, progress=67) 184 | # deploy 185 | deploys.append_comment(deploy, "deploy:\n") 186 | logger.debug("deploy:") 187 | logger.debug("rsync:") 188 | if deploy.host.ssh_method == 0: 189 | cmd = ("rsync -avzq " 190 | "--rsh=\"sshpass -p {ssh_pass} ssh -p {ssh_port}\" " 191 | "--exclude='.git' {local_dest}/ {ssh_user}@{ssh_host}:" 192 | "{remote_dest}/" 193 | ).format(local_dest=deploy.project.target_dir, 194 | remote_dest=os.path.join( 195 | deploy.project.deploy_history_dir, 196 | deploy.softln_filename), 197 | ssh_user=deploy.host.ssh_user, 198 | ssh_host=deploy.host.ssh_host, 199 | ssh_port=deploy.host.ssh_port, 200 | ssh_pass=deploy.host.ssh_pass) 201 | else: 202 | cmd = ("rsync -avzq --exclude='.git' {local_dest}/ " 203 | "{ssh_user}@{ssh_host}:{remote_dest}/" 204 | ).format(local_dest=deploy.project.target_dir, 205 | remote_dest=os.path.join( 206 | deploy.project.deploy_history_dir, 207 | deploy.softln_filename), 208 | ssh_user=deploy.host.ssh_user, 209 | ssh_host=deploy.host.ssh_host, 210 | ssh_port=deploy.host.ssh_port, 211 | ssh_pass=deploy.host.ssh_pass) 212 | LocalShell.check_call(cmd, shell=True) 213 | ssh.check_call("ln -snf {0} {1}".format( 214 | os.path.join(deploy.project.deploy_history_dir, 215 | deploy.softln_filename), 216 | deploy.project.deploy_dir)) 217 | deploys.append_comment(deploy, "OK!\n") 218 | deploys.update(deploy, progress=83) 219 | 220 | # after deploy 221 | deploys.append_comment(deploy, "after deploy:\n") 222 | logger.debug("after deploy:") 223 | after_deploy = deploy.project.after_deploy.replace("\r", "").replace( 224 | "\n", " && ") 225 | if after_deploy: 226 | ssh.check_call( 227 | "WORKSPACE='{0}' && cd $WORKSPACE && {1}".format( 228 | deploy.project.deploy_dir, after_deploy)) 229 | deploys.append_comment(deploy, "OK!\n") 230 | except Exception as err: 231 | traceback.print_exc() 232 | logger.error(err) 233 | deploys.append_comment(deploy, repr(err)) 234 | deploys.update(deploy, status=0) 235 | else: 236 | deploys.update(deploy, progress=100, status=1) 237 | finally: 238 | logger.info("deploy thread end: %d" % deploy.id) 239 | ssh.close() 240 | deploy = deploys.first(project_id=project_id, status=3) 241 | if deploy: 242 | logger.info("deploy thread fetch from wait: {}".format(deploy)) 243 | deploys.deploy(deploy) 244 | -------------------------------------------------------------------------------- /web/services/hosts.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | # -*- coding:utf-8 -*- 3 | from web import db 4 | from web.models.hosts import Hosts 5 | from .base import Base 6 | from web.utils.log import Logger 7 | logger = Logger("host service") 8 | __author__ = 'Rocky Peng' 9 | 10 | 11 | class HostsService(Base): 12 | __model__ = Hosts 13 | 14 | hosts = HostsService() 15 | -------------------------------------------------------------------------------- /web/services/projects.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | # -*- coding:utf-8 -*- 3 | from web import db 4 | from web.models.projects import Projects 5 | from web.utils.git import Git 6 | from .base import Base 7 | from web.utils.log import Logger 8 | logger = Logger("web.services.projects") 9 | __author__ = 'Rocky Peng' 10 | 11 | 12 | class ProjectsService(Base): 13 | __model__ = Projects 14 | 15 | def git_clone(self, project): 16 | git = Git(project.checkout_dir, project.repo_url) 17 | git.clone() 18 | 19 | def git_branch(self, project): 20 | git = Git(project.checkout_dir, project.repo_url) 21 | return git.remote_branch() 22 | 23 | def git_tag(self, project): 24 | git = Git(project.checkout_dir, project.repo_url) 25 | return git.tag() 26 | 27 | def git_branch_commit_log(self, project, branch): 28 | git = Git(project.checkout_dir, project.repo_url) 29 | git.checkout_branch(branch) 30 | return git.log() 31 | 32 | projects = ProjectsService() 33 | -------------------------------------------------------------------------------- /web/services/sessions.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | # -*- coding:utf-8 -*- 3 | from web import db 4 | from web.models.sessions import Sessions 5 | from .base import Base 6 | from web.utils.log import Logger 7 | logger = Logger("web.services.sessions") 8 | __author__ = 'Rocky Peng' 9 | 10 | 11 | class SessionsService(Base): 12 | __model__ = Sessions 13 | 14 | 15 | sessions = SessionsService() 16 | -------------------------------------------------------------------------------- /web/services/users.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | # -*- coding:utf-8 -*- 3 | from datetime import datetime 4 | import sys 5 | import time 6 | import random 7 | import string 8 | from hashlib import md5 9 | 10 | from web import db 11 | from web.utils.log import Logger 12 | from web.models.users import Users 13 | from web.services.hosts import hosts 14 | from web.services.projects import projects 15 | 16 | from .base import Base 17 | from web.utils.error import Error 18 | from .sessions import sessions 19 | logger = Logger("web.services.users") 20 | if sys.version_info > (3,): 21 | string.letters = string.ascii_letters 22 | __author__ = 'Rocky Peng' 23 | 24 | 25 | class UsersService(Base): 26 | __model__ = Users 27 | 28 | def login(self, username, password): 29 | password = md5(password.encode("utf-8")).hexdigest().upper() 30 | user = self.first(name=username, password=password) 31 | if user is None: 32 | raise Error(13000) 33 | session = sessions.first(user_id=user.id) 34 | expired = datetime.fromtimestamp(time.time()+24*60*60).isoformat() 35 | if session is None: 36 | sign = ''.join(random.choice(string.letters+string.digits) for _ in 37 | range(20)) 38 | sessions.create(user_id=user.id, 39 | session=sign, 40 | expired=expired) 41 | else: 42 | sessions.update(session, expired=expired) 43 | sign = session.session 44 | return sign 45 | 46 | def logout(self, user): 47 | session = sessions.first(user_id=user.id) 48 | if session is not None: 49 | sessions.update( 50 | session, 51 | expired=datetime.now().isoformat()) 52 | 53 | def is_login(self, session, apikey): 54 | if users.first(apikey=apikey): 55 | return True 56 | session = sessions.first(session=session) 57 | if session is not None: 58 | delta = session.expired-datetime.now() 59 | if (delta.microseconds + (delta.seconds + delta.days * 24 * 3600) * 60 | 10**6) / 10**6 > 0: 61 | return True 62 | return False 63 | 64 | def get_user_hosts(self, user, **kargs): 65 | if user.role == user.ROLE["ADMIN"]: 66 | return dict(hosts=hosts.all(kargs.get("offset"), 67 | kargs.get("limit"), 68 | kargs.get("order_by")), 69 | count=hosts.count()) 70 | else: 71 | return dict(hosts=user.hosts.all(), 72 | count=user.hosts.count()) 73 | 74 | def get_user_projects(self, user, **kargs): 75 | if user.role == user.ROLE["ADMIN"]: 76 | return dict(projects=projects.all(kargs.get("offset"), 77 | kargs.get("limit"), 78 | kargs.get("order_by")), 79 | count=projects.count()) 80 | else: 81 | return dict(projects=user.projects.all(), 82 | count=user.projects.count()) 83 | 84 | 85 | users = UsersService() 86 | -------------------------------------------------------------------------------- /web/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.4 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.4",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.4",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.4",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.4",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=c(d),f={relatedTarget:this};e.hasClass("open")&&(e.trigger(b=a.Event("hide.bs.dropdown",f)),b.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f)))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.4",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(this.options.viewport.selector||this.options.viewport),this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c&&c.$tip&&c.$tip.is(":visible")?void(c.hoverState="in"):(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.options.container?a(this.options.container):this.$element.parent(),p=this.getPosition(o);h="bottom"==h&&k.bottom+m>p.bottom?"top":"top"==h&&k.top-mp.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.width&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type)})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.4",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.4",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.4",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=a(document.body).height();"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); -------------------------------------------------------------------------------- /web/static/js/jquery.cookie.min.js: -------------------------------------------------------------------------------- 1 | /*! jquery.cookie v1.4.1 | MIT */ 2 | !function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?a(require("jquery")):a(jQuery)}(function(a){function b(a){return h.raw?a:encodeURIComponent(a)}function c(a){return h.raw?a:decodeURIComponent(a)}function d(a){return b(h.json?JSON.stringify(a):String(a))}function e(a){0===a.indexOf('"')&&(a=a.slice(1,-1).replace(/\\"/g,'"').replace(/\\\\/g,"\\"));try{return a=decodeURIComponent(a.replace(g," ")),h.json?JSON.parse(a):a}catch(b){}}function f(b,c){var d=h.raw?b:e(b);return a.isFunction(c)?c(d):d}var g=/\+/g,h=a.cookie=function(e,g,i){if(void 0!==g&&!a.isFunction(g)){if(i=a.extend({},h.defaults,i),"number"==typeof i.expires){var j=i.expires,k=i.expires=new Date;k.setTime(+k+864e5*j)}return document.cookie=[b(e),"=",d(g),i.expires?"; expires="+i.expires.toUTCString():"",i.path?"; path="+i.path:"",i.domain?"; domain="+i.domain:"",i.secure?"; secure":""].join("")}for(var l=e?void 0:{},m=document.cookie?document.cookie.split("; "):[],n=0,o=m.length;o>n;n++){var p=m[n].split("="),q=c(p.shift()),r=p.join("=");if(e&&e===q){l=f(r,g);break}e||void 0===(r=f(r))||(l[q]=r)}return l};h.defaults={},a.removeCookie=function(b,c){return void 0===a.cookie(b)?!1:(a.cookie(b,"",a.extend({},c,{expires:-1})),!a.cookie(b))}}); -------------------------------------------------------------------------------- /web/static/js/pydelo/account_change_password.js: -------------------------------------------------------------------------------- 1 | 2 | $(document).ready(function() { 3 | $("#submit").click(function () { 4 | account_change_password( 5 | {"password" : $("#password").val()}, 6 | function (data) { 7 | check_return(data); 8 | window.location.assign('/') 9 | } 10 | ); 11 | }); 12 | }) 13 | -------------------------------------------------------------------------------- /web/static/js/pydelo/api.js: -------------------------------------------------------------------------------- 1 | function append_option_to_select(data, select) { 2 | // select.append($("").text("请选择...")); 3 | $.each(data, function () { 4 | select.append($("").text(this["text"]).attr("value",this["value"])) }); 5 | } 6 | 7 | function append_tr_to_table(data, table) { 8 | table.empty(); 9 | $.each(data, function () { 10 | table.append($("").text($(""))) }); 11 | } 12 | 13 | function get_url_vars() { 14 | var vars = [], hash; 15 | var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); 16 | for (var i = 0; i < hashes.length; i++) { 17 | hash = hashes[i].split('='); 18 | vars.push(hash[0]); 19 | vars[hash[0]] = hash[1]; 20 | } 21 | var hashes = window.location.pathname.slice(1).split('/'); 22 | for (var i = 0; i < hashes.length-1; i++) { 23 | vars.push(hashes[i]); 24 | vars[hashes[i]] = hashes[i+1]; 25 | } 26 | return vars; 27 | } 28 | 29 | function check_return(data){ 30 | if(data["rc"] != 0){ 31 | alert(data["msg"]); 32 | } 33 | } 34 | 35 | function login(data, callback){ 36 | $.post("/api/login", data, callback, "json"); 37 | } 38 | 39 | function account_change_password(data, callback){ 40 | $.ajax({ 41 | url: "/api/accounts/password", 42 | type: "PUT", 43 | data: data, 44 | success : callback, 45 | dataType: "json" 46 | }); 47 | } 48 | 49 | function get_users(callback, offset, limit) { 50 | var url = ""; 51 | url += "/api/users"; 52 | if (typeof(offset) != "undefined" && typeof(limit) != "undefined"){ 53 | url += "?offset="+offset.toString()+"&limit="+limit.toString(); 54 | } 55 | $.get(url, callback, "json"); 56 | } 57 | 58 | function get_user_hosts(user_id, callback, offset, limit) { 59 | var url = ""; 60 | url += "/api/users/"+user_id.toString()+"/hosts"; 61 | if (typeof(offset) != "undefined" && typeof(limit) != "undefined"){ 62 | url += "?offset="+offset.toString()+"&limit="+limit.toString(); 63 | } 64 | $.get(url, callback, "json"); 65 | } 66 | 67 | function update_user_hosts(user_id, data, callback) { 68 | $.ajax({ 69 | url: "/api/users/"+user_id.toString()+"/hosts", 70 | type: "PUT", 71 | data: data, 72 | success : callback, 73 | dataType: "json" 74 | }); 75 | } 76 | 77 | function get_user_projects(user_id, callback, offset, limit) { 78 | var url = ""; 79 | url += "/api/users/"+user_id.toString()+"/projects"; 80 | if (typeof(offset) != "undefined" && typeof(limit) != "undefined"){ 81 | url += "?offset="+offset.toString()+"&limit="+limit.toString(); 82 | } 83 | $.get(url, callback, "json"); 84 | } 85 | 86 | function update_user_projects(user_id, data, callback) { 87 | $.ajax({ 88 | url: "/api/users/"+user_id.toString()+"/projects", 89 | type: "PUT", 90 | data: data, 91 | success : callback, 92 | dataType: "json" 93 | }); 94 | } 95 | 96 | function get_deploys(callback, offset, limit) { 97 | var url = "/api/deploys"; 98 | if (typeof(offset) != "undefined" && typeof(limit) != "undefined"){ 99 | url += "?offset="+offset.toString()+"&limit="+limit.toString(); 100 | } 101 | $.get(url, callback, "json"); 102 | } 103 | 104 | function create_deploy(project_id, host_id, data, callback){ 105 | $.post("/api/deploys?project_id="+project_id+"&host_id="+host_id, data, callback, "json"); 106 | } 107 | 108 | function update_deploy_by_id(id, data, callback) { 109 | $.ajax({ 110 | url: "/api/deploys/"+id.toString(), 111 | type: "PUT", 112 | data: data, 113 | success : callback, 114 | dataType: "json" 115 | }); 116 | } 117 | 118 | function get_deploy_progress(deploy_id, callback) { 119 | $.get("/api/deploys/"+deploy_id.toString(), callback, "json"); 120 | } 121 | 122 | function get_projects(offset, limit, callback) { 123 | if(arguments.length == 1){ 124 | $.get("/api/projects", arguments[0], "json"); 125 | }else{ 126 | $.get("/api/projects?offset="+offset.toString()+"&limit="+limit.toString(), callback, "json"); 127 | } 128 | } 129 | 130 | function get_project_by_id(id, callback) { 131 | $.get("/api/projects/"+id.toString(), callback, "json"); 132 | } 133 | 134 | function create_project(data, callback) { 135 | $.ajax({ 136 | url: "/api/projects", 137 | type: "POST", 138 | data: data, 139 | success : callback, 140 | dataType: "json" 141 | }); 142 | } 143 | 144 | function update_project_by_id(id, data, callback) { 145 | $.ajax({ 146 | url: "/api/projects/"+id.toString(), 147 | type: "PUT", 148 | data: data, 149 | success : callback, 150 | dataType: "json" 151 | }); 152 | } 153 | 154 | function get_tags_by_id(project_id, callback) { 155 | $.get("/api/projects/"+project_id.toString()+"/tags", callback, "json"); 156 | } 157 | 158 | function get_branches_by_id(project_id, callback) { 159 | $.get("/api/projects/"+project_id.toString()+"/branches", callback, "json"); 160 | } 161 | 162 | function get_commits_by_id(project_id, branch, callback) { 163 | $.get("/api/projects/"+project_id.toString()+"/branches/"+branch+"/commits", 164 | callback, 165 | "json"); 166 | } 167 | function get_hosts(callback, offset, limit) { 168 | var url = "/api/hosts"; 169 | if (typeof(offset) != "undefined" && typeof(limit) != "undefined"){ 170 | url += "?offset="+offset.toString()+"&limit="+limit.toString(); 171 | } 172 | $.get(url, callback, "json"); 173 | } 174 | 175 | function get_host_by_id(id, callback) { 176 | $.get("/api/hosts/"+id.toString(), callback, "json"); 177 | } 178 | 179 | function create_user(data, callback) { 180 | $.ajax({ 181 | url: "/api/users", 182 | type: "POST", 183 | data: data, 184 | success : callback, 185 | dataType: "json" 186 | }); 187 | } 188 | 189 | function create_host(data, callback) { 190 | $.ajax({ 191 | url: "/api/hosts", 192 | type: "POST", 193 | data: data, 194 | success : callback, 195 | dataType: "json" 196 | }); 197 | } 198 | 199 | function update_host_by_id(id, data, callback) { 200 | $.ajax({ 201 | url: "/api/hosts/"+id.toString(), 202 | type: "PUT", 203 | data: data, 204 | success : callback, 205 | dataType: "json" 206 | }); 207 | } 208 | -------------------------------------------------------------------------------- /web/static/js/pydelo/deploy_create.js: -------------------------------------------------------------------------------- 1 | function refresh_projects() { 2 | $("#projects").empty(); 3 | get_projects(function (data) { 4 | check_return(data); 5 | var data = data["data"]; 6 | var projects = []; 7 | $.each(data["projects"], function(){projects.push({"text": this["name"], "value": this["id"]})}); 8 | $("#projects").append($("").text("请选择...")); 9 | append_option_to_select(projects, $("#projects")); 10 | }); 11 | } 12 | 13 | function refresh_hosts() { 14 | $("#hosts").empty(); 15 | get_hosts(function (data) { 16 | check_return(data); 17 | var data = data["data"]; 18 | var hosts = []; 19 | $.each(data["hosts"], function(){hosts.push({"text": this["name"], "value": this["id"]})}); 20 | $("#hosts").append($("").text("请选择...")); 21 | append_option_to_select(hosts, $("#hosts")); 22 | }); 23 | } 24 | 25 | function refresh_branches () { 26 | $("#branches").empty(); 27 | $("#commits").empty(); 28 | get_branches_by_id($("#projects").val(), function(data){ 29 | check_return(data); 30 | var data = data["data"]; 31 | var branches = []; 32 | $.each(data, function(){ 33 | branches.push({"text":this, "value":this}); 34 | }); 35 | $("#branches").append($("").text("请选择...")); 36 | append_option_to_select(branches, $("#branches")); 37 | }); 38 | } 39 | 40 | function refresh_commits () { 41 | $("#commits").empty(); 42 | get_commits_by_id($("#projects").val(), $("#branches option:selected").text(), function(data){ 43 | check_return(data); 44 | var data = data["data"]; 45 | var commits = []; 46 | $.each(data, function(){ 47 | commits.push({"value":this["abbreviated_commit"], "text":this["abbreviated_commit"]+" - "+this["author_name"]+" - "+this["subject"]}); 48 | }); 49 | $("#commits").append($("").text("请选择...")); 50 | append_option_to_select(commits, $("#commits")); 51 | }); 52 | } 53 | 54 | function refresh_tags(){ 55 | $("#tags").empty(); 56 | get_tags_by_id($("#projects").val(), function(data){ 57 | check_return(data); 58 | var data = data["data"]; 59 | var tags = []; 60 | $.each(data, function(){ 61 | tags.push({"value":this, "text":this}); 62 | }); 63 | $("#tags").append($("").text("请选择...")); 64 | append_option_to_select(tags, $("#tags")); 65 | }); 66 | } 67 | 68 | $(document).ready(function() { 69 | refresh_projects(); 70 | refresh_hosts(); 71 | $("#projects").change(function(){ 72 | var deploy_mode = $("input[name='deploy_mode']:checked").val(); 73 | if(deploy_mode == 0){ 74 | refresh_branches(); 75 | }else{ 76 | refresh_tags(); 77 | } 78 | }); 79 | $("#branches").change(refresh_commits); 80 | $("#submit").click(function () { 81 | var deploy_mode = $("input[name='deploy_mode']:checked").val(); 82 | var data; 83 | if(deploy_mode == 0){ 84 | data = {"mode": 0, 85 | "branch": $("#branches").val(), 86 | "commit" : $("#commits").val()}; 87 | }else{ 88 | data = {"mode": 1, 89 | "tag": $("#tags").val()}; 90 | } 91 | alert("running"); 92 | console.log(data); 93 | create_deploy( 94 | $("#projects").val(), 95 | $("#hosts").val(), 96 | data, 97 | function (data) { 98 | check_return(data); 99 | var id = data["data"]["id"]; 100 | window.location.assign('/deploys/'+id.toString()+'/progress') 101 | } 102 | ); 103 | }); 104 | $("input[name='deploy_mode']").click(function(){ 105 | var deploy_mode = $("input[name='deploy_mode']:checked").val(); 106 | if(deploy_mode == 0){ 107 | $(".deploy_branch_mode").show(); 108 | $(".deploy_tag_mode").hide(); 109 | refresh_branches(); 110 | }else{ 111 | $(".deploy_branch_mode").hide(); 112 | $(".deploy_tag_mode").show(); 113 | refresh_tags(); 114 | } 115 | }); 116 | }) 117 | -------------------------------------------------------------------------------- /web/static/js/pydelo/deploy_progress.js: -------------------------------------------------------------------------------- 1 | $.extend({ 2 | progress: function(deploy_id){ 3 | get_deploy_progress(deploy_id, function(data){ 4 | check_return(data); 5 | var data = data["data"]; 6 | var width = "width: "+data["progress"].toString()+"%;"; 7 | $("#progress-bar").attr("style", width); 8 | if (data["status"] == 2){ 9 | $("#progress-status").text("running"); 10 | $("#progress-msg").text(data["comment"]); 11 | setTimeout("$.progress("+deploy_id+")",3000); 12 | } else if (data["status"] == 3){ 13 | $("#progress-status").text("waiting"); 14 | $("#progress-msg").text(data["comment"]); 15 | setTimeout("$.progress("+deploy_id+")",3000); 16 | } else if (data["status"] == 1){ 17 | $("#progress-status").text("success"); 18 | $("#progress-msg").text(data["comment"]); 19 | } else { 20 | $("#progress-status").text("fail"); 21 | $("#progress-msg").text(data["comment"]); 22 | } 23 | }); 24 | } 25 | }); 26 | $(document).ready(function() { 27 | var vars = get_url_vars(); 28 | $.progress(vars["deploys"]); 29 | }) 30 | -------------------------------------------------------------------------------- /web/static/js/pydelo/deploys.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | vars = get_url_vars(); 3 | if (typeof(vars["offset"]) == "undefined"){ 4 | vars["offset"] = 0 5 | }else{ 6 | vars["offset"] = parseInt(vars["offset"]) 7 | } 8 | if (typeof(vars["limit"]) == "undefined"){ 9 | vars["limit"] = 10 10 | }else{ 11 | vars["limit"] = parseInt(vars["limit"]) 12 | } 13 | $("table tbody").empty(); 14 | get_deploys(function (data) { 15 | check_return(data); 16 | var data = data["data"]; 17 | $.each(data["deploys"], function(i, n) { 18 | var tr = $(""); 19 | tr.append($("").text(n["user"]["name"])); 20 | tr.append($("").text(n["project"]["name"])); 21 | tr.append($("").text(n["branch"])); 22 | tr.append($("").text(n["version"])); 23 | if (n["status"] == 1) { 24 | tr.append($("").text("success")); 25 | } else if(n["status"] == 0) { 26 | tr.append($("").text("fail")); 27 | } else if(n["status"] == 2) { 28 | tr.append($("").text("running")); 29 | } else if(n["status"] == 3){ 30 | tr.append($("").text("waiting")); 31 | } else { 32 | tr.append($("").text("unkown")); 33 | } 34 | tr.append($("").text(n["updated_at"])); 35 | var action_td = $(""); 36 | action_td.append($("info")); 37 | if (n["status"] == 1){ 38 | action_td.append($("¦")); 39 | action_td.append($("rollback")); 40 | } else if(n["status"] == 0){ 41 | action_td.append($("¦")); 42 | action_td.append($("redeploy")); 43 | } 44 | tr.append(action_td); 45 | $("table tbody").append(tr); 46 | }); 47 | $(".pagination").empty(); 48 | for(var i=1, offset=0; offset < data["count"]; i++){ 49 | $(".pagination").append($("
  • "+i.toString()+"
  • ")); 50 | offset += vars["limit"]; 51 | } 52 | }, vars["offset"], vars["limit"]); 53 | $("tbody").delegate(".rollback", "click", function () { 54 | var deploy_id = $(this).attr("deploy_id"); 55 | alert("running"); 56 | update_deploy_by_id( 57 | deploy_id, 58 | {"action": "rollback"}, 59 | function(data){ 60 | check_return(data); 61 | var deploy_id = data["data"]["id"] 62 | window.location.assign('/deploys/'+deploy_id.toString()+'/progress') 63 | }); 64 | }); 65 | $("tbody").delegate(".redeploy", "click", function () { 66 | var deploy_id = $(this).attr("deploy_id"); 67 | alert("running"); 68 | update_deploy_by_id( 69 | deploy_id, 70 | {"action": "redeploy"}, 71 | function(data){ 72 | check_return(data); 73 | var deploy_id = data["data"]["id"] 74 | window.location.assign('/deploys/'+deploy_id.toString()+'/progress') 75 | }); 76 | }); 77 | $("tbody").delegate(".info", "click", function () { 78 | var deploy_id = $(this).attr("deploy_id"); 79 | window.location.assign('/deploys/'+deploy_id.toString()+'/progress') 80 | }); 81 | }) 82 | -------------------------------------------------------------------------------- /web/static/js/pydelo/host_create.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $("#submit").click(function(e){ 3 | create_host( 4 | {"name": $("#name").val(), "ssh_host": $("#ssh_host").val(), "ssh_port": $("#ssh_port").val(), "ssh_method": 0, "ssh_user": $("#ssh_user").val(), "ssh_pass": $("#ssh_pass").val()}, 5 | function(data){ 6 | check_return(data); 7 | window.location.assign('/hosts'); 8 | }); 9 | }); 10 | }) 11 | -------------------------------------------------------------------------------- /web/static/js/pydelo/host_detail.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | vars = get_url_vars(); 3 | get_host_by_id(vars["hosts"], function(data){ 4 | var data=data["data"]; 5 | $("#name").attr("value", data["name"]); 6 | $("#ssh_host").attr("value", data["ssh_host"]); 7 | $("#ssh_port").attr("value", data["ssh_port"]); 8 | $("#ssh_user").attr("value", data["ssh_user"]); 9 | $("#ssh_method").val(data["ssh_method"]); 10 | $("#ssh_pass").attr("value", data["ssh_pass"]); 11 | }); 12 | $("#submit").click(function(e){ 13 | update_host_by_id( 14 | vars["hosts"], 15 | {"name": $("#name").val(), "ssh_host": $("#ssh_host").val(), 16 | "ssh_port": $("#ssh_port").val(), 17 | "ssh_user": $("#ssh_user").val(), 18 | "ssh_method": $("#ssh_method").val(), 19 | "ssh_pass": $("#ssh_pass").val() 20 | }, 21 | function(data){ 22 | check_return(data); 23 | window.location.reload(); 24 | }); 25 | }); 26 | }) 27 | -------------------------------------------------------------------------------- /web/static/js/pydelo/hosts.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | vars = get_url_vars(); 3 | if (typeof(vars["offset"]) == "undefined"){ 4 | vars["offset"] = 0 5 | }else{ 6 | vars["offset"] = parseInt(vars["offset"]) 7 | } 8 | if (typeof(vars["limit"]) == "undefined"){ 9 | vars["limit"] = 10 10 | }else{ 11 | vars["limit"] = parseInt(vars["limit"]) 12 | } 13 | $("table tbody").empty(); 14 | get_hosts(function (data) { 15 | check_return(data); 16 | var data=data["data"]; 17 | $.each(data["hosts"], function(i, n) { 18 | var tr = $(""); 19 | tr.append($("").text(n["name"])); 20 | tr.append($("").text(n["ssh_host"])); 21 | //tr.append($("").append($("detail  group"))); 22 | tr.append($("").append($("detail"))); 23 | $("table tbody").append(tr); 24 | }); 25 | $(".pagination").empty(); 26 | for(var i=1, offset=0; offset < data["count"]; i++){ 27 | $(".pagination").append($("
  • "+i.toString()+"
  • ")); 28 | offset += vars["limit"]; 29 | } 30 | }, vars["offset"], vars["limit"]); 31 | }) 32 | -------------------------------------------------------------------------------- /web/static/js/pydelo/login.js: -------------------------------------------------------------------------------- 1 | 2 | $(document).ready(function() { 3 | $("#submit").click(function () { 4 | login( 5 | {"username": $("#username").val(), 6 | "password" : $("#password").val()}, 7 | function (result) { 8 | check_return(result); 9 | var data = result["data"]; 10 | $.cookie('sign', data["sign"], { expires: 1, path: '/' }); 11 | window.location.assign('/') 12 | } 13 | ); 14 | }); 15 | }) 16 | -------------------------------------------------------------------------------- /web/static/js/pydelo/project_create.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $("#submit").click(function(e){ 3 | create_project( 4 | { "name": $("#name").val(), 5 | "repo_url": $("#repo_url").val(), 6 | "checkout_dir": $("#checkout_dir").val(), 7 | "target_dir": $("#target_dir").val(), 8 | "deploy_dir": $("#deploy_dir").val(), 9 | "deploy_history_dir": $("#deploy_history_dir").val(), 10 | "before_checkout": $("#before_checkout").val(), 11 | "after_checkout": $("#after_checkout").val(), 12 | "before_deploy": $("#before_deploy").val(), 13 | "after_deploy": $("#after_deploy").val(), 14 | }, 15 | function(data){ 16 | check_return(data); 17 | window.location.assign('/projects') 18 | }); 19 | }); 20 | }) 21 | -------------------------------------------------------------------------------- /web/static/js/pydelo/project_detail.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | vars = get_url_vars(); 3 | get_project_by_id(vars["projects"], function(data){ 4 | check_return(data); 5 | var data=data["data"]; 6 | $("#name").attr("value", data["name"]); 7 | $("#repo_url").attr("value", data["repo_url"]); 8 | $("#checkout_dir").attr("value", data["checkout_dir"]); 9 | $("#target_dir").attr("value", data["target_dir"]); 10 | $("#deploy_dir").attr("value", data["deploy_dir"]); 11 | $("#deploy_history_dir").attr("value", data["deploy_history_dir"]); 12 | $("#before_checkout").text(data["before_checkout"]); 13 | $("#after_checkout").text(data["after_checkout"]); 14 | $("#before_deploy").text(data["before_deploy"]); 15 | $("#after_deploy").text(data["after_deploy"]); 16 | }); 17 | $("#submit").click(function(e){ 18 | update_project_by_id( 19 | vars["projects"], 20 | { "name": $("#name").val(), 21 | "repo_url": $("#repo_url").val(), 22 | "checkout_dir": $("#checkout_dir").val(), 23 | "target_dir": $("#target_dir").val(), 24 | "deploy_dir": $("#deploy_dir").val(), 25 | "deploy_history_dir": $("#deploy_history_dir").val(), 26 | "before_checkout": $("#before_checkout").val(), 27 | "after_checkout": $("#after_checkout").val(), 28 | "before_deploy": $("#before_deploy").val(), 29 | "after_deploy": $("#after_deploy").val(), 30 | }, 31 | function(data){ 32 | check_return(data); 33 | alert("OK"); 34 | //window.location.reload(); 35 | }); 36 | }); 37 | }) 38 | -------------------------------------------------------------------------------- /web/static/js/pydelo/projects.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | vars = get_url_vars(); 3 | if (typeof(vars["offset"]) == "undefined"){ 4 | vars["offset"] = 0 5 | }else{ 6 | vars["offset"] = parseInt(vars["offset"]) 7 | } 8 | if (typeof(vars["limit"]) == "undefined"){ 9 | vars["limit"] = 10 10 | }else{ 11 | vars["limit"] = parseInt(vars["limit"]) 12 | } 13 | $("table tbody").empty(); 14 | get_projects(vars["offset"], vars["limit"], function (data) { 15 | check_return(data); 16 | var data=data["data"]; 17 | $.each(data["projects"], function(i, n) { 18 | var tr = $(""); 19 | tr.append($("").text(n["name"])); 20 | //tr.append($("").append($("detail  group"))); 21 | tr.append($("").append($("detail"))); 22 | $("table tbody").append(tr); 23 | }); 24 | $(".pagination").empty(); 25 | for(var i=1, offset=0; offset < data["count"]; i++){ 26 | $(".pagination").append($("
  • "+i.toString()+"
  • ")); 27 | offset += vars["limit"]; 28 | } 29 | }); 30 | }) 31 | -------------------------------------------------------------------------------- /web/static/js/pydelo/user_create.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $("#submit").click(function(e){ 3 | create_user( 4 | {"name": $("#name").val(), "password": $("#password").val(), "email": $("#email").val(), "phone": $("#phone").val()}, 5 | function(data){ 6 | check_return(data); 7 | window.location.assign('/users'); 8 | }); 9 | }); 10 | }) 11 | -------------------------------------------------------------------------------- /web/static/js/pydelo/user_hosts.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | vars = get_url_vars(); 3 | get_user_hosts(vars["users"], function(data){ 4 | check_return(data); 5 | var hosts = []; 6 | $.each(data["data"]["hosts"], function(){hosts.push({"text": this["name"], "value": this["id"]})}); 7 | $("#hosts_selected").empty(); 8 | append_option_to_select(hosts, $("#hosts_selected")); 9 | }); 10 | get_hosts(function(data){ 11 | check_return(data); 12 | var users = []; 13 | $.each(data["data"]["hosts"], function(){users.push({"text": this["name"], "value": this["id"]})}); 14 | $("#hosts").empty(); 15 | append_option_to_select(users, $("#hosts")); 16 | }); 17 | $("#add_hosts").click(function(e){ 18 | var selected = []; 19 | var to_add = []; 20 | $.each($("#hosts_selected option"), function(){ 21 | selected.push(parseInt(this.value)); 22 | }); 23 | $.each($("#hosts option:selected"), function(){ 24 | if (! selected.includes(parseInt(this.value))){ 25 | to_add.push({"text": this.text, "value": this.value}); 26 | } 27 | }); 28 | append_option_to_select(to_add, $("#hosts_selected")); 29 | }); 30 | $("#remove_hosts").click(function(e){ 31 | $.each($("#hosts_selected option:selected"), function(){ 32 | this.remove(); 33 | }); 34 | }); 35 | $("#submit").click(function(e){ 36 | var selected = []; 37 | $.each($("#hosts_selected option"), function(){ 38 | selected.push(this.value); 39 | }); 40 | update_user_hosts(vars["users"], {"hosts": selected}, function(data){ 41 | check_return(data); 42 | }); 43 | }); 44 | }) 45 | -------------------------------------------------------------------------------- /web/static/js/pydelo/user_projects.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | vars = get_url_vars(); 3 | get_user_projects(vars["users"], function(data){ 4 | check_return(data); 5 | var projects = []; 6 | $.each(data["data"]["projects"], function(){projects.push({"text": this["name"], "value": this["id"]})}); 7 | $("#projects_selected").empty(); 8 | append_option_to_select(projects, $("#projects_selected")); 9 | }); 10 | get_projects(function(data){ 11 | check_return(data); 12 | var users = []; 13 | $.each(data["data"]["projects"], function(){users.push({"text": this["name"], "value": this["id"]})}); 14 | $("#projects").empty(); 15 | append_option_to_select(users, $("#projects")); 16 | }); 17 | $("#add_projects").click(function(e){ 18 | var selected = []; 19 | var to_add = []; 20 | $.each($("#projects_selected option"), function(){ 21 | selected.push(parseInt(this.value)); 22 | }); 23 | $.each($("#projects option:selected"), function(){ 24 | if (! selected.includes(parseInt(this.value))){ 25 | to_add.push({"text": this.text, "value": this.value}); 26 | } 27 | }); 28 | append_option_to_select(to_add, $("#projects_selected")); 29 | }); 30 | $("#remove_projects").click(function(e){ 31 | $.each($("#projects_selected option:selected"), function(){ 32 | this.remove(); 33 | }); 34 | }); 35 | $("#submit").click(function(e){ 36 | var selected = []; 37 | $.each($("#projects_selected option"), function(){ 38 | selected.push(this.value); 39 | }); 40 | update_user_projects(vars["users"], {"projects": selected}, function(data){ 41 | check_return(data); 42 | }); 43 | }); 44 | }) 45 | -------------------------------------------------------------------------------- /web/static/js/pydelo/users.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | vars = get_url_vars(); 3 | if (typeof(vars["offset"]) == "undefined"){ 4 | vars["offset"] = 0 5 | }else{ 6 | vars["offset"] = parseInt(vars["offset"]) 7 | } 8 | if (typeof(vars["limit"]) == "undefined"){ 9 | vars["limit"] = 10 10 | }else{ 11 | vars["limit"] = parseInt(vars["limit"]) 12 | } 13 | $("table tbody").empty(); 14 | get_users(function (data) { 15 | check_return(data); 16 | var data=data["data"]; 17 | $.each(data["users"], function(i, n) { 18 | var tr = $(""); 19 | tr.append($("").text(n["name"])); 20 | var action = $(""); 21 | action.append($("hosts")); 22 | action.append($(" ¦ ")); 23 | action.append($("projects")); 24 | tr.append(action); 25 | $("table tbody").append(tr); 26 | }); 27 | $(".pagination").empty(); 28 | for(var i=1, offset=0; offset < data["count"]; i++){ 29 | $(".pagination").append($("
  • "+i.toString()+"
  • ")); 30 | offset += vars["limit"]; 31 | } 32 | }, vars["offset"], vars["limit"]); 33 | }) 34 | -------------------------------------------------------------------------------- /web/templates/account_change_password.html: -------------------------------------------------------------------------------- 1 | {% extends "blank.html" %} 2 | {% block head %} 3 | 4 | {% endblock %} 5 | {% block body %} 6 |
    7 |
    8 | 9 |
    10 | 11 | 12 |
    13 | 14 | 15 |
    16 |
    17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /web/templates/blank.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block head %}{% endblock %} 9 | pydelo 10 | 11 | 12 | 13 |
    14 |
    15 |
    16 |

    Pydelo

    17 |
    18 |
    19 |
    20 |
    21 | 22 | 23 | {% block body %}{% endblock %} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /web/templates/deploy_create.html: -------------------------------------------------------------------------------- 1 | {% extends "nav.html" %} 2 | {% block head %} 3 | 4 | {% endblock %} 5 | {% block content %} 6 |
    7 | 11 |
    12 |
    13 | 14 |
    15 |
    16 | 17 |
    18 |
    19 | 21 |
    22 |
    23 |
    24 |
    25 | 26 |
    27 |
    28 |
    29 | Branch 30 |
    31 |
    32 | Tag 33 |
    34 |
    35 |
    36 |
    37 |
    38 | 39 |
    40 |
    41 | 43 |
    44 |
    45 |
    46 |
    47 | 48 |
    49 |
    50 | 52 |
    53 |
    54 | 63 |
    64 |
    65 | 66 |
    67 |
    68 | 70 |
    71 |
    72 | 73 | 74 |
    75 | {% endblock %} 76 | -------------------------------------------------------------------------------- /web/templates/deploy_progress.html: -------------------------------------------------------------------------------- 1 | {% extends "nav.html" %} 2 | {% block head %} 3 | 4 | {% endblock %} 5 | {% block content %} 6 |
    7 | 11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 | 18 |
    19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /web/templates/deploys.html: -------------------------------------------------------------------------------- 1 | {% extends "nav.html" %} 2 | {% block head %} 3 | 4 | {% endblock %} 5 | {% block content %} 6 |
    7 | 10 |
    11 |
    12 | New deploy 13 |
    14 |
    15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
    userprojectbranchversionstatustimeaction
    31 |
    32 |
    33 |
      34 |
    35 |
    36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /web/templates/host_create.html: -------------------------------------------------------------------------------- 1 | {% extends "nav.html" %} 2 | {% block head %} 3 | 4 | {% endblock %} 5 | {% block content %} 6 |
    7 | 11 |
    12 |
    13 | 14 |
    15 |
    16 | 17 |
    18 |
    19 | 20 |
    21 |
    22 |
    23 |
    24 | 25 |
    26 |
    27 | 28 |
    29 |
    30 |
    31 |
    32 | 33 |
    34 |
    35 | 36 |
    37 |
    38 |
    39 |
    40 | 41 |
    42 |
    43 | 44 |
    45 |
    46 |
    47 |
    48 | 49 |
    50 |
    51 | 52 |
    53 |
    54 | 55 | 56 |
    57 | {% endblock %} 58 | -------------------------------------------------------------------------------- /web/templates/host_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "nav.html" %} 2 | {% block head %} 3 | 4 | {% endblock %} 5 | {% block content %} 6 |
    7 | 11 |
    12 |
    13 | 14 |
    15 |
    16 | 17 |
    18 |
    19 | 20 |
    21 |
    22 |
    23 |
    24 | 25 |
    26 |
    27 | 28 |
    29 |
    30 |
    31 |
    32 | 33 |
    34 |
    35 | 36 |
    37 |
    38 |
    39 |
    40 | 41 |
    42 |
    43 | 47 |
    48 |
    49 |
    50 |
    51 | 52 |
    53 |
    54 | 55 |
    56 |
    57 |
    58 |
    59 | 60 |
    61 |
    62 | 63 |
    64 |
    65 | 66 | 67 |
    68 | {% endblock %} 69 | -------------------------------------------------------------------------------- /web/templates/hosts.html: -------------------------------------------------------------------------------- 1 | {% extends "nav.html" %} 2 | {% block head %} 3 | 4 | {% endblock %} 5 | {% block content %} 6 |
    7 | 10 |
    11 |
    12 | Add host 13 |
    14 |
    15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
    namessh_hostaction
    26 |
    27 |
    28 |
      29 |
    30 |
    31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /web/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "blank.html" %} 2 | {% block head %} 3 | 4 | 5 | {% endblock %} 6 | {% block body %} 7 |
    8 |
    9 |
    10 | 11 | 12 |
    13 |
    14 | 15 | 16 |
    17 | 18 |
    19 |
    20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /web/templates/nav.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block head %}{% endblock %} 9 | 10 | pydelo 11 | 12 | 13 |
    14 |
    15 |
    16 |

    Pydelo

    17 |
    18 |
    19 | 30 |
    31 |
    32 |
    33 |
    34 |
    35 |
    36 |
    37 | 48 |
    49 |
    50 | {% block content %}{% endblock %} 51 |
    52 |
    53 |
    54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /web/templates/project_create.html: -------------------------------------------------------------------------------- 1 | {% extends "nav.html" %} 2 | {% block head %} 3 | 4 | {% endblock %} 5 | {% block content %} 6 |
    7 | 11 |
    12 |
    13 | 14 |
    15 |
    16 | 17 |
    18 |
    19 | 20 |
    21 |
    22 |
    23 |
    24 | 25 |
    26 |
    27 | 28 |
    29 |
    30 |
    31 |
    32 |
    33 | 34 |
    35 |
    36 | 37 |
    38 |
    39 |
    40 |
    41 | 42 |
    43 |
    44 | 45 |
    46 |
    47 |
    48 |
    49 | 50 |
    51 |
    52 | 53 |
    54 |
    55 |
    56 |
    57 | 58 |
    59 |
    60 | 61 |
    62 |
    63 |
    64 |
    65 |
    66 | 67 |
    68 |
    69 | 70 |
    71 |
    72 |
    73 |
    74 | 75 |
    76 |
    77 | 78 |
    79 |
    80 |
    81 |
    82 |
    83 | 84 |
    85 |
    86 | 87 |
    88 |
    89 |
    90 |
    91 | 92 |
    93 |
    94 | 95 |
    96 |
    97 | 98 | 99 |
    100 | {% endblock %} 101 | -------------------------------------------------------------------------------- /web/templates/project_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "nav.html" %} 2 | {% block head %} 3 | 4 | {% endblock %} 5 | {% block content %} 6 |
    7 | 11 |
    12 |
    13 | 14 |
    15 |
    16 | 17 |
    18 |
    19 | 20 |
    21 |
    22 |
    23 |
    24 | 25 |
    26 |
    27 | 28 |
    29 |
    30 |
    31 |
    32 |
    33 | 34 |
    35 |
    36 | 37 |
    38 |
    39 |
    40 |
    41 | 42 |
    43 |
    44 | 45 |
    46 |
    47 |
    48 |
    49 | 50 |
    51 |
    52 | 53 |
    54 |
    55 |
    56 |
    57 | 58 |
    59 |
    60 | 61 |
    62 |
    63 |
    64 |
    65 |
    66 | 67 |
    68 |
    69 | 70 |
    71 |
    72 |
    73 |
    74 | 75 |
    76 |
    77 | 78 |
    79 |
    80 |
    81 |
    82 |
    83 | 84 |
    85 |
    86 | 87 |
    88 |
    89 |
    90 |
    91 | 92 |
    93 |
    94 | 95 |
    96 |
    97 | 98 | 99 |
    100 | {% endblock %} 101 | -------------------------------------------------------------------------------- /web/templates/projects.html: -------------------------------------------------------------------------------- 1 | {% extends "nav.html" %} 2 | {% block head %} 3 | 4 | {% endblock %} 5 | {% block content %} 6 |
    7 | 10 |
    11 |
    12 | Add project 13 |
    14 |
    15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
    nameaction
    26 |
    27 |
    28 |
      29 |
    30 |
    31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /web/templates/user_create.html: -------------------------------------------------------------------------------- 1 | {% extends "nav.html" %} 2 | {% block head %} 3 | 4 | {% endblock %} 5 | {% block content %} 6 |
    7 | 11 |
    12 |
    13 | 14 |
    15 |
    16 | 17 |
    18 |
    19 | 20 |
    21 |
    22 |
    23 |
    24 | 25 |
    26 |
    27 | 28 |
    29 |
    30 |
    31 |
    32 | 33 |
    34 |
    35 | 36 |
    37 |
    38 |
    39 |
    40 | 41 |
    42 |
    43 | 44 |
    45 |
    46 | 47 | 48 |
    49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /web/templates/user_hosts.html: -------------------------------------------------------------------------------- 1 | {% extends "nav.html" %} 2 | {% block head %} 3 | 4 | {% endblock %} 5 | {% block content %} 6 |
    7 | 11 |
    12 |
    13 | 14 |
    15 |
    16 | 18 |
    19 |
    20 |
    21 | 22 | 23 |
    24 |
    25 |
    26 | 28 |
    29 |
    30 | 31 | 32 |
    33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /web/templates/user_projects.html: -------------------------------------------------------------------------------- 1 | {% extends "nav.html" %} 2 | {% block head %} 3 | 4 | {% endblock %} 5 | {% block content %} 6 |
    7 | 11 |
    12 |
    13 | 14 |
    15 |
    16 | 18 |
    19 |
    20 |
    21 | 22 | 23 |
    24 |
    25 |
    26 | 28 |
    29 |
    30 | 31 | 32 |
    33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /web/templates/users.html: -------------------------------------------------------------------------------- 1 | {% extends "nav.html" %} 2 | {% block head %} 3 | 4 | {% endblock %} 5 | {% block content %} 6 |
    7 | 10 |
    11 |
    12 | Add user 13 |
    14 |
    15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
    nameaction
    26 |
    27 |
    28 |
      29 |
    30 |
    31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /web/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meanstrong/pydelo/74ba65b4150479ff6a4b8aec3c83401d24022056/web/utils/__init__.py -------------------------------------------------------------------------------- /web/utils/error.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | __author__ = 'Rocky Peng' 3 | 4 | 5 | class Error(Exception): 6 | MAPS = { 7 | 10000: "income parameters error", 8 | 10001: "project not exists.", 9 | 10002: "host not exists.", 10 | 10003: "user not exists.", 11 | 10004: "deploy permission denied.", 12 | 10005: "Incomplete parameter", 13 | # 远端shell部分 14 | 11000: "pre deploy shell called exception", 15 | 11001: "post deploy shell called exception", 16 | 11002: "restart shell called exception", 17 | 11003: "rsync called exception", 18 | # 本地shell部分 19 | 12000: "git repo clone exception", 20 | # 用户部分 21 | 13000: "username or password incorrect", 22 | 13001: "user not exists", 23 | } 24 | 25 | def __init__(self, rc, msg=None): 26 | self.rc = rc 27 | if msg is None: 28 | self.msg = self.MAPS[rc] 29 | else: 30 | self.msg = msg 31 | 32 | def __repr__(self): 33 | return "%s: %s" % (self.rc, self.msg) 34 | -------------------------------------------------------------------------------- /web/utils/git.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | # -*- coding:utf-8 -*- 3 | from web.utils.localshell import LocalShell 4 | from web.utils.error import Error 5 | from web.utils.log import Logger 6 | logger = Logger("web.utils.git") 7 | __author__ = 'Rocky Peng' 8 | 9 | 10 | class Git(object): 11 | 12 | def __init__(self, dest, url): 13 | self.dest = dest 14 | self.url = url 15 | 16 | def local_branch(self): 17 | shell = "cd {0} && git fetch -q -a && git branch".format(self.dest) 18 | stdout = LocalShell.check_output(shell, shell=True) 19 | stdout = stdout.strip().split("\n") 20 | stdout = [s.strip("* ") for s in stdout] 21 | return stdout 22 | 23 | def remote_branch(self): 24 | shell = "cd {0} && git fetch -q -a && git branch -r".format(self.dest) 25 | stdout = LocalShell.check_output(shell, shell=True) 26 | stdout = stdout.strip().split("\n") 27 | stdout = [s.strip(" ").split("/", 1)[1] for s in stdout if "->" not in 28 | s] 29 | return stdout 30 | 31 | def tag(self): 32 | shell = "cd {0} && git fetch -q -a && git tag".format(self.dest) 33 | stdout = LocalShell.check_output(shell, shell=True) 34 | if stdout: 35 | return stdout.strip().split("\n") 36 | else: 37 | return [] 38 | 39 | def log(self): 40 | shell = ("cd {0} && git log -20 --pretty=\"%h %an %s\"" 41 | ).format(self.dest) 42 | stdout = LocalShell.check_output(shell, shell=True) 43 | stdout = stdout.strip().split("\n") 44 | stdout = [s.split(" ", 2) for s in stdout] 45 | return [{"abbreviated_commit": s[0], 46 | "author_name": s[1], 47 | "subject": s[2]} 48 | for s in stdout] 49 | 50 | def clone(self): 51 | logger.debug("clone repo:") 52 | shell = ("mkdir -p {0} && cd {0} && git clone -q {1} ." 53 | ).format(self.dest, self.url) 54 | rc = LocalShell.call(shell, shell=True) 55 | 56 | # destination path '.' already exists and is not an empty directory. 57 | if rc == 128: 58 | shell = ("cd {0} && git clean -xdfq && git reset -q --hard && git " 59 | "remote update && git checkout -q master && git remote " 60 | "prune origin && git pull -q --all && git branch " 61 | "| grep -v \\* | xargs git branch -D").format(self.dest) 62 | rc = LocalShell.call(shell, shell=True) 63 | # branch name required 64 | if rc == 123: 65 | return 66 | if rc != 0: 67 | raise Error(12000) 68 | 69 | def checkout_tag(self, tag): 70 | logger.debug("checkout to tag: %s" % tag) 71 | LocalShell.check_call( 72 | "cd {0} && git checkout -q {1}".format(self.dest, tag), 73 | shell=True) 74 | 75 | def checkout_branch(self, branch, version=""): 76 | logger.debug("checkout branch:") 77 | if branch in self.local_branch(): 78 | LocalShell.check_call("cd {0} && git checkout -q {1} && git pull " 79 | "-q origin {1} && git reset --hard {2}" 80 | .format(self.dest, branch, version), 81 | shell=True) 82 | else: 83 | LocalShell.check_call("cd {0} && git checkout -q -b {1} -t " 84 | "origin/{1} && git pull -q origin {1} && " 85 | "git reset --hard {2}" 86 | .format(self.dest, branch, version), 87 | shell=True) 88 | -------------------------------------------------------------------------------- /web/utils/jsonencoder.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from flask.json import JSONEncoder as BaseJSONEncoder 3 | 4 | 5 | class JSONEncoder(BaseJSONEncoder): 6 | def default(self, obj): 7 | if isinstance(obj, JsonSerializer): 8 | return obj.to_json() 9 | return super(JSONEncoder, self).default(obj) 10 | 11 | 12 | class JsonSerializer(object): 13 | __json_hidden__ = None 14 | 15 | def to_json(self): 16 | hidden = self.__json_hidden__ or [] 17 | value = dict() 18 | for p in self.__mapper__.iterate_properties: 19 | if isinstance(getattr(self, p.key), datetime): 20 | value[p.key] = str(getattr(self, p.key)) 21 | else: 22 | value[p.key] = getattr(self, p.key) 23 | for key in hidden: 24 | value.pop(key, None) 25 | return value 26 | -------------------------------------------------------------------------------- /web/utils/localshell.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | # -*- coding:utf-8 -*- 3 | from subprocess import Popen, PIPE, CalledProcessError 4 | 5 | from web.utils.log import Logger 6 | logger = Logger("web.utils.localshell") 7 | __author__ = 'Rocky Peng' 8 | 9 | 10 | class LocalShell(object): 11 | 12 | @staticmethod 13 | def check_output(*args, **kargs): 14 | cmd = kargs.get("args") or args[0] 15 | logger.debug("local shell: %s" % cmd) 16 | process = Popen(*args, stdout=PIPE, stderr=PIPE, **kargs) 17 | stdout, stderr = process.communicate() 18 | stdout = stdout.decode("utf-8") 19 | stderr = stderr.decode("utf-8") 20 | rc = process.poll() 21 | logger.debug("rc: %d" % rc) 22 | logger.debug("stdout: %s" % stdout) 23 | logger.warn("stderr: %s" % stderr) 24 | if rc: 25 | raise CalledProcessError(rc, cmd, stdout) 26 | return stdout 27 | 28 | @staticmethod 29 | def call(*args, **kargs): 30 | cmd = kargs.get("args") or args[0] 31 | logger.debug("local shell: %s" % cmd) 32 | process = Popen(*args, stdout=PIPE, stderr=PIPE, **kargs) 33 | stdout, stderr = process.communicate() 34 | stdout = stdout.decode("utf-8") 35 | stderr = stderr.decode("utf-8") 36 | rc = process.poll() 37 | logger.debug("rc: %d" % rc) 38 | logger.debug("stdout: %s" % stdout) 39 | logger.warn("stderr: %s" % stderr) 40 | return rc 41 | 42 | @staticmethod 43 | def check_call(*args, **kargs): 44 | cmd = kargs.get("args") or args[0] 45 | logger.debug("local shell: %s" % cmd) 46 | process = Popen(*args, stdout=PIPE, stderr=PIPE, **kargs) 47 | stdout, stderr = process.communicate() 48 | stdout = stdout.decode("utf-8") 49 | stderr = stderr.decode("utf-8") 50 | rc = process.poll() 51 | logger.debug("rc: %d" % rc) 52 | logger.debug("stdout: %s" % stdout) 53 | logger.warn("stderr: %s" % stderr) 54 | if rc: 55 | raise CalledProcessError(rc, cmd, stdout+"\n"+stderr) 56 | return rc 57 | -------------------------------------------------------------------------------- /web/utils/log.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | """ 3 | 公共log模块 4 | v1.0.0: 5 | 实现公共log模块 6 | v1.0.1: 7 | 判断logger属性是否已经有handler,若有则不再需要增加handler 8 | """ 9 | import logging 10 | import platform 11 | __version__ = '1.0.1' 12 | __author__ = 'Rocky Peng' 13 | if platform.system() == 'Windows': 14 | from ctypes import windll, c_ulong 15 | 16 | def color_text_decorator(function): 17 | def real_func(self, string): 18 | windll.Kernel32.GetStdHandle.restype = c_ulong 19 | h = windll.Kernel32.GetStdHandle(c_ulong(0xfffffff5)) 20 | if function.__name__.upper() == 'ERROR': 21 | windll.Kernel32.SetConsoleTextAttribute(h, 12) 22 | elif function.__name__.upper() == 'WARN': 23 | windll.Kernel32.SetConsoleTextAttribute(h, 13) 24 | elif function.__name__.upper() == 'INFO': 25 | windll.Kernel32.SetConsoleTextAttribute(h, 14) 26 | elif function.__name__.upper() == 'DEBUG': 27 | windll.Kernel32.SetConsoleTextAttribute(h, 15) 28 | else: 29 | windll.Kernel32.SetConsoleTextAttribute(h, 15) 30 | function(self, string) 31 | windll.Kernel32.SetConsoleTextAttribute(h, 15) 32 | return real_func 33 | else: 34 | def color_text_decorator(function): 35 | def real_func(self, string): 36 | if function.__name__.upper() == 'ERROR': 37 | self.stream.write('\033[0;31;40m') 38 | elif function.__name__.upper() == 'WARN': 39 | self.stream.write('\033[0;35;40m') 40 | elif function.__name__.upper() == 'INFO': 41 | self.stream.write('\033[0;33;40m') 42 | elif function.__name__.upper() == 'DEBUG': 43 | self.stream.write('\033[0;37;40m') 44 | else: 45 | self.stream.write('\033[0;37;40m') 46 | function(self, string) 47 | self.stream.write('\033[0m') 48 | return real_func 49 | 50 | 51 | def singleton(cls, *args, **kw): 52 | instances = {} 53 | 54 | def _singleton(*args, **kw): 55 | if cls not in instances: 56 | instances[cls] = cls(*args, **kw) 57 | instances[cls].__init__(*args, **kw) 58 | return instances[cls] 59 | return _singleton 60 | 61 | 62 | FORMAT = '[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s' 63 | 64 | 65 | class Logger(object): 66 | DEBUG_MODE = False 67 | GLOBAL_FILENAME = 'default.log' 68 | 69 | def __init__(self, name, filename=None): 70 | self.logger = logging.getLogger(name) 71 | if len(self.logger.handlers): 72 | raise Exception("Duplicate logger names!!!") 73 | self.logger.setLevel(logging.DEBUG) 74 | formatter = logging.Formatter(FORMAT) 75 | 76 | sh = logging.StreamHandler() 77 | sh.setFormatter(formatter) 78 | 79 | # sh.setLevel(logging.DEBUG) 80 | sh.setLevel(logging.DEBUG if self.DEBUG_MODE else logging.INFO) 81 | self.logger.addHandler(sh) 82 | self.stream = sh.stream 83 | 84 | if self.GLOBAL_FILENAME: 85 | fh_all = logging.FileHandler(self.GLOBAL_FILENAME, 'a') 86 | fh_all.setFormatter(formatter) 87 | fh_all.setLevel(logging.DEBUG) 88 | self.logger.addHandler(fh_all) 89 | 90 | if filename is not None: 91 | fh = logging.FileHandler(filename, 'a') 92 | fh.setFormatter(formatter) 93 | fh.setLevel(logging.DEBUG) 94 | self.logger.addHandler(fh) 95 | 96 | @color_text_decorator 97 | def debug(self, string): 98 | return self.logger.debug(string) 99 | 100 | @color_text_decorator 101 | def info(self, string): 102 | return self.logger.info(string) 103 | 104 | @color_text_decorator 105 | def warn(self, string): 106 | return self.logger.warn(string) 107 | 108 | @color_text_decorator 109 | def error(self, string): 110 | return self.logger.error(string) 111 | 112 | 113 | if __name__ == '__main__': 114 | Logger.DEBUG_MODE = True 115 | class A: 116 | def __init__(self): 117 | self.logger = Logger(self.__class__.__name__) 118 | def log(self, msg): 119 | self.logger.debug(msg) 120 | class B: 121 | def __init__(self): 122 | self.logger = Logger(self.__class__.__name__) 123 | def log(self, msg): 124 | self.logger.debug(msg) 125 | a = A() 126 | b = B() 127 | a.log("1111111111111111111111") 128 | b.log("2222222222222222222") 129 | c = A() 130 | c.log("3333333333333333333333333") 131 | -------------------------------------------------------------------------------- /web/utils/mysql.py: -------------------------------------------------------------------------------- 1 | import MySQLdb 2 | import pymysql 3 | import config 4 | 5 | from log import Logger 6 | logger = Logger("web.utils.mysql") 7 | 8 | 9 | class PyMySQL(object): 10 | def __init__(self, host, port, user, passwd, db): 11 | self.host = host 12 | self.port = port 13 | self.user = user 14 | self.passwd = passwd 15 | self.db = db 16 | 17 | self.connect() 18 | self.conn.autocommit(1) 19 | self.cursor = self.conn.cursor() 20 | 21 | def connect(self): 22 | self.conn = pymysql.connect( 23 | host=self.host, 24 | port=self.port, 25 | user=self.user, 26 | passwd=self.passwd, 27 | db=self.db, 28 | charset="utf8") 29 | 30 | sql_conn = PyMySQL( 31 | host=config.DB_HOST, 32 | user=config.DB_USER, 33 | passwd=config.DB_PASS, 34 | port=config.DB_PORT, 35 | db=config.DB_NAME) 36 | -------------------------------------------------------------------------------- /web/utils/remoteshell.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | # -*- coding:utf-8 -*- 3 | import time 4 | import paramiko 5 | from subprocess import CalledProcessError 6 | 7 | from web.utils.log import Logger 8 | logger = Logger("web.utils.remoteshell") 9 | __author__ = 'Rocky Peng' 10 | 11 | 12 | class RemoteShell(object): 13 | 14 | def __init__(self, host, port, user, passwd): 15 | self.host = host 16 | self.port = port 17 | self.user = user 18 | self.passwd = passwd 19 | self.connect() 20 | 21 | def connect(self): 22 | self.ssh = paramiko.SSHClient() 23 | self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 24 | self.ssh.connect(self.host, self.port, self.user, self.passwd, 25 | timeout=10) 26 | 27 | def exec_command(self, shell): 28 | logger.debug("remote shell: %s" % shell) 29 | # stdin, stdout, stderr = self.ssh.exec_command(shell) 30 | chan = self.ssh.get_transport().open_session() 31 | chan.exec_command(shell) 32 | buff_size = 1024 33 | stdout = "" 34 | stderr = "" 35 | while not chan.exit_status_ready(): 36 | time.sleep(1) 37 | if chan.recv_ready(): 38 | stdout += chan.recv(buff_size) 39 | if chan.recv_stderr_ready(): 40 | stderr += chan.recv_stderr(buff_size) 41 | exit_status = chan.recv_exit_status() 42 | # Need to gobble up any remaining output after program terminates... 43 | while chan.recv_ready(): 44 | stdout += chan.recv(buff_size) 45 | while chan.recv_stderr_ready(): 46 | stderr += chan.recv_stderr(buff_size) 47 | logger.debug("rc: %d" % exit_status) 48 | logger.debug("stdout: %s" % stdout) 49 | logger.warn("stderr: %s" % stderr) 50 | return exit_status, stdout, stderr 51 | 52 | def check_call(self, shell): 53 | rc, stdout, stderr = self.exec_command(shell) 54 | if rc: 55 | raise CalledProcessError(rc, shell, stdout+"\n"+stderr) 56 | return rc 57 | 58 | def close(self): 59 | self.ssh.close() 60 | --------------------------------------------------------------------------------