├── .gitignore ├── AUTHORS.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── README.md ├── Vagrantfile ├── docs ├── 12factor.rst ├── Makefile ├── cli.rst ├── conf.py ├── configuration.rst ├── directory.rst ├── index.rst ├── installation.rst ├── make.bat ├── plugin.rst ├── quick_start.rst └── signals.rst ├── examples ├── curl-example.yml ├── git-example.yml ├── git-example │ └── deploy.yml ├── smallest-example │ └── deploy.yml └── supervisor-example │ ├── app.py │ ├── configs │ └── supervisor_staging_app.conf │ ├── deploy.yml │ └── requirements.txt ├── fapistrano ├── __init__.py ├── app.py ├── cli.py ├── configuration.py ├── deploy.py ├── directory.py ├── plugins │ ├── __init__.py │ ├── curl.py │ ├── fis.py │ ├── git.py │ ├── git2slack.py │ ├── localshared.py │ ├── slack.py │ ├── supervisorctl.py │ ├── virtualenv.py │ ├── virtualenvtools.py │ └── virtualenvwrapper.py ├── signal.py └── utils.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/eda2ee70de1bf7db7e0aba7c7b71aeb2c1c7eeec/Global/OSX.gitignore 2 | 3 | .DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | 7 | # Icon must ends with two \r. 8 | Icon 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | # Files that might appear on external disk 14 | .Spotlight-V100 15 | .Trashes 16 | 17 | 18 | ### https://raw.github.com/github/gitignore/eda2ee70de1bf7db7e0aba7c7b71aeb2c1c7eeec/Python.gitignore 19 | 20 | # Byte-compiled / optimized / DLL files 21 | __pycache__/ 22 | *.py[cod] 23 | 24 | # C extensions 25 | *.so 26 | 27 | # Distribution / packaging 28 | .Python 29 | env/ 30 | bin/ 31 | build/ 32 | develop-eggs/ 33 | dist/ 34 | eggs/ 35 | lib/ 36 | lib64/ 37 | parts/ 38 | sdist/ 39 | var/ 40 | *.egg-info/ 41 | .installed.cfg 42 | *.egg 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .coverage 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | 56 | # Translations 57 | *.mo 58 | 59 | # Mr Developer 60 | .mr.developer.cfg 61 | .project 62 | .pydevproject 63 | 64 | # Rope 65 | .ropeproject 66 | 67 | # Django stuff: 68 | *.log 69 | *.pot 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | 75 | 76 | ### https://raw.github.com/github/gitignore/3d6f6f82101bcc6d79216c74a2bd1e7d61cfcda7/Global/Vagrant.gitignore 77 | 78 | .vagrant/ 79 | 80 | 81 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Ju Lin 9 | * Miao Hu 10 | 11 | Contributors 12 | ------------ 13 | 14 | * Jay 15 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | 5 | 0.9.2 (2017-03-20) 6 | ------------------ 7 | 8 | * fix contaminated outdated releases. 9 | 10 | 0.9.1 (2016-9-18) 11 | ------------------ 12 | 13 | * fix stale releases left on servers. 14 | 15 | 0.9.0 (2016-4-21) 16 | ------------------ 17 | 18 | * support different login user and sudo user. 19 | 20 | 0.8.3 (2016-4-18) 21 | ------------------ 22 | 23 | * fix missing output from curl. 24 | * fix .env format. 25 | * support formatted env for list/dict. 26 | 27 | 0.8.2 (2016-4-9) 28 | ------------------ 29 | 30 | * add `--dry-run` option. 31 | 32 | 0.8.1 (2016-4-1) 33 | ------------------ 34 | 35 | * hide curl password. 36 | 37 | 0.8.0 (2016-3-30) 38 | ------------------ 39 | 40 | * add command `once`. 41 | * add command `shell` (only support python now). 42 | * find plugins for each command if possible, fallback to default plugin. 43 | * add `.env` file at remote. 44 | 45 | 0.7.3 (2016-3-29) 46 | ------------------ 47 | 48 | * bugfix for cross env pollution. 49 | 50 | 0.7.2 (2016-3-29) 51 | ------------------ 52 | 53 | * cli support group deploy by role or stage. 54 | * add option `--curl-extract-tgz`. 55 | * add option `--git-archive-tree`. 56 | 57 | 0.7.1 (2016-3-25) 58 | ------------------ 59 | 60 | * fix fis option. 61 | * add option `--shared_writable`. 62 | * alert on missing localshared files. 63 | * add option `--curl-postinstall-output`. 64 | * add option `--curl-output`. 65 | 66 | 0.7.0 (2016-3-21) 67 | ------------------ 68 | 69 | * release to pypi. 70 | 71 | 0.6.0 (2016-3-15) 72 | ------------------ 73 | 74 | * move git, supervisor, slack as plugins. 75 | * put git bare repo on path. 76 | * add fis plugin. 77 | * add `fap` cli. 78 | * use signal to refactor release/rollback flow. 79 | * add curl plugin. 80 | * add supervisor plugin. 81 | 82 | 0.5.1 (2015-2-1) 83 | ------------------ 84 | 85 | * support supervisor group 86 | * slack notification. 87 | 88 | 0.1.0 (2015-9-23) 89 | ------------------ 90 | 91 | * First release. 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Liwushuo Inc. and contributors. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | 7 | recursive-include tests * 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | 11 | recursive-include docs *.rst conf.py Makefile make.bat 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fapistrano 2 | 3 | A remote server automation and deployment tool. 4 | 5 | * Document: [http://pythonhosted.org/fapistrano/](http://pythonhosted.org/fapistrano/) 6 | 7 | ## Install 8 | 9 | ``` bash 10 | pip install fapistrano 11 | ``` 12 | 13 | To upgrade 14 | 15 | ``` bash 16 | pip install -U fapistrano 17 | ``` 18 | 19 | ## How to Use 20 | 21 | ``` 22 | $ fap release --stage production --role web 23 | $ fap rollback --stage production --role web 24 | $ fap restart --stage production --role web 25 | ``` 26 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure(2) do |config| 5 | config.vm.box = "parallels/ubuntu-14.04" 6 | config.vm.box_check_update = false 7 | # config.vm.network "private_network", ip: "192.168.33.10" 8 | # config.vm.network "public_network" 9 | # config.vm.network "forwarded_port", guest: 80, host: 8080 10 | # config.vm.synced_folder "../data", "/vagrant_data" 11 | 12 | config.vm.provider "parallels" do |prl| 13 | prl.name = "fapistrano" 14 | prl.check_guest_tools = false 15 | prl.update_guest_tools = true 16 | prl.memory = 2048 17 | prl.cpus = 4 18 | end 19 | 20 | if Vagrant.has_plugin?("vagrant-cachier") 21 | config.cache.scope = :box 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /docs/12factor.rst: -------------------------------------------------------------------------------- 1 | 12 Factor 2 | ========= 3 | 4 | Fapistrano is not a silver-bullet, you can possibly mess up deployment process on wrongly 5 | configuring you application. 6 | 7 | Following the guideline of `The Twelve-Factor App` may reduce potential traps. 8 | 9 | Codebase 10 | --------- 11 | 12 | One codebase tracked in revision control, many deploys. 13 | 14 | Fapistrano archive that goal by configurating item `stage_role_configs`. 15 | Each stage can connected with several roles. 16 | 17 | For instance, an application is deployed to production but with several instance below. 18 | Codebase is `git@git.your-corp.com:owner/repo.git`. Deployment instances are `production + web`, 19 | `production + worker`, `production + cron`. These instances are running on different path 20 | on different servers, sharing different config.:: 21 | 22 | plugins: 23 | - fapistrano.git 24 | - fapistrano.supervisorctl 25 | repo: git@git.your-corp.com:owner/repo.git 26 | stage_role_configs: 27 | production: 28 | web: 29 | hosts: 30 | - app-web01 31 | - app-web02 32 | linked_files: 33 | - configs/supervisor_production_web.conf 34 | worker: 35 | hosts: 36 | - app-job01 37 | linked_files: 38 | - configs/supervisor_production_worker.conf 39 | cron: 40 | hosts: 41 | - app-job01 42 | linked_files: 43 | - configs/supervisor_production_cron.conf 44 | 45 | 46 | Dependencies 47 | ------------------ 48 | 49 | Explicity declare and isolate dependencies. 50 | 51 | Fapistrano archives that goal by loading property plugin. 52 | 53 | For example, if you loading a `fapistrnao.virtualenv` plugin, Fapistrano will create a `venv` 54 | directory as python execution environment:: 55 | 56 | plugins: 57 | - fapistrano.git 58 | - fapistrano.virtualenv 59 | stage_role_configs: 60 | production: 61 | web: 62 | virtualenv_requirements: '%(release_path)s/production-requirements.txt 63 | 64 | It assumes that you have a `production-requirements.txt` in your git repository. 65 | Once updating git repository, Fapistrano will run these commands:: 66 | 67 | $ virtualenv venv 68 | $ venv/bin/pip install -r production-requirements.txt 69 | 70 | WARNING: This is still not the recommend way to install dependencies for Python. 71 | Fetching dependencies and compiling binaries at build stage, bundling your codebase and 72 | wheel packages as deployment artifact may be a better practice. 73 | 74 | 75 | Config 76 | ------------------ 77 | 78 | 79 | Store config in the environment. 80 | 81 | DO NOT EVER COMMIT SECRETS INTO YOUR REPOSITORY. 82 | 83 | It is recommended to save your secrets at your shared folder and then link them on deploying:: 84 | 85 | plugins: 86 | - fapistrano.git 87 | stage_role_configs: 88 | production: 89 | web: 90 | linked_files: 91 | app/settings/production.py 92 | 93 | Load these linked files as configurations. They won't hurt you! 94 | 95 | Build, Release, Run 96 | ------------------- 97 | 98 | Strictly separate build and run stages. 99 | 100 | It's not recommended to write configs below:: 101 | 102 | # deploy.yml 103 | plugins: 104 | - fapistrano.git 105 | - fapistrano_webpack 106 | 107 | # fapistrano_webpack.py 108 | def init(): 109 | signal.register('deploy.updating', compile_static_resource) 110 | 111 | The reason is simple: build stage is totally different from release stage and run stage. 112 | It's not worth installing entire build infrastructure on your production servers. 113 | 114 | We prefer converting a code repo into an executable bundle first. It turned out simpler 115 | and faster to release your codebase to production.:: 116 | 117 | # deploy.yml 118 | plugins: 119 | - fapistrano.curl 120 | 121 | curl_extract_tgz: true 122 | curl_postinstall_script: "./install.sh" 123 | 124 | In the above, all you need to do is to pass a `--curl-url` option into `fap` command. 125 | Once artifact downloaded, Fapistrano will 126 | 127 | * Extract your final codes: python code, static resource compiled by webpack. 128 | * Run `./install.sh` which possibly create virtualenv and install python dependencies. (virtualenv and dependencies have been put into tgz) 129 | 130 | Processes 131 | ------------------- 132 | 133 | Execute the app as one or more stateless processes. 134 | 135 | Make sure your application is stateless and share-nothing. 136 | 137 | Your application is running in a easy-to-lost directory, since release directory can 138 | only be kept to at max number of `keep_releases`. 139 | 140 | If your have any persist data, commit them into database or write them into shared files:: 141 | 142 | # deploy.yml 143 | stage_role_configs: 144 | production: 145 | web: 146 | linked_files: 147 | - log/audio-transcoding.log 148 | - log/image-compress.log 149 | 150 | NOTICE: do not write supervisor log in shared, since they are written by `root` user. 151 | 152 | Concurrency 153 | ------------------- 154 | 155 | Scale out via the process model 156 | 157 | If you want to scale out your application, you can add a new host to `deploy.yml` definition:: 158 | 159 | stage_role_configs: 160 | production: 161 | web: 162 | hosts: 163 | - app-web01 164 | 165 | stage_role_configs: 166 | production: 167 | web: 168 | hosts: 169 | - app-web01 170 | - app-web02 171 | - app-web03 172 | 173 | Use your load balance infrastructure to route traffic to these applciation instance:: 174 | 175 | upstream app_servers { 176 | server app-web01:8080; 177 | server app-web02:8080; 178 | server app-web03:8080; 179 | } 180 | 181 | server { 182 | listen 80; 183 | server_name example.org; 184 | 185 | location / { 186 | proxy_redirect off; 187 | proxy_set_header Host $host; 188 | proxy_set_header X-Real-IP $remote_addr; 189 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 190 | proxy_pass http://app_servers; 191 | } 192 | } 193 | 194 | Disposability 195 | ------------------- 196 | 197 | Maximize robustness with fast startup and graceful shutdown. 198 | 199 | Starting or stoping your application should not take a long time for waiting. 200 | Few seconds are durable. 201 | 202 | It is recommended to rely on process manager, such as supervisor, to manage output stream, 203 | respond to crashed processes, and handle restarts and shutdowns:: 204 | 205 | plugins: 206 | - fapistrano.supervisorctl 207 | - fapistrano.git 208 | 209 | supervisor_check_status: true 210 | supervisor_output: true 211 | supervisor_refresh: false 212 | supervisor_conf: configs/supervisor_%(role)s.conf 213 | 214 | Dev/Prod parity 215 | --------------- 216 | 217 | Keep development, staging, and production as similar as possible. 218 | 219 | A typically Fapistrano way of Dev/Prod parity is to deploy same code but 220 | to symlink different config files.:: 221 | 222 | stage_role_configs: 223 | production: 224 | web: 225 | linked_files: 226 | - app/settings/production.py 227 | staging: 228 | web: 229 | linked_files: 230 | - app/settings/staging.py 231 | 232 | Admin Processes 233 | ---------------- 234 | 235 | Run admin/management tasks as one-off processes. 236 | 237 | It is recommended to commit your one-off scripts into your repository and treat it as 238 | a brand new release. A one-off goal may be archived by disabling supervisor pluging and 239 | customizing running endpoint. 240 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/fapistrano.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/fapistrano.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/fapistrano" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/fapistrano" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/cli.rst: -------------------------------------------------------------------------------- 1 | Command-Line Tool 2 | ================= 3 | 4 | Fapistrano offers a default command-line tool: `fap`:: 5 | 6 | $ fap --help 7 | Usage: fap [OPTIONS] COMMAND [ARGS]... 8 | 9 | Options: 10 | -d, --deployfile TEXT 11 | --help Show this message and exit. 12 | 13 | Commands: 14 | release 15 | restart 16 | rollback 17 | once 18 | shell 19 | 20 | global options 21 | --------------- 22 | 23 | * set `--dry-run=true` to debug the workflow but no task's really ran. 24 | 25 | `fap release` 26 | ------------- 27 | 28 | This command is designed to ship new deployments to you server. 29 | 30 | It do pretter little things: 31 | 32 | * Start 33 | * Create a new release directory under `releases_path`; 34 | * Create link files/directories in release directory, symlinking them to files/directories to `shared_path`; 35 | * Update 36 | * Default behaviour is blank; 37 | * Publish 38 | * Switch current release defined by `current_path` to newly created release directory; 39 | * Finish 40 | * Remove stale releases, according the number defined by `keep_releases`. 41 | 42 | Example:: 43 | 44 | [server01] Executing task 'deploy.release' 45 | ===> Starting 46 | [server01] run: mkdir -p /home/deploy/www/example/{releases,shared/log} 47 | [server01] run: chmod -R g+w /home/deploy/www/example/shared 48 | [server01] run: mkdir -p /home/deploy/www/example/releases/160314-085322 49 | ===> Started 50 | ===> Updating 51 | ===> Updated 52 | ===> Publishing 53 | [server01] run: ln -nfs /home/deploy/www/example/releases/160314-085322 current 54 | ===> Published 55 | ===> Finishing 56 | ===> Cleanning up old release(s) 57 | [server01] run: ls -x /home/deploy/www/example/releases 58 | [server01] run: rm -rf 160313-230707 59 | ===> Finished 60 | Done. 61 | Disconnecting from server:2333... done. 62 | 63 | `fap rollback` 64 | -------------- 65 | 66 | This command is designed to rollback to previously deployed release. 67 | 68 | * Start 69 | * Check if there is a rollback release, which is deployed before current release; 70 | * Define: 71 | * `rollback_from`: current_release; 72 | * `rollback_to`: release that is deployed previous than current release; 73 | * Update 74 | * Default behaviour is blank; 75 | * Publish 76 | * Switch current release defined by `current_path` to `rollback_to`; 77 | * Finish 78 | * Remove `rollback_from` release. 79 | 80 | 81 | Example:: 82 | 83 | [server01] Executing task 'deploy.release' 84 | ===> Starting 85 | [server01] run: mkdir -p /home/deploy/www/example/{releases,shared/log} 86 | [server01] run: chmod -R g+w /home/deploy/www/example/shared 87 | ===> Started 88 | ===> Updating 89 | ===> Updated 90 | ===> Publishing 91 | [server01] run: ln -nfs /home/deploy/www/example/releases/160314-083000 current 92 | ===> Published 93 | ===> Finishing 94 | ===> Cleanning up old release(s) 95 | [server01] run: rm -rf 160314-085322 96 | ===> Finished 97 | Done. 98 | Disconnecting from server:2333... done. 99 | 100 | `fap restart` 101 | ------------- 102 | 103 | This command is designed to restart your application. 104 | 105 | * Restart 106 | * Default behavious is blank. 107 | 108 | Example:: 109 | 110 | [server01] Executing task 'deploy.release' 111 | ===> Restarting 112 | ===> Restarted 113 | Done. 114 | Disconnecting from server:2333... done. 115 | 116 | `fap shell` 117 | ----------- 118 | 119 | This command is designed to start a REPL for your application. 120 | 121 | Both `--stage` and `--role` are required. 122 | 123 | NOTICE: currently only Python support:: 124 | 125 | $ fap shell --stage staging --role app 126 | Executing app at staging 127 | [server01] Executing task 'shell' 128 | [server01] run: venv/bin/ipython 129 | [server01] out: Python 2.7.10 (default, Jun 30 2015, 15:30:23) 130 | [server01] out: Type "copyright", "credits" or "license" for more information. 131 | [server01] out: 132 | [server01] out: IPython 4.1.2 -- An enhanced Interactive Python. 133 | [server01] out: ? -> Introduction and overview of IPython's features. 134 | [server01] out: %quickref -> Quick reference. 135 | [server01] out: help -> Python's own help system. 136 | [server01] out: object? -> Details about 'object', use 'object??' for extra details. 137 | [server01] out: 138 | [server01] out: In [1]: import os; print os.environ.get('ENV') 139 | [server01] out: stag 140 | [server01] out: 141 | [server01] out: In [2]: exit 142 | [server01] out: 143 | 144 | `fap once` 145 | ---------- 146 | 147 | This command is designed to run script for your application. 148 | 149 | Both `--stage` and `--role` are required. 150 | 151 | `fap once` needs additionaly option `--command`:: 152 | 153 | $ fap once --stage staging --role app --command='which scrapy' 154 | Executing app at staging 155 | [app-stag01] Executing task 'once' 156 | ===> Running 157 | [app-stag01] run: which scrapy 158 | [app-stag01] out: venv/bin/scrapy 159 | [app-stag01] out: 160 | 161 | ===> Ran 162 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # fapistrano documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Feb 1 10:33:58 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [] 32 | 33 | # Add any paths that contain templates here, relative to this directory. 34 | templates_path = ['_templates'] 35 | 36 | # The suffix(es) of source filenames. 37 | # You can specify multiple suffix as a list of string: 38 | # source_suffix = ['.rst', '.md'] 39 | source_suffix = '.rst' 40 | 41 | # The encoding of source files. 42 | #source_encoding = 'utf-8-sig' 43 | 44 | # The master toctree document. 45 | master_doc = 'index' 46 | 47 | # General information about the project. 48 | project = u'fapistrano' 49 | copyright = u'2016, Lin Ju' 50 | author = u'Lin Ju' 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | version = u'0.9.5' 58 | # The full version, including alpha/beta/rc tags. 59 | release = u'0.9.5' 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | # 64 | # This is also used if you do content translation via gettext catalogs. 65 | # Usually you set "language" from the command line for these cases. 66 | language = None 67 | 68 | # There are two options for replacing |today|: either, you set today to some 69 | # non-false value, then it is used: 70 | #today = '' 71 | # Else, today_fmt is used as the format for a strftime call. 72 | #today_fmt = '%B %d, %Y' 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | exclude_patterns = ['_build'] 77 | 78 | # The reST default role (used for this markup: `text`) to use for all 79 | # documents. 80 | #default_role = None 81 | 82 | # If true, '()' will be appended to :func: etc. cross-reference text. 83 | #add_function_parentheses = True 84 | 85 | # If true, the current module name will be prepended to all description 86 | # unit titles (such as .. function::). 87 | #add_module_names = True 88 | 89 | # If true, sectionauthor and moduleauthor directives will be shown in the 90 | # output. They are ignored by default. 91 | #show_authors = False 92 | 93 | # The name of the Pygments (syntax highlighting) style to use. 94 | pygments_style = 'sphinx' 95 | 96 | # A list of ignored prefixes for module index sorting. 97 | #modindex_common_prefix = [] 98 | 99 | # If true, keep warnings as "system message" paragraphs in the built documents. 100 | #keep_warnings = False 101 | 102 | # If true, `todo` and `todoList` produce output, else they produce nothing. 103 | todo_include_todos = False 104 | 105 | 106 | # -- Options for HTML output ---------------------------------------------- 107 | 108 | # The theme to use for HTML and HTML Help pages. See the documentation for 109 | # a list of builtin themes. 110 | html_theme = 'alabaster' 111 | 112 | # Theme options are theme-specific and customize the look and feel of a theme 113 | # further. For a list of options available for each theme, see the 114 | # documentation. 115 | #html_theme_options = {} 116 | 117 | # Add any paths that contain custom themes here, relative to this directory. 118 | #html_theme_path = [] 119 | 120 | # The name for this set of Sphinx documents. If None, it defaults to 121 | # " v documentation". 122 | #html_title = None 123 | 124 | # A shorter title for the navigation bar. Default is the same as html_title. 125 | #html_short_title = None 126 | 127 | # The name of an image file (relative to this directory) to place at the top 128 | # of the sidebar. 129 | #html_logo = None 130 | 131 | # The name of an image file (within the static path) to use as favicon of the 132 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 133 | # pixels large. 134 | #html_favicon = None 135 | 136 | # Add any paths that contain custom static files (such as style sheets) here, 137 | # relative to this directory. They are copied after the builtin static files, 138 | # so a file named "default.css" will overwrite the builtin "default.css". 139 | html_static_path = ['_static'] 140 | 141 | # Add any extra paths that contain custom files (such as robots.txt or 142 | # .htaccess) here, relative to this directory. These files are copied 143 | # directly to the root of the documentation. 144 | #html_extra_path = [] 145 | 146 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 147 | # using the given strftime format. 148 | #html_last_updated_fmt = '%b %d, %Y' 149 | 150 | # If true, SmartyPants will be used to convert quotes and dashes to 151 | # typographically correct entities. 152 | #html_use_smartypants = True 153 | 154 | # Custom sidebar templates, maps document names to template names. 155 | #html_sidebars = {} 156 | 157 | # Additional templates that should be rendered to pages, maps page names to 158 | # template names. 159 | #html_additional_pages = {} 160 | 161 | # If false, no module index is generated. 162 | #html_domain_indices = True 163 | 164 | # If false, no index is generated. 165 | #html_use_index = True 166 | 167 | # If true, the index is split into individual pages for each letter. 168 | #html_split_index = False 169 | 170 | # If true, links to the reST sources are added to the pages. 171 | #html_show_sourcelink = True 172 | 173 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 174 | #html_show_sphinx = True 175 | 176 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 177 | #html_show_copyright = True 178 | 179 | # If true, an OpenSearch description file will be output, and all pages will 180 | # contain a tag referring to it. The value of this option must be the 181 | # base URL from which the finished HTML is served. 182 | #html_use_opensearch = '' 183 | 184 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 185 | #html_file_suffix = None 186 | 187 | # Language to be used for generating the HTML full-text search index. 188 | # Sphinx supports the following languages: 189 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 190 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 191 | #html_search_language = 'en' 192 | 193 | # A dictionary with options for the search language support, empty by default. 194 | # Now only 'ja' uses this config value 195 | #html_search_options = {'type': 'default'} 196 | 197 | # The name of a javascript file (relative to the configuration directory) that 198 | # implements a search results scorer. If empty, the default will be used. 199 | #html_search_scorer = 'scorer.js' 200 | 201 | # Output file base name for HTML help builder. 202 | htmlhelp_basename = 'fapistranodoc' 203 | 204 | # -- Options for LaTeX output --------------------------------------------- 205 | 206 | latex_elements = { 207 | # The paper size ('letterpaper' or 'a4paper'). 208 | #'papersize': 'letterpaper', 209 | 210 | # The font size ('10pt', '11pt' or '12pt'). 211 | #'pointsize': '10pt', 212 | 213 | # Additional stuff for the LaTeX preamble. 214 | #'preamble': '', 215 | 216 | # Latex figure (float) alignment 217 | #'figure_align': 'htbp', 218 | } 219 | 220 | # Grouping the document tree into LaTeX files. List of tuples 221 | # (source start file, target name, title, 222 | # author, documentclass [howto, manual, or own class]). 223 | latex_documents = [ 224 | (master_doc, 'fapistrano.tex', u'fapistrano Documentation', 225 | u'Lin Ju', 'manual'), 226 | ] 227 | 228 | # The name of an image file (relative to this directory) to place at the top of 229 | # the title page. 230 | #latex_logo = None 231 | 232 | # For "manual" documents, if this is true, then toplevel headings are parts, 233 | # not chapters. 234 | #latex_use_parts = False 235 | 236 | # If true, show page references after internal links. 237 | #latex_show_pagerefs = False 238 | 239 | # If true, show URL addresses after external links. 240 | #latex_show_urls = False 241 | 242 | # Documents to append as an appendix to all manuals. 243 | #latex_appendices = [] 244 | 245 | # If false, no module index is generated. 246 | #latex_domain_indices = True 247 | 248 | 249 | # -- Options for manual page output --------------------------------------- 250 | 251 | # One entry per manual page. List of tuples 252 | # (source start file, name, description, authors, manual section). 253 | man_pages = [ 254 | (master_doc, 'fapistrano', u'fapistrano Documentation', 255 | [author], 1) 256 | ] 257 | 258 | # If true, show URL addresses after external links. 259 | #man_show_urls = False 260 | 261 | 262 | # -- Options for Texinfo output ------------------------------------------- 263 | 264 | # Grouping the document tree into Texinfo files. List of tuples 265 | # (source start file, target name, title, author, 266 | # dir menu entry, description, category) 267 | texinfo_documents = [ 268 | (master_doc, 'fapistrano', u'fapistrano Documentation', 269 | author, 'fapistrano', 'One line description of project.', 270 | 'Miscellaneous'), 271 | ] 272 | 273 | # Documents to append as an appendix to all manuals. 274 | #texinfo_appendices = [] 275 | 276 | # If false, no module index is generated. 277 | #texinfo_domain_indices = True 278 | 279 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 280 | #texinfo_show_urls = 'footnote' 281 | 282 | # If true, do not generate a @detailmenu in the "Top" node's menu. 283 | #texinfo_no_detailmenu = False 284 | -------------------------------------------------------------------------------- /docs/configuration.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | Deployfile Searching Strategy 5 | ----------------------------- 6 | 7 | Here is the algorithm which Fapistrano find deployfile: 8 | 9 | * Read system environment variable `FAP_APP` and then find deployfile by reading another system environment varialble `%(FAP_APP)s_DEPLOYFILE`, `EXAMPLE_DEPLOYFILE` in the assuming case:: 10 | 11 | $ export FAP_APP=EXAMPLE 12 | $ export EXAMPLE_DEPLOYFILE=/path/to/example/deploy.yml 13 | $ fap release -s production 14 | 15 | * If `FAP_APP` does not exist, Fapistrano will try to load deployfile by `--deployfile` / `-d` option:: 16 | 17 | $ fap --deployfile /path/to/example/deploy.yml release -s production 18 | 19 | * If both two variables listed above are missing, Fapistrano will try to load default deployfile `deploy.yml` in current workong dir:: 20 | 21 | $ ls 22 | deploy.yml 23 | $ fap release -s production 24 | 25 | Configuration Loading Strategy 26 | ------------------------------ 27 | 28 | Here is the algorithm which Fapistrano load configuration items: 29 | 30 | * Read command line options, with key turning `-` into `_`. NOTICE: key must exists in fabric env, Fapistrano default env or deploy.yml:: 31 | 32 | $ fap release -s production --current-release=/var/www/example 33 | 34 | * Read Configs defined in `stage_role_configs`. In below case, Fapistrano will use path defined in `stage_role_configs/stage/app/current_release`:: 35 | 36 | $ tail -n5 deploy.yml 37 | stage_role_configs: 38 | stage: 39 | app: 40 | current_release: /var/www/example-staging 41 | current_release: /var/www/example-production 42 | $ fap release -s stage 43 | 44 | * Read Configs defined in `deploy.yml` not not in `stage_role_configs`.:: 45 | 46 | $ tail -n5 deploy.yml 47 | stage_role_configs: 48 | stage: 49 | app: 50 | current_release: /var/www/example-staging 51 | production: 52 | app: 53 | current_release: /var/www/example-production 54 | $ fap release -s production 55 | 56 | 57 | Value Formatting 58 | ---------------- 59 | 60 | All configuration item values can be defined in Python string template format with a 61 | keyword inside. 62 | 63 | For instance, you have an `app_name` variable and a `path` variable defined like this:: 64 | 65 | app_name: example 66 | path: /var/www/%(app_name)s 67 | 68 | Variable `path` will be formatted to `/var/www/example` before running task. 69 | 70 | NOTICE: Currently only string value are supported. 71 | 72 | Configuration Items 73 | ------------------- 74 | 75 | The following variables are used for fabric: 76 | 77 | * `user` 78 | * `hosts` 79 | 80 | The following variables are used for Fapistrano: 81 | 82 | * `project_name` 83 | * `app_name` 84 | * `path` 85 | * `current_path` 86 | * `releases_path` 87 | * `shared_path` 88 | * `new_release` 89 | * `release_path`, 90 | * `linked_files`, list, default []. 91 | * `linked_dirs`, list, default []. 92 | * `stage_role_configs`, dict. 93 | * `keep_releases`, integer, default 5. 94 | 95 | Additional configurations are defined by Fapistrano plugins. 96 | -------------------------------------------------------------------------------- /docs/directory.rst: -------------------------------------------------------------------------------- 1 | Directory 2 | ========== 3 | 4 | Root 5 | ----- 6 | 7 | The root path of this structure can be defined with the configuration variable `path`. 8 | 9 | Default `path` is `/home/%(user)s/www/%(app_name)s`. 10 | 11 | Assuming your `deploy.yml` contains this:: 12 | 13 | user: deploy 14 | app_name: example 15 | 16 | Then you project will be deployed to `/home/deploy/www/example`. 17 | 18 | If you have special demand, you can rewrite `path` in your `deploy.yml`:: 19 | 20 | user: deploy 21 | app_name: example 22 | path: /var/www/example 23 | 24 | Basic Structure 25 | --------------- 26 | 27 | The directory hierarchy on each remote host is strictly defined. 28 | Inspecting the tree inside `path` may look like this:: 29 | 30 | ├── current -> /home/deploy/www/example/releases/160323-164537 31 | ├── releases 32 | │   ├── 160323-164333 33 | │   ├── 160323-164407 34 | │   ├── 160323-164537 35 | │   │   ├── configs 36 | │   │   │   └── supervisor_staging_app.conf -> /home/deploy/www/example/shared/configs/supervisor_staging_app.conf 37 | └── shared 38 | ├── configs 39 | │   └── supervisor_staging_app.conf 40 | 41 | Latest Releases 42 | --------------- 43 | 44 | `releases` is a directory contains several latest deployments. 45 | 46 | A directory naming with `%y%m%d-%H%M%S` will be created after a deployment starting. 47 | 48 | Default path is `%(path)s/releases`. 49 | 50 | Current Release 51 | --------------- 52 | 53 | `current` is a symlink to latest deployment listed in `releases` directory. 54 | 55 | This symlink is updated at the end of a successful deployment. 56 | If the deployment fails in any step the current symlink still points to the old release. 57 | 58 | If you are releasing a new deployment, `current` will be symlink to newly created directory. 59 | If you are rollbacking an old deployment, `current` will be symlink to elderly existed directory. 60 | 61 | Default symlink is `%(path)s/current`. 62 | 63 | Shared files or directories 64 | --------------------------- 65 | 66 | `shared` is a directory contains files and directories that will be symlinked in each release. 67 | 68 | Default path is `%(path)s/shared`. 69 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. fapistrano documentation master file, created by 2 | sphinx-quickstart on Mon Feb 1 10:33:58 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to fapistrano's documentation! 7 | ====================================== 8 | 9 | Fapistrano is a remote server automation and deployment tool. 10 | It can be easily used to rolling update your sites. 11 | 12 | Fapistrano grows from several `fabric` tasks and inspired by capistrano. 13 | 14 | Contents: 15 | 16 | .. toctree:: 17 | :maxdepth: 2 18 | 19 | installation 20 | quick_start 21 | directory 22 | configuration 23 | cli 24 | plugin 25 | signals 26 | 12factor 27 | 28 | Indices and tables 29 | ================== 30 | 31 | * :ref:`genindex` 32 | * :ref:`modindex` 33 | * :ref:`search` 34 | 35 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Fapistrano is best installed via pip (highly recommended) or easy_install. 5 | 6 | e.g.:: 7 | 8 | $ pip install fapistrano 9 | 10 | Or, to install a developing version for debugging or hacking, you may want to use pip editable instalation:: 11 | 12 | $ pip install -e git+git@github.com:liwushuo/fapistrano.git#egg=fapistrano 13 | 14 | 15 | If you don't want to pollute your global Python environment, you can use virtualenvwrapper 16 | to create a virtualenv envrionment and install fapistrano inside it:: 17 | 18 | $ mkvirtualenv ops 19 | (ops) $ pip install fapistrano 20 | 21 | 22 | Config File 23 | ----------- 24 | 25 | You have to tell fapistrano where to deploy and what to deploy. Thus, you must put 26 | a YAML config `deploy.yml` in your working directory to let fapistrano know basic 27 | information. You can also specify YAML config as command line option. We will explain 28 | it later. 29 | 30 | NOTICE: All configuration items available for `fabric.api.env` are Fapistrano configuration 31 | items as well. 32 | 33 | SSH 34 | ---- 35 | 36 | Fapistrano deploys using SSH. Thus, you must be able to SSH (ideally with keys and 37 | ssh-agent) from the deployment system to the destination system for Fapistrano to work. 38 | 39 | Basically, you can add `user`, `use_ssh_config` and `hosts` configuration items to 40 | your YAML config. 41 | 42 | If you are still struggling to get login working, try the Fabric docs. 43 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 1>NUL 2>NUL 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\fapistrano.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\fapistrano.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /docs/plugin.rst: -------------------------------------------------------------------------------- 1 | Plugin 2 | ====== 3 | 4 | This section talks about installing, loading and writing third party plugins. 5 | 6 | Installing Plugins 7 | ------------------ 8 | 9 | Installing a third party plugin can be easily done with pip:: 10 | 11 | $ pip install fapistrano-PLUGIN 12 | 13 | Loading Plugins 14 | --------------- 15 | 16 | In your `deploy.yml`, append plugin name to configuration item `plugins`:: 17 | 18 | plugins: 19 | - fapistrano.git 20 | - fapistrano.virtualenv 21 | 22 | Normally, plugins have their own configurations. 23 | Don't forget configure these configurations before running `fap` command. 24 | 25 | You can set plugin configurations globally, or custmozie for each Stage-Role, or even 26 | on each cli running. 27 | 28 | Tracing Plugins(TODO) 29 | ---------------------- 30 | 31 | If you want to find out a complete function call sequence during deploying applicaiton, 32 | you can find out the answer by typing:: 33 | 34 | $ fap release -s production --dry-run 35 | 36 | Deactivating Plugins(TODO) 37 | -------------------------- 38 | 39 | TODO 40 | 41 | You can prevent plugins from loading them:: 42 | 43 | fap release -s staging --no-plugins fapistrano.git 44 | 45 | This means that any subsequent try to activate/load the named plugin will not work. 46 | 47 | 48 | Writing Plugins 49 | --------------- 50 | 51 | It's easy to implement plugins for your own vanilla project or pip installable plugins that 52 | can be used throughout many projects. 53 | 54 | A plugin contains an `init` function. 55 | 56 | Normally plugin register default configurations and signal handlers at `init` function. 57 | A plugin is recommended to use it's name as prefix in configuration definition. Here is 58 | a hello-world example:: 59 | 60 | 61 | # fapistrano_echo.py 62 | import click 63 | from fapistrano import signal, configuration, env 64 | 65 | def init(): 66 | configuration.setdefault('echo_message', 'Hello World') 67 | signal.register('deploy.finished', echo_message) 68 | 69 | def echo_message(**kwargs): 70 | click.secho(env.echo_message, fg='green') 71 | 72 | 73 | 74 | Default Plugins 75 | --------------- 76 | 77 | Fapistrano internally bootstrap several plugins: 78 | 79 | * `fapistrano.curl`: dowloading a file into release directory on updating release. 80 | * `fapistrano.git`: using git as a scm tool to update files on updateing release. 81 | * `fapistrano.localshared`: copy some files from one place at your server to shared directory before starting release. 82 | * `fapistrano.supervisorctl`: restart your application on release published. 83 | * `fapistrano.virtualenv`: using virtualenv to create python environment on updating release. 84 | * `fapistrano.virtualenvwrapper`: using virtualenvwrapper to create python environment on updating release. 85 | -------------------------------------------------------------------------------- /docs/quick_start.rst: -------------------------------------------------------------------------------- 1 | Quick Start 2 | ============ 3 | 4 | Once installed, Fapistrano bootstraps a `fap` command to complete release/rollback/restart task. 5 | 6 | First Run 7 | --------- 8 | 9 | Add a file named `deploy.yml`:: 10 | 11 | project_name: smallest-example 12 | app_name: smallest-example 13 | user: deploy 14 | use_ssh_config: true 15 | 16 | stage_role_configs: 17 | staging: 18 | app: 19 | hosts: 20 | - app-stag01 21 | 22 | After Runing command below, Fapistrano will create a smallest example on your host:: 23 | 24 | $ fap release --stage staging --role app 25 | [app-stag01] Executing task 'release' 26 | ===> Starting 27 | [app-stag01] run: mkdir -p /home/deploy/www/smallest-example/{releases,shared/log} 28 | [app-stag01] run: chmod -R g+w /home/deploy/www/smallest-example/shared 29 | [app-stag01] run: mkdir -p /home/deploy/www/smallest-example/releases/160328-162832 30 | ===> Started 31 | ===> Updating 32 | ===> Updated 33 | ===> Publishing 34 | [app-stag01] run: ln -nfs /home/deploy/www/smallest-example/releases/160328-162832 /home/deploy/www/smallest-example/current 35 | ===> Published 36 | ===> Finishing 37 | [app-stag01] run: ls -x /home/deploy/www/smallest-example/releases 38 | ===> Finished 39 | 40 | For now, it does nothing but leaving a blank directory hierachy to you:: 41 | 42 | $ ssh deploy@app-stag01 tree /home/deploy/www/smallest-example/ 43 | /home/deploy/www/smallest-example/ 44 | ├── current -> /home/deploy/www/smallest-example/releases/160328-162832 45 | ├── releases 46 | │   └── 160328-162832 47 | └── shared 48 | └── log 49 | 50 | 5 directories, 0 files 51 | 52 | Using Git 53 | --------- 54 | 55 | Git is a popular scm tool. Fapistrano integrated git as a plugin. 56 | You need to declare loading `fapistrano.git` and specify repository in `deploy.yml`:: 57 | 58 | project_name: git-example 59 | app_name: git-example 60 | user: deploy 61 | use_ssh_config: true 62 | 63 | stage_role_configs: 64 | staging: 65 | app: 66 | hosts: 67 | - app-stag01 68 | 69 | plugins: 70 | - fapistrano.plugins.git 71 | 72 | repo: git@github.com:octocat/Hello-World.git 73 | branch: master 74 | 75 | By Executing release flow, Fapistrano clones remote git repo and export code in 76 | `master` branch to release directory:: 77 | 78 | $ fap release --stage staging --role app 79 | [app-stag01] Executing task 'release' 80 | ===> Starting 81 | [app-stag01] run: mkdir -p /home/deploy/www/git-example/{releases,shared/log} 82 | [app-stag01] run: chmod -R g+w /home/deploy/www/git-example/shared 83 | [app-stag01] run: mkdir -p /home/deploy/www/git-example/releases/160328-163937 84 | ===> Started 85 | [app-stag01] run: git clone --mirror --depth 1 --no-single-branch git@github.com:octocat/Hello-World.git /home/deploy/www/git-example/repo 86 | [app-stag01] run: git ls-remote --heads git@github.com:octocat/Hello-World.git 87 | ===> Updating 88 | [app-stag01] run: git fetch --depth 1 origin master 89 | [app-stag01] run: git rev-list --max-count=1 --abbrev-commit --abbrev=12 master 90 | [app-stag01] run: git archive master | tar -x -f - -C /home/deploy/www/git-example/releases/160328-163937/ 91 | [app-stag01] run: echo 7fd1a60b01f9 >> REVISION 92 | ===> Updated 93 | ===> Publishing 94 | [app-stag01] run: ln -nfs /home/deploy/www/git-example/releases/160328-163937 /home/deploy/www/git-example/current 95 | ===> Published 96 | ===> Finishing 97 | [app-stag01] run: ls -x /home/deploy/www/git-example/releases 98 | ===> Finished 99 | 100 | Now we have added a `repo` directory and updated out release directory:: 101 | 102 | % ssh deploy@lws-stag01 tree /home/deploy/www/git-example 103 | /home/deploy/www/git-example 104 | ├── current -> /home/deploy/www/git-example/releases/160328-163937 105 | ├── releases 106 | │   └── 160328-163937 107 | │   ├── README 108 | │   └── REVISION 109 | ├── repo 110 | │   ├── branches 111 | │   ├── config 112 | │   ├── description 113 | │   ├── FETCH_HEAD 114 | │   ├── HEAD 115 | │   ├── hooks 116 | │   │   ├── applypatch-msg.sample 117 | │   │   ├── commit-msg.sample 118 | │   │   ├── post-update.sample 119 | │   │   ├── pre-applypatch.sample 120 | │   │   ├── pre-commit.sample 121 | │   │   ├── prepare-commit-msg.sample 122 | │   │   ├── pre-push.sample 123 | │   │   ├── pre-rebase.sample 124 | │   │   └── update.sample 125 | │   ├── info 126 | │   │   └── exclude 127 | │   ├── objects 128 | │   │   ├── info 129 | │   │   └── pack 130 | │   │   ├── pack-c8b9cbbd14e791b8beddf1033f5b4357d4f179da.idx 131 | │   │   └── pack-c8b9cbbd14e791b8beddf1033f5b4357d4f179da.pack 132 | │   ├── packed-refs 133 | │   ├── refs 134 | │   │   ├── heads 135 | │   │   └── tags 136 | │   └── shallow 137 | └── shared 138 | └── log 139 | 140 | 141 | Using Supervisor 142 | ---------------- 143 | 144 | A static git repository updating is trivial. 145 | Let's get a little bit more complicated now. 146 | 147 | We are going to define `deploy.yml` for a flask hello-world application:: 148 | 149 | project_name: supervisor-example 150 | app_name: supervisor-example 151 | user: deploy 152 | use_ssh_config: true 153 | 154 | stage_role_configs: 155 | staging: 156 | app: 157 | hosts: 158 | - app-stag01 159 | 160 | plugins: 161 | - fapistrano.plugins.git 162 | - fapistrano.plugins.virtualenv 163 | - fapistrano.plugins.supervisorctl 164 | 165 | repo: git@github.com:liwushuo/fapistrano.git 166 | git_archive_tree: examples/supervisor-example 167 | 168 | supervisor_check_status: true 169 | supervisor_conf: configs/supervisor_%(stage)s_%(role)s.conf 170 | 171 | Then we add `configs/supervisor_staging_app.conf` to the repository:: 172 | 173 | [program:supervisor-example] 174 | command=python app.py 175 | directory=/home/deploy/www/%(program_name)s/current 176 | environment=PATH="/home/deploy/www/%(program_name)s/current/venv/bin",FLASK_ENV="stag" 177 | numprocs=1 178 | user=deploy 179 | autostart=true 180 | autorestart=true 181 | redirect_stderr=true 182 | stdout_logfile=/var/log/supervisor/%(program_name)s-web.log 183 | stdout_logfile_maxbytes=100MB 184 | stdout_logfile_backups=10 185 | 186 | Finally, Run with option `--supervisor-refresh=true`, since we first 187 | registered our supervisor config to supervisord. In the next release, 188 | there is no need to add option `--supervisor-refresh=true` unless 189 | supervisor config file modified.:: 190 | 191 | [app-stag01] Executing task 'release' 192 | ===> Starting 193 | [app-stag01] run: mkdir -p /home/deploy/www/supervisor-example/{releases,shared/log} 194 | [app-stag01] run: chmod -R g+w /home/deploy/www/supervisor-example/shared 195 | [app-stag01] run: mkdir -p /home/deploy/www/supervisor-example/releases/160328-173812 196 | ===> Started 197 | [app-stag01] run: git clone --mirror --depth 1 --no-single-branch git@github.com:liwushuo/fapistrano.git /home/deploy/www/supervisor-example/repo 198 | [app-stag01] run: git ls-remote --heads git@github.com:liwushuo/fapistrano.git 199 | [app-stag01] run: ln -nfs /home/deploy/www/supervisor-example/current/configs/supervisor_staging_app.conf /etc/supervisor/conf.d/supervisor-example.conf 200 | ===> Updating 201 | [app-stag01] run: git fetch --depth 1 origin master 202 | [app-stag01] run: git rev-list --max-count=1 --abbrev-commit --abbrev=12 master 203 | [app-stag01] run: git archive master examples/supervisor-example | tar -x --strip-components 2 -f - -C /home/deploy/www/supervisor-example/releases/160328-173812/ 204 | [app-stag01] run: echo 86ba572f3d8e >> REVISION 205 | ===> Updated 206 | [app-stag01] run: /usr/bin/env virtualenv /home/deploy/www/supervisor-example/releases/160328-173812/venv 207 | [app-stag01] run: pip install -U pip setuptools wheel 208 | [app-stag01] run: pip install -r /home/deploy/www/supervisor-example/releases/160328-173812/requirements.txt 209 | ===> Publishing 210 | [app-stag01] run: ln -nfs /home/deploy/www/supervisor-example/releases/160328-173812 /home/deploy/www/supervisor-example/current 211 | ===> Published 212 | [app-stag01] run: supervisorctl stop supervisor-example 213 | [app-stag01] run: supervisorctl reread 214 | [app-stag01] run: supervisorctl update 215 | [app-stag01] run: supervisorctl start supervisor-example 216 | [app-stag01] run: supervisorctl status supervisor-example 217 | [app-stag01] out: supervisor-example RUNNING pid 13014, uptime 0:00:02 218 | [app-stag01] out: 219 | 220 | ===> Finishing 221 | [app-stag01] run: ls -x /home/deploy/www/supervisor-example/releases 222 | [app-stag01] run: rm -rf 160328-173248 223 | ===> Finished 224 | 225 | Now, our flask application is running!:: 226 | 227 | $ ssh deploy@app-stag01 curl -s http://0.0.0.0:5000 228 | hello world 229 | 230 | Rollback! 231 | --------- 232 | 233 | Rollback is easily by replacing `release` to `rollback`. 234 | 235 | After releasing again, our release are now at 160328-175016. 236 | 237 | let's trigger a rollback flow.:: 238 | 239 | $ fap rollback --stage staging --role app 240 | staging app 241 | [app-stag01] Executing task 'rollback' 242 | ===> Starting 243 | [app-stag01] run: readlink /home/deploy/www/supervisor-example/current 244 | [app-stag01] run: ls -x /home/deploy/www/supervisor-example/releases 245 | ===> Started 246 | [app-stag01] run: git ls-remote --heads git@github.com:liwushuo/fapistrano.git 247 | [app-stag01] run: ln -nfs /home/deploy/www/supervisor-example/current/configs/supervisor_staging_app.conf /etc/supervisor/conf.d/supervisor-example.conf 248 | ===> Reverting 249 | ===> Reverted 250 | ===> Publishing 251 | [app-stag01] run: ln -nfs /home/deploy/www/supervisor-example/releases/160328-173812 /home/deploy/www/supervisor-example/current 252 | ===> Published 253 | [app-stag01] run: supervisorctl restart supervisor-example 254 | [app-stag01] run: supervisorctl status supervisor-example 255 | [app-stag01] out: supervisor-example RUNNING pid 15272, uptime 0:00:02 256 | [app-stag01] out: 257 | 258 | ===> Finishing rollback 259 | [app-stag01] run: rm -rf /home/deploy/www/supervisor-example/releases/160328-175016 260 | ===> Finished 261 | 262 | This command help us rollback our current release back to `160328-173812`, which is deployed 263 | in last example. 264 | 265 | Using with Beeper 266 | ----------------- 267 | 268 | Beeper is a tool bundling virtualenv.py, wheels and our project as a tar file. 269 | With the help of Jenkins, we can build a beeper tgz file before release. 270 | 271 | We can use a curl plugin to update our application:: 272 | 273 | 274 | project_name: curl-example 275 | app_name: curl-example 276 | user: deploy 277 | use_ssh_config: true 278 | stage_role_configs: 279 | staging: 280 | app: 281 | hosts: 282 | - app-stag01 283 | plugins: 284 | - fapistrano.plugins.curl 285 | - fapistrano.plugins.supervisorctl 286 | 287 | curl_extract_tar: true 288 | curl_postinstall_script: "sh ./install.sh" 289 | 290 | supervisor_check_status: true 291 | 292 | When running `fap release`, we can attach option `--curl-url`. Assuming you are 293 | using Jenkins to build your application and have authority to fetch artifact:: 294 | 295 | $ fap release -s staging -r app --curl-url=http://ci.your-corp.com/view/Server/job/server.builder.curl-example/lastSuccessfulBuild/artifact/dist/curl-example-0f2da63.tar --curl-options="--user $JENKINS_USERNAME:$JENKINS_TOKEN" 296 | 297 | 298 | Using with Jar 299 | -------------- 300 | 301 | If there is a Java application to deploy, it's recommend to build it to a Jar file. 302 | You can integrate `fapistrano.plugins.curl` to deploy jar.:: 303 | 304 | project_name: jar-example 305 | app_name: jar-example 306 | user: deploy 307 | use_ssh_config: true 308 | stage_role_configs: 309 | staging: 310 | app: 311 | hosts: 312 | - app-stag01 313 | plugins: 314 | - fapistrano.plugins.localshared 315 | - fapistrano.plugins.curl 316 | - fapistrano.plugins.supervisorctl 317 | 318 | # supervisor conf 319 | supervisor_refresh: false 320 | supervisor_output: false 321 | supervisor_check_status: true 322 | 323 | # curl conf 324 | curl_output: 'bayarea.jar' 325 | -------------------------------------------------------------------------------- /docs/signals.rst: -------------------------------------------------------------------------------- 1 | Signals 2 | ======= 3 | 4 | Basically, Fapistrano provides 3 type of deploy flow: 5 | 6 | 1. release flow 7 | 2. rollback flow 8 | 3. restart flow 9 | 10 | Release Flow 11 | ------------ 12 | 13 | When you trigger a release task, it emits at least 8 signals: 14 | 15 | * `deploy.starting` 16 | * `deploy.started` 17 | * `deploy.updating` 18 | * `deploy.updated` 19 | * `deploy.publishing` 20 | * `deploy.published` 21 | * `deploy.finishing` 22 | * `deploy.finished` 23 | 24 | Rollback Flow 25 | ------------- 26 | 27 | When you trigger a rollback task, it emits at least 8 signals: 28 | 29 | * `deploy.starting` 30 | * `deploy.started` 31 | * `deploy.reverting` 32 | * `deploy.reverted` 33 | * `deploy.publishing` 34 | * `deploy.published` 35 | * `deploy.finishing_rollback` 36 | * `deploy.finished` 37 | 38 | Restart Flow 39 | ------------ 40 | 41 | When you tirgger a restart flow, it emits at least 2 signals: 42 | 43 | * `deploy.restarting` 44 | * `deploy.restarted` 45 | 46 | You can hook this signals and write you own deploy logic. 47 | -------------------------------------------------------------------------------- /examples/curl-example.yml: -------------------------------------------------------------------------------- 1 | # An example of how to deploy a project built in beeper tar: 2 | # 3 | # Deploy a beeper tar to server 4 | # 5 | # fap release -s staging -r app --curl-url=http://ci.example.org/curl-example.tar 6 | # 7 | # Rollback a release 8 | # 9 | # $ fap rollback -s staging -r app 10 | # 11 | # Restart: 12 | # 13 | # $ fap restart -s staging -r app 14 | # 15 | # Restart with supervisor restart: 16 | # 17 | # $ fap restart -s staging -r app --supervisor-refresh=true 18 | # 19 | project_name: curl-example 20 | app_name: curl-example 21 | user: deploy 22 | use_ssh_config: true 23 | keep_releases: 5 24 | 25 | stage_role_configs: 26 | staging: 27 | app: 28 | hosts: 29 | - app-stag01 30 | app_name: curl-example-staging 31 | linked_files: 32 | - configs/supervisor_staging.conf 33 | - settings/staging.py 34 | production: 35 | app: 36 | hosts: 37 | - app-app01 38 | linked_files: 39 | - configs/supervisor_production_app.conf 40 | - settings/production.py 41 | 42 | plugins: 43 | - fapistrano.plugins.localshared 44 | - fapistrano.plugins.curl 45 | - fapistrano.plugins.supervisorctl 46 | 47 | curl_extract_tar: true 48 | curl_postinstall_script: "sh ./install.sh" 49 | 50 | supervisor_check_status: true 51 | 52 | localshared_source: '/etc/appconf/curl-example' 53 | 54 | -------------------------------------------------------------------------------- /examples/git-example.yml: -------------------------------------------------------------------------------- 1 | # An example of how to deploy a git repo: 2 | # 3 | # Deploy a beeper tar to server 4 | # 5 | # fap release -s staging -r app 6 | # 7 | # Rollback a release 8 | # 9 | # $ fap rollback -s staging -r app 10 | # 11 | # Restart: 12 | # 13 | # $ fap restart -s staging -r app 14 | # 15 | # Restart with supervisor restart: 16 | # 17 | # $ fap restart -s staging -r app --supervisor-refresh=true 18 | # 19 | project_name: git-example 20 | app_name: git-example 21 | user: deploy 22 | use_ssh_config: true 23 | keep_releases: 5 24 | 25 | stage_role_configs: 26 | staging: 27 | app: 28 | hosts: 29 | - app-stag01 30 | app_name: git-example-staging 31 | linked_files: 32 | - configs/supervisor_staging.conf 33 | - settings/staging.py 34 | production: 35 | app: 36 | hosts: 37 | - app-app01 38 | linked_files: 39 | - configs/supervisor_production_app.conf 40 | - settings/production.py 41 | 42 | plugins: 43 | - fapistrano.plugins.localshared 44 | - fapistrano.plugins.git 45 | - fapistrano.plugins.virtualenvwrapper 46 | - fapistrano.plugins.supervisorctl 47 | 48 | repo: 'git@git.example.org:fap/git-example.git' 49 | branch: master 50 | git_shallow_clone: 1 51 | 52 | supervisor_check_status: true 53 | 54 | localshared_source: '/etc/appconf/git-example' 55 | -------------------------------------------------------------------------------- /examples/git-example/deploy.yml: -------------------------------------------------------------------------------- 1 | project_name: git-example 2 | app_name: git-example 3 | user: deploy 4 | use_ssh_config: true 5 | 6 | stage_role_configs: 7 | staging: 8 | app: 9 | hosts: 10 | - app-stag01 11 | 12 | plugins: 13 | - fapistrano.plugins.git 14 | 15 | repo: git@github.com:octocat/Hello-World.git 16 | -------------------------------------------------------------------------------- /examples/smallest-example/deploy.yml: -------------------------------------------------------------------------------- 1 | project_name: smallest-example 2 | app_name: smallest-example 3 | user: deploy 4 | use_ssh_config: true 5 | 6 | stage_role_configs: 7 | staging: 8 | app: 9 | hosts: 10 | - app-stag01 11 | -------------------------------------------------------------------------------- /examples/supervisor-example/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask import Flask 4 | 5 | app = Flask(__name__) 6 | 7 | @app.route('/') 8 | def index(): 9 | return 'hello world' 10 | 11 | if __name__ == '__main__': 12 | app.run(host='0.0.0.0') 13 | -------------------------------------------------------------------------------- /examples/supervisor-example/configs/supervisor_staging_app.conf: -------------------------------------------------------------------------------- 1 | [program:supervisor-example] 2 | command=python app.py 3 | directory=/home/deploy/www/%(program_name)s/current 4 | environment=PATH="/home/deploy/www/%(program_name)s/current/venv/bin",FLASK_ENV="stag" 5 | numprocs=1 6 | user=app 7 | autostart=true 8 | autorestart=true 9 | redirect_stderr=true 10 | stdout_logfile=/var/log/supervisor/%(program_name)s-web.log 11 | stdout_logfile_maxbytes=100MB 12 | stdout_logfile_backups=10 13 | -------------------------------------------------------------------------------- /examples/supervisor-example/deploy.yml: -------------------------------------------------------------------------------- 1 | project_name: supervisor-example 2 | app_name: supervisor-example 3 | user: deploy 4 | use_ssh_config: true 5 | 6 | stage_role_configs: 7 | staging: 8 | app: 9 | hosts: 10 | - app-stag01 11 | 12 | plugins: 13 | - fapistrano.plugins.git 14 | - fapistrano.plugins.virtualenv 15 | - fapistrano.plugins.supervisorctl 16 | 17 | repo: git@github.com:liwushuo/fapistrano.git 18 | git_archive_tree: examples/supervisor-example 19 | 20 | supervisor_check_status: true 21 | supervisor_conf: configs/supervisor_%(stage)s_%(role)s.conf 22 | -------------------------------------------------------------------------------- /examples/supervisor-example/requirements.txt: -------------------------------------------------------------------------------- 1 | flask==0.10.1 2 | -------------------------------------------------------------------------------- /fapistrano/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __version__ = '0.9.5' 4 | 5 | from fabric.api import env 6 | from . import signal, configuration 7 | -------------------------------------------------------------------------------- /fapistrano/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from importlib import import_module 4 | 5 | from fabric.api import env 6 | 7 | def init(): 8 | if not hasattr(env, 'plugins'): 9 | return 10 | for plugin in env.plugins: 11 | mod = import_module(plugin) 12 | mod.init() 13 | 14 | def init_cli(conf): 15 | for key, value in conf.items(): 16 | setattr(env, key, value) 17 | init() 18 | -------------------------------------------------------------------------------- /fapistrano/cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from collections import defaultdict 4 | 5 | import yaml 6 | import click 7 | from click.parser import OptionParser 8 | from fabric.api import env, execute 9 | 10 | from fapistrano.configuration import apply_env, apply_yaml_to_env, set_default_configurations 11 | from fapistrano import deploy 12 | 13 | 14 | @click.group() 15 | @click.option('-d', '--deployfile', default='./deploy.yml') 16 | @click.pass_context 17 | def fap(ctx, deployfile): 18 | try: 19 | with open(deployfile, 'rb') as f: 20 | ctx.obj = {'yaml': yaml.load(f.read())} 21 | 22 | except IOError: 23 | if deployfile == './deploy.yml': 24 | _abort("cannot find deployfile. Did you put a deploy.yml file on current directory?") 25 | else: 26 | _abort('cannot find deployfile. Does this file really exist?') 27 | 28 | 29 | @fap.command(context_settings=dict( 30 | ignore_unknown_options=True, 31 | )) 32 | @click.option('-r', '--role', help='deploy role, for example: production, staging') 33 | @click.option('-s', '--stage', help='deploy stage, for example: app, worker, cron') 34 | @click.argument('plugin_args', nargs=-1, type=click.UNPROCESSED) 35 | @click.pass_context 36 | def release(ctx, role, stage, plugin_args): 37 | _execute(ctx, deploy.release, stage, role, None, plugin_args) 38 | 39 | 40 | @fap.command(context_settings=dict( 41 | ignore_unknown_options=True, 42 | )) 43 | @click.option('-r', '--role', help='deploy role, for example: production, staging') 44 | @click.option('-s', '--stage', help='deploy stage, for example: app, worker, cron') 45 | @click.argument('plugin_args', nargs=-1, type=click.UNPROCESSED) 46 | @click.pass_context 47 | def rollback(ctx, role, stage, plugin_args): 48 | _execute(ctx, deploy.rollback, stage, role, None, plugin_args) 49 | 50 | 51 | @fap.command(context_settings=dict( 52 | ignore_unknown_options=True, 53 | )) 54 | @click.option('-r', '--role', help='deploy role, for example: production, staging') 55 | @click.option('-s', '--stage', help='deploy stage, for example: app, worker, cron') 56 | @click.argument('plugin_args', nargs=-1, type=click.UNPROCESSED) 57 | @click.pass_context 58 | def restart(ctx, role, stage, plugin_args): 59 | _execute(ctx, deploy.restart, stage, role, None, plugin_args) 60 | 61 | @fap.command(context_settings=dict( 62 | ignore_unknown_options=True, 63 | )) 64 | @click.option('-r', '--role', required=True, help='deploy role, for example: production, staging') 65 | @click.option('-s', '--stage', required=True, help='deploy stage, for example: app, worker, cron') 66 | @click.option('-c', '--command', help='run command') 67 | @click.argument('plugin_args', nargs=-1, type=click.UNPROCESSED) 68 | @click.pass_context 69 | def once(ctx, role, stage, command, plugin_args): 70 | _execute(ctx, deploy.once, stage, role, command, plugin_args) 71 | 72 | 73 | @fap.command(context_settings=dict( 74 | ignore_unknown_options=True, 75 | )) 76 | @click.option('-r', '--role', required=True, help='deploy role, for example: production, staging') 77 | @click.option('-s', '--stage', required=True, help='deploy stage, for example: app, worker, cron') 78 | @click.argument('plugin_args', nargs=-1, type=click.UNPROCESSED) 79 | @click.pass_context 80 | def shell(ctx, role, stage, plugin_args): 81 | _execute(ctx, deploy.shell, stage, role, None, plugin_args) 82 | 83 | 84 | def _apply_plugin_options(plugin_args): 85 | parser = OptionParser() 86 | for key in env: 87 | option_key = '--%s' % key.replace('_', '-') 88 | parser.add_option([option_key], key) 89 | 90 | opts, largs, order = parser.parse_args(list(plugin_args)) 91 | for arg_key in order: 92 | setattr(env, arg_key, opts[arg_key]) 93 | 94 | def get_method_name(method): 95 | return method.__name__.split('.')[-1] 96 | 97 | def _setup_execution(ctx, method, role, stage, command, plugin_args): 98 | env.run_command = command or '' 99 | set_default_configurations(force=True) 100 | apply_yaml_to_env(ctx.obj.get('yaml'), get_method_name(method)) 101 | apply_env(stage, role) 102 | _apply_plugin_options(plugin_args) 103 | 104 | def _abort(message): 105 | click.secho('Error: %s' % message, blink=True, fg='red') 106 | exit(1) 107 | 108 | def _log(message): 109 | click.secho(message, blink=True, fg='green') 110 | 111 | def _get_execute_stage_and_roles(ctx, stage, role): 112 | stage_role_configs = ctx.obj.get('yaml').get('stage_role_configs') 113 | 114 | if not stage_role_configs: 115 | _abort('Stage role config not found.') 116 | 117 | if not role and not stage: 118 | _abort('Stage or role not found.') 119 | 120 | if not role and stage not in stage_role_configs: 121 | _abort('Stage not found.') 122 | 123 | if not role and not stage_role_configs.get(stage): 124 | _abort('No role defined in this stage.') 125 | 126 | if stage and role and stage: 127 | return [(stage, role)] 128 | 129 | if not role: 130 | comb = [] 131 | for _role in stage_role_configs[stage].keys(): 132 | comb.append((stage, _role)) 133 | return comb 134 | 135 | roles = defaultdict(set) 136 | for _stage in stage_role_configs: 137 | for _role in stage_role_configs[_stage]: 138 | roles[_role].add(_stage) 139 | 140 | if role not in roles: 141 | _abort('Role not found.') 142 | 143 | if not roles[role]: 144 | _abort('No stage defined for this role.') 145 | 146 | comb = [] 147 | for _stage in roles[role]: 148 | comb.append((_stage, role)) 149 | return comb 150 | 151 | def _execute(ctx, method, stage=None, role=None, command=None, plugin_args=None): 152 | combinations = _get_execute_stage_and_roles(ctx, stage, role) 153 | for stage, role in combinations: 154 | _log('Executing %s at %s' % (role, stage)) 155 | _setup_execution(ctx, method, role, stage, command, plugin_args) 156 | execute(method) 157 | 158 | 159 | if __name__ == '__main__': 160 | import os 161 | auto_envvar_prefix = os.environ.get('FAP_APP') or '' 162 | fap(obj={}, auto_envvar_prefix=auto_envvar_prefix) 163 | -------------------------------------------------------------------------------- /fapistrano/configuration.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | from importlib import import_module 5 | from functools import wraps 6 | from datetime import datetime 7 | from fabric.api import env, abort, show, hide 8 | 9 | 10 | def format_definition(): 11 | 12 | defs = dict(env.items()) 13 | cals = {} 14 | 15 | def _apply_basic(val): 16 | if not isinstance(val, str): 17 | return val 18 | keys = re.findall(r'%\(([^)]*)\)', val) 19 | ctx = { 20 | k: _format_basic(k) 21 | for k in keys 22 | } 23 | return val % ctx 24 | 25 | def _format_basic(key): 26 | if key in cals: 27 | return cals[key] 28 | elif not isinstance(key, str): 29 | cals[key] = defs[key] 30 | return cals[key] 31 | else: 32 | cals[key] = _apply_basic(defs[key]) 33 | return cals[key] 34 | 35 | def _format_structure(structure, key): 36 | if isinstance(structure, str): 37 | return _apply_basic(structure) 38 | elif not isinstance(structure, (list, dict)): 39 | return structure 40 | elif isinstance(structure, list): 41 | cals[key] = [_format_structure(elem, key) for elem in structure] 42 | return cals[key] 43 | else: 44 | cals[key] = {k: _format_structure(structure[k], key) for k in structure} 45 | return cals[key] 46 | 47 | for key in defs: 48 | if isinstance(defs[key], (int, str, long, bool, )): 49 | _format_basic(key) 50 | 51 | for key in defs: 52 | if isinstance(defs[key], (list, dict, )): 53 | _format_structure(defs[key], key) 54 | 55 | return cals 56 | 57 | 58 | def setdefault(key, value, force=True): 59 | if force: 60 | setattr(env, key, value) 61 | elif not hasattr(env, key): 62 | setattr(env, key, value) 63 | 64 | RELEASE_PATH_FORMAT = '%y%m%d-%H%M%S' 65 | 66 | def set_default_configurations(force=True): 67 | setdefault('show_output', False, force) 68 | setdefault('user', 'deploy', force) 69 | setdefault('use_ssh_config', True, force) 70 | setdefault('sudo_user', 'deploy', force) 71 | setdefault('sudo_prefix', 'sudo -i ', force) 72 | setdefault('shared_writable', True, force) 73 | setdefault('path', '/home/%(sudo_user)s/www/%(app_name)s', force) 74 | setdefault('current_path', '%(path)s/current', force) 75 | setdefault('releases_path', '%(path)s/releases', force) 76 | setdefault('shared_path', '%(path)s/shared', force) 77 | setdefault('new_release', datetime.now().strftime(RELEASE_PATH_FORMAT), force) 78 | setdefault('release_path', '%(releases_path)s/%(new_release)s', force) 79 | setdefault('environment_file', '%(release_path)s/.env', force) 80 | setdefault('environment', {}, force) 81 | setdefault('linked_files', [], force) 82 | setdefault('linked_dirs', [], force) 83 | setdefault('env_role_configs', {}, force) 84 | setdefault('keep_releases', 5, force) 85 | setdefault('stage_role_configs', {}, force) 86 | setdefault('dry_run', False, force) 87 | 88 | def check_stage_and_role(): 89 | stage = env.get('stage') 90 | role = env.get('role') 91 | 92 | # raise error when env/role not set both 93 | if not stage or not role: 94 | abort('stage or role not set!') 95 | 96 | def apply_configurations_to_env(conf): 97 | for env_item in conf: 98 | env_value = conf.get(env_item) 99 | setattr(env, env_item, env_value) 100 | 101 | def apply_role_configurations_to_env(stage, role): 102 | if stage in env.stage_role_configs: 103 | if role in env.stage_role_configs[stage]: 104 | config = env.stage_role_configs[stage][role] 105 | apply_configurations_to_env(config) 106 | 107 | def apply_yaml_to_env(confs, operation): 108 | 109 | from .signal import clear 110 | clear() 111 | 112 | plugins = confs.get(operation + '_plugins') or confs.get('plugins') or [] 113 | 114 | for plugin in plugins: 115 | mod = import_module(plugin) 116 | mod.init() 117 | 118 | for key, value in confs.items(): 119 | setattr(env, key, value) 120 | 121 | def apply_env(stage, role): 122 | env.stage = stage 123 | env.role = role 124 | check_stage_and_role() 125 | set_default_configurations(force=False) 126 | apply_role_configurations_to_env(stage, role) 127 | apply_configurations_to_env(format_definition()) 128 | 129 | 130 | def with_configs(func): 131 | @wraps(func) 132 | def wrapped(*args, **kwargs): 133 | output_func = show if env.show_output else hide 134 | with output_func('output'): 135 | ret = func(*args, **kwargs) 136 | return ret 137 | return wrapped 138 | -------------------------------------------------------------------------------- /fapistrano/deploy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from fabric.api import ( 4 | runs_once, env, cd, 5 | task, abort, show, prefix, 6 | ) 7 | from fabric.contrib.files import exists, append 8 | from fabric.context_managers import shell_env 9 | 10 | from .utils import green_alert, run_function, run 11 | from .configuration import with_configs 12 | from .directory import ( 13 | get_current_release, get_previous_release, 14 | get_linked_files, get_linked_file_dirs, 15 | get_linked_dirs, get_linked_dir_parents, 16 | get_outdated_releases, 17 | ) 18 | from . import signal 19 | 20 | 21 | @task 22 | @with_configs 23 | def restart(): 24 | signal.emit('deploy.restarting') 25 | signal.emit('deploy.restarted') 26 | 27 | 28 | @task 29 | @with_configs 30 | def release(): 31 | green_alert('Starting') 32 | signal.emit('deploy.starting') 33 | run_function(_start_deploy) 34 | 35 | green_alert('Started') 36 | signal.emit('deploy.started') 37 | 38 | green_alert('Updating') 39 | signal.emit('deploy.updating') 40 | 41 | green_alert('Updated') 42 | signal.emit('deploy.updated') 43 | 44 | green_alert('Publishing') 45 | signal.emit('deploy.publishing') 46 | run_function(_symlink_current) 47 | 48 | green_alert('Published') 49 | signal.emit('deploy.published') 50 | 51 | green_alert('Finishing') 52 | signal.emit('deploy.finishing') 53 | run_function(_cleanup) 54 | 55 | green_alert('Finished') 56 | signal.emit('deploy.finished') 57 | 58 | 59 | @task 60 | @with_configs 61 | def resetup_repo(): 62 | with cd('%(current_path)s' % env): 63 | signal.emit('git.building') 64 | signal.emit('git.built') 65 | 66 | @task 67 | @with_configs 68 | def rollback(): 69 | green_alert('Starting') 70 | signal.emit('deploy.starting') 71 | env.rollback_from = get_current_release() 72 | env.rollback_to = get_previous_release() 73 | env.release_path = '%(releases_path)s/%(rollback_to)s' % env 74 | run_function(_check_rollback_to) 75 | 76 | green_alert('Started') 77 | signal.emit('deploy.started') 78 | 79 | green_alert('Reverting') 80 | signal.emit('deploy.reverting') 81 | 82 | green_alert('Reverted') 83 | signal.emit('deploy.reverted') 84 | 85 | green_alert('Publishing') 86 | signal.emit('deploy.publishing') 87 | run_function(_symlink_current) 88 | 89 | green_alert('Published') 90 | signal.emit('deploy.published') 91 | 92 | green_alert('Finishing rollback') 93 | signal.emit('deploy.finishing_rollback') 94 | run_function(_cleanup_rollback) 95 | 96 | green_alert('Finished') 97 | signal.emit('deploy.finished') 98 | 99 | @task 100 | @with_configs 101 | def once(): 102 | green_alert('Running') 103 | with cd(env.current_path), shell_env(**env.environment), show('output'): 104 | run_function(_run_command) 105 | green_alert('Ran') 106 | 107 | 108 | @task 109 | @with_configs 110 | def shell(): 111 | with cd(env.current_path), shell_env(**env.environment), show('output'): 112 | run_function(_run_shell) 113 | 114 | def _run_command(): 115 | if env.run_command: 116 | run(env.run_command) 117 | 118 | def _run_shell(): 119 | if exists('venv/bin/activate'): 120 | with prefix('source venv/bin/activate'): 121 | if exists('manage.py'): 122 | run('python manage.py shell') 123 | return 124 | elif exists('venv/bin/ipython'): 125 | run('venv/bin/ipython') 126 | return 127 | elif exists('venv/bin/python'): 128 | run('venv/bin/python') 129 | return 130 | else: 131 | abort('Sorry, currently only support Python shell.') 132 | 133 | 134 | def _start_deploy(): 135 | _check() 136 | _write_env() 137 | _symlink_shared_files() 138 | 139 | def _write_env(): 140 | if not env.environment: 141 | return 142 | for env_key, env_value in env.environment.items(): 143 | env.env_line = 'export %s="%s"' % (env_key, env_value) 144 | run("echo '%(env_line)s' >> $(echo '%(environment_file)s')" % env) 145 | if not exists(env.environment_file): 146 | run('touch %(environment_file)s' % env) 147 | 148 | def _check(): 149 | run('mkdir -p %(path)s/{releases,shared/log}' % env) 150 | 151 | if env.shared_writable: 152 | run('chmod -R g+w %(shared_path)s' % env) 153 | 154 | run('mkdir -p %(release_path)s' % env) 155 | for linked_file_dir in get_linked_file_dirs(): 156 | dir = '%(release_path)s/' % env 157 | dir += linked_file_dir 158 | run('mkdir -p %s' % dir) 159 | for linked_dir_parent in get_linked_dir_parents(): 160 | dir = '%(release_path)s/' % env 161 | dir += linked_dir_parent 162 | run('mkdir -p %s' % dir) 163 | 164 | def _symlink_shared_files(): 165 | for linked_file in get_linked_files(): 166 | env.linked_file = linked_file 167 | if exists('%(release_path)s/%(linked_file)s' % env): 168 | run('rm %(release_path)s/%(linked_file)s' % env) 169 | run('ln -nfs %(shared_path)s/%(linked_file)s %(release_path)s/%(linked_file)s' % env) 170 | for linked_dir in get_linked_dirs(): 171 | env.linked_dir = linked_dir 172 | if exists('%(release_path)s/%(linked_dir)s' % env): 173 | run('rm -rf %(release_path)s/%(linked_dir)s' % env) 174 | run('ln -nfs %(shared_path)s/%(linked_dir)s %(release_path)s/%(linked_dir)s' % env) 175 | 176 | 177 | def _symlink_current(): 178 | run('ln -nfs %(release_path)s %(current_path)s' % env) 179 | 180 | def _check_rollback_to(): 181 | if not env.release_path: 182 | abort('No release to rollback') 183 | 184 | def _cleanup_rollback(): 185 | run('rm -rf %(releases_path)s/%(rollback_from)s' % env) 186 | 187 | def _cleanup(): 188 | with cd(env.releases_path): 189 | outdated_releases = get_outdated_releases() 190 | if outdated_releases: 191 | run('rm -rf %s' % ' '.join(outdated_releases)) 192 | -------------------------------------------------------------------------------- /fapistrano/directory.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | from fabric.api import run, env 5 | 6 | # TODO: remove most of it, because we have better default option now. 7 | 8 | def set_path(): 9 | env.path = '/home/%(user)s/www/%(project_name)s' % env 10 | 11 | def get_path(): 12 | if not hasattr(env, 'path'): 13 | set_path() 14 | return env.path 15 | 16 | def set_releases_path(): 17 | env.releases_path = '%s/releases' % get_path() 18 | 19 | def get_releases_path(): 20 | if not hasattr(env, 'releases_path'): 21 | set_releases_path() 22 | return env.releases_path 23 | 24 | def set_shared_path(): 25 | env.shared_path = '%s/shared' % get_path() 26 | 27 | def get_shared_path(): 28 | if not hasattr(env, 'shared_path'): 29 | set_shared_path() 30 | return env.shared_path 31 | 32 | def set_current_path(): 33 | env.current_path = '%s/current' % get_path() 34 | 35 | def get_current_path(): 36 | if not hasattr(env, 'current_path'): 37 | set_current_path() 38 | return env.current_path 39 | 40 | def _get_all_releases(): 41 | return sorted(run('ls -x %(releases_path)s' % env).split()) 42 | 43 | def set_all_releases(): 44 | env.releases = _get_all_releases() 45 | 46 | def get_all_releases(): 47 | if not hasattr(env, 'releases'): 48 | set_all_releases() 49 | return env.releases 50 | 51 | def set_current_release(): 52 | env.current_release = run('readlink %(current_path)s' % env).rsplit('/', 1)[1] 53 | 54 | def get_current_release(): 55 | if not hasattr(env, 'current_release'): 56 | set_current_release() 57 | return env.current_release 58 | 59 | def set_previous_release(): 60 | current_index = get_all_releases().index(env.current_release) 61 | if current_index > 1: 62 | env.previous_release = env.releases[current_index-1] 63 | else: 64 | env.previous_release = None 65 | 66 | def get_previous_release(): 67 | if not hasattr(env, 'previous_release'): 68 | set_previous_release() 69 | return env.previous_release 70 | 71 | def set_dirty_releases(): 72 | all_releases = get_all_releases() 73 | current_release = get_current_release() 74 | current_index = all_releases.index(current_release) 75 | if len(all_releases) != current_index + 1: 76 | env.dirty_releases = all_releases[current_index + 1:] 77 | else: 78 | env.dirty_releases = [] 79 | 80 | def get_dirty_releases(): 81 | if not hasattr(env, 'dirty_releases'): 82 | set_dirty_releases() 83 | return env.dirty_releases 84 | 85 | def get_keep_releases_count(): 86 | if not hasattr(env, 'keep_releases'): 87 | env.keep_releases = 5 88 | return env.keep_releases 89 | 90 | def get_outdated_releases(): 91 | all_releases = _get_all_releases() 92 | keep_releases_count = get_keep_releases_count() 93 | if len(all_releases) > keep_releases_count: 94 | directories = list(reversed(all_releases)) 95 | del directories[:env.keep_releases] 96 | return directories 97 | else: 98 | return [] 99 | 100 | def set_linked_files(): 101 | env.linked_files = [] 102 | 103 | def get_linked_files(): 104 | if not hasattr(env, 'linked_files'): 105 | set_linked_files() 106 | return env.linked_files 107 | 108 | def set_linked_dirs(): 109 | env.linked_dirs = [] 110 | 111 | def get_linked_dirs(): 112 | if not hasattr(env, 'linked_dirs'): 113 | set_linked_dirs() 114 | return env.linked_dirs 115 | 116 | def get_linked_file_dirs(): 117 | linked_files = get_linked_files() 118 | return set(map(os.path.dirname, linked_files)) 119 | 120 | def get_linked_dir_parents(): 121 | linked_dirs = get_linked_dirs() 122 | return set(map(os.path.dirname, linked_dirs)) 123 | -------------------------------------------------------------------------------- /fapistrano/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liwushuo/fapistrano/2a31aad01a04d7ea9108dc6f95aee9a53290459f/fapistrano/plugins/__init__.py -------------------------------------------------------------------------------- /fapistrano/plugins/curl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | from contextlib import contextmanager 5 | 6 | from fabric.api import cd, env, show, hide 7 | from .. import signal, configuration 8 | from ..utils import run 9 | 10 | def init(): 11 | configuration.setdefault('curl_url', '') 12 | configuration.setdefault('curl_output', '') 13 | configuration.setdefault('curl_options', '') 14 | configuration.setdefault('curl_extract_tar', '') 15 | configuration.setdefault('curl_extract_tgz', '') 16 | configuration.setdefault('curl_user', '') 17 | configuration.setdefault('curl_postinstall_script', '') 18 | configuration.setdefault('curl_postinstall_output', True) 19 | signal.register('deploy.updating', download_artifact) 20 | 21 | class StreamFilter(object): 22 | 23 | def __init__(self, filter, stream): 24 | self.stream = stream 25 | self.filter = filter 26 | 27 | def write(self,data): 28 | if not self.filter: 29 | self.stream.write(data) 30 | self.stream.flush() 31 | else: 32 | user = self.filter[:self.filter.index(':')] 33 | data = data.replace(self.filter, '%s:**************' % user) 34 | self.stream.write(data) 35 | self.stream.flush() 36 | 37 | def flush(self): 38 | self.stream.flush() 39 | 40 | @contextmanager 41 | def credential_output(): 42 | sys_stdout = sys.stdout 43 | credential_stdout = StreamFilter(env.curl_user, sys_stdout) 44 | 45 | sys.stdout = credential_stdout 46 | yield 47 | sys.stdout = sys_stdout 48 | 49 | def download_artifact(**kwargs): 50 | with cd(env.release_path), credential_output(): 51 | cmd = 'curl --max-time 30 --retry 3 %(curl_url)s' % env 52 | if env.curl_user: 53 | cmd += ' --user %(curl_user)s' % env 54 | if env.curl_output: 55 | cmd += ' -o %(curl_output)s' % env 56 | if env.curl_options: 57 | cmd += ' %(curl_options)s' % env 58 | if env.curl_extract_tar: 59 | cmd += ' | tar -x' 60 | elif env.curl_extract_tgz: 61 | cmd += ' | tar -xz' 62 | run(cmd) 63 | if env.curl_postinstall_script: 64 | output = show if env.curl_postinstall_output else hide 65 | with output('output'): 66 | run(env.curl_postinstall_script) 67 | -------------------------------------------------------------------------------- /fapistrano/plugins/fis.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from fabric.api import show, env, cd, show, hide 4 | 5 | from .. import signal, configuration 6 | from ..utils import run 7 | 8 | def init(): 9 | configuration.setdefault('fis_output', False) 10 | configuration.setdefault('fis_domains', False) 11 | configuration.setdefault('fis_md5', True) 12 | configuration.setdefault('fis_optimize', True) 13 | configuration.setdefault('fis_pack', True) 14 | configuration.setdefault('fis_conf', 'fis-conf.js') 15 | signal.register('deploy.updated', build_fis_assets) 16 | # FIXME: asset fis install 17 | 18 | def build_fis_assets(): 19 | output = show if env.fis_output else hide 20 | with output('output'): 21 | with cd('%(release_path)s/%(fis_source)s' % env): 22 | cmd = ( 23 | 'fis release ' 24 | '--file %(fis_conf)s ' 25 | '--dest %(fis_dest)s ' 26 | ) % env 27 | if env.fis_md5: 28 | cmd += '--md5 ' 29 | if env.fis_optimize: 30 | cmd += '--optimize ' 31 | if env.fis_pack: 32 | cmd += '--pack ' 33 | if env.fis_domains: 34 | cmd += '--domains ' 35 | run(cmd) 36 | -------------------------------------------------------------------------------- /fapistrano/plugins/git.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from fabric.api import cd, env, local, task, abort 4 | from fabric.contrib.files import exists 5 | 6 | from ..utils import green_alert, red_alert, yellow_alert, run 7 | from .. import signal, configuration 8 | 9 | def init(): 10 | configuration.setdefault('git_shallow_clone', 1) 11 | configuration.setdefault('branch', 'master') 12 | configuration.setdefault('revision_file', 'REVISION') 13 | configuration.setdefault('repo_path', '%(path)s/repo') 14 | configuration.setdefault('repo_head_path', '%(repo_path)s/HEAD') 15 | configuration.setdefault('git_archive_tree', '') 16 | signal.register('deploy.started', check_repo) 17 | signal.register('deploy.reverting', log_previous_revision) 18 | signal.register('deploy.finishing_rollback', log_rollback_revision) 19 | signal.register('deploy.updating', update_git_repo) 20 | 21 | 22 | def check_repo(**kwargs): 23 | _check() 24 | 25 | 26 | def log_previous_revision(**kwargs): 27 | if _does_current_revision_exist(): 28 | head = _read_current_revision() 29 | green_alert('Rollback from %s' % head) 30 | 31 | 32 | def log_rollback_revision(**kwargs): 33 | if _does_current_revision_exist(): 34 | head = _read_current_revision() 35 | green_alert('Rollback to %s' % head) 36 | 37 | 38 | def update_git_repo(**kwargs): 39 | _update() 40 | _set_current_version() 41 | _release() 42 | _echo_revision() 43 | 44 | 45 | def _check(): 46 | if not exists(env.repo_path): 47 | _clone() 48 | 49 | with cd(env.repo_path): 50 | run('git ls-remote --heads %(repo)s' % env) 51 | 52 | 53 | def _clone(): 54 | if exists(env.repo_head_path): 55 | abort('Repo has cloned already!') 56 | 57 | if env.git_shallow_clone: 58 | run('git clone --mirror --depth %(git_shallow_clone)s ' 59 | '--no-single-branch %(repo)s %(repo_path)s' % env) 60 | else: 61 | run('git clone --mirror %(repo)s %(repo_path)s' % env) 62 | 63 | 64 | def _update(): 65 | with cd(env.repo_path): 66 | if env.git_shallow_clone: 67 | run('git fetch --depth %(git_shallow_clone)s origin %(branch)s' % env) 68 | else: 69 | run('git remote update --prune') 70 | 71 | 72 | def _release(): 73 | with cd(env.repo_path): 74 | if env.git_archive_tree: 75 | env.git_strip_components = len(env.git_archive_tree.split('/')) 76 | run('git archive %(branch)s %(git_archive_tree)s | tar -x --strip-components %(git_strip_components)d -f - -C %(release_path)s/' % env) 77 | else: 78 | run('git archive %(branch)s | tar -x -f - -C %(release_path)s/' % env) 79 | 80 | 81 | def _get_revision(): 82 | with cd(env.repo_path): 83 | return run( 84 | 'git rev-list ' 85 | '--max-count=1 ' 86 | '--abbrev-commit ' 87 | '--abbrev=12 ' 88 | '%(branch)s' % env 89 | ) 90 | 91 | 92 | def _echo_revision(): 93 | with cd(env.release_path): 94 | run('echo %(current_version)s >> %(revision_file)s' % env) 95 | 96 | 97 | def _read_current_revision(): 98 | return run('cat %(current_path)s/%(revision_file)s' % env) 99 | 100 | def _does_current_revision_exist(): 101 | return exists('%(current_path)s/$(revision_file)s' % env) 102 | 103 | def _set_current_version(): 104 | env.current_version = _get_revision() 105 | -------------------------------------------------------------------------------- /fapistrano/plugins/git2slack.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | 5 | import yaml 6 | 7 | from fabric.api import env 8 | from .. import signal 9 | 10 | def init(): 11 | signal.register('git.delta.publishing', _publish_git_delta_to_slack) 12 | signal.register('git.head.publishing', _publish_git_head_to_slack) 13 | signal.register('git.reverted', _publish_rollback_head_to_slack) 14 | signal.register('git.updated', _publish_updated_delta_to_slack) 15 | 16 | 17 | def _publish_git_delta_to_slack(**kwargs): 18 | payload = _format_delta_payload(kwargs.get('delta_log')) 19 | signal.emit('slack.send', payload=payload) 20 | 21 | def _publish_git_head_to_slack(**kwargs): 22 | target = _format_target() 23 | head = kwargs.get('head') 24 | head = _format_git_commit(head) 25 | text = """[%s] Current head: %s""" % (target, head) 26 | signal.emit('slack.send', text=text) 27 | 28 | def _publish_rollback_head_to_slack(**kwargs): 29 | target = _format_target() 30 | head = kwargs.get('head') 31 | head = _format_git_commit(head) 32 | text = """[%s] Rollback to %s""" % (target, head) 33 | signal.emit('slack.send', text=text) 34 | 35 | def _publish_updated_delta_to_slack(**kwargs): 36 | delta_log = kwargs.get('delta_log') 37 | remote_head = kwargs.get('head') 38 | payload = _format_release_payload(remote_head, delta_log) 39 | signal.emit('slack.send', payload=payload) 40 | 41 | def _format_delta_payload(delta_log): 42 | notes = '[%s] Please check if the commits are ready to deploy.' % _format_target() 43 | return _format_common_gitlog_payload(delta_log, notes, '#aaccaa') 44 | 45 | def _format_target(): 46 | return '{app_name}-{env}'.format(**env) 47 | 48 | def _format_common_gitlog_payload(gitlog, notes, color='#D00000'): 49 | text = u'```%s```\n%s' % (gitlog if gitlog else 'No commit.', notes) 50 | 51 | richlog = _format_git_richlog(gitlog) 52 | if not richlog: 53 | payload = { 'text': text } 54 | else: 55 | payload = { 56 | 'attachments': [ 57 | { 58 | 'fallback': text, 59 | 'color': color, 60 | 'fields': [ 61 | richlog, 62 | { 63 | 'title': 'Notes', 64 | 'value': notes 65 | }, 66 | ], 67 | }, 68 | ] 69 | } 70 | 71 | return payload 72 | 73 | def _format_git_richlog(text): 74 | if not text: 75 | return 76 | 77 | conf = _get_config() 78 | git_web = conf.get('git_web') 79 | if not git_web: 80 | return 81 | commits = [] 82 | 83 | for line in text.splitlines(): 84 | commit_hash, commit_log = line.split(' ', 1) 85 | commits.append(u'<{git_web}{commit_hash}|{commit_hash}> {commit_log}'.format(**locals())) 86 | return { 87 | 'value': u'\n'.join(commits) if commits else 'No commit.' 88 | } 89 | 90 | def _get_config(): 91 | try: 92 | with open(os.path.expanduser('~/.fapistranorc')) as f: 93 | configs = yaml.load(f) 94 | return configs.get(os.getcwd(), {}) 95 | except IOError: 96 | return {} 97 | 98 | def _format_git_commit(commit): 99 | conf = _get_config() 100 | git_web = conf.get('git_web') 101 | if not git_web: 102 | return commit 103 | return u'<%s%s|%s>' % (git_web, commit, commit) 104 | 105 | def _format_release_payload(remote_head, delta_log): 106 | notes = '[%s] Deploy to %s. Please check if it works properly.' % ( 107 | _format_target(), _format_git_commit(remote_head) 108 | ) 109 | return _format_common_gitlog_payload(delta_log, notes) 110 | -------------------------------------------------------------------------------- /fapistrano/plugins/localshared.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from fabric.api import env 5 | from fabric.contrib.files import exists 6 | from .. import signal, configuration, directory 7 | from ..utils import red_alert, run 8 | 9 | def init(): 10 | configuration.setdefault('localshared_source', '') 11 | signal.register('deploy.starting', copy_localshared_linked_files) 12 | 13 | def copy_localshared_linked_files(): 14 | _check_shared_file_dirs() 15 | _check_shared_dir_parents() 16 | _copy_linked_files() 17 | _copy_linked_dirs() 18 | 19 | def _check_shared_file_dirs(): 20 | for linked_file_dir in directory.get_linked_file_dirs(): 21 | env.linked_file_dir = linked_file_dir 22 | run('mkdir -p %(shared_path)s/%(linked_file_dir)s' % env) 23 | del env['linked_file_dir'] 24 | 25 | def _check_shared_dir_parents(): 26 | for linked_dir_parent in directory.get_linked_dir_parents(): 27 | env.linked_dir_parent = linked_dir_parent 28 | run('mkdir -p %(shared_path)s/%(linked_dir_parent)s' % env) 29 | del env['linked_dir_parent'] 30 | 31 | def _copy_linked_files(): 32 | for linked_file in directory.get_linked_files(): 33 | env.linked_file = linked_file 34 | if exists('%(localshared_source)s/%(linked_file)s' % env): 35 | run('cp %(localshared_source)s/%(linked_file)s %(shared_path)s/%(linked_file)s' % env) 36 | else: 37 | red_alert('Missing %(localshared_source)s/%(linked_file)s' % env) 38 | del env['linked_file'] 39 | 40 | def _copy_linked_dirs(): 41 | for linked_dir in directory.get_linked_dirs(): 42 | env.linked_dir = linked_dir 43 | if exists('%(localshared_source)s/%(linked_dir)s' % env): 44 | run('cp -R %(localshared_source)s/%(linked_dir)s/* %(shared_path)s/%(linked_dir)s' % env) 45 | else: 46 | red_alert('Missing %(localshared_source)s/%(linked_dir)s' % env) 47 | del env['linked_dir'] 48 | -------------------------------------------------------------------------------- /fapistrano/plugins/slack.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | import requests 5 | import atexit 6 | 7 | from fabric.api import env 8 | from fabric.api import task 9 | 10 | from ..utils import red_alert 11 | from .. import signal 12 | 13 | slack_sendbox = [] 14 | 15 | def init(): 16 | signal.register('slack.send', _send_data_to_sendbox) 17 | 18 | @task 19 | def send_message(text, icon_emoji=':trollface:', timeout=10): 20 | assert env.slack_webhook 21 | signal.emit('slack.send', text=text, icon_emoji=icon_emoji, timeout=timeout) 22 | 23 | def _check_slack_sendbox(data): 24 | if data in slack_sendbox: 25 | return False 26 | slack_sendbox.append(data) 27 | return True 28 | 29 | def _send_data_to_sendbox(**kwargs): 30 | if 'text' in kwargs: 31 | payload = { 32 | 'text': kwargs.get('text'), 33 | 'icon_emoji': kwargs.get('icon_emoji', ':trollface:'), 34 | } 35 | elif 'payload' in kwargs: 36 | payload = kwargs['payload'] 37 | else: 38 | red_alert('Nothing to be sent to slack.') 39 | return 40 | 41 | if hasattr(env, 'slack_channel'): 42 | payload['channel'] = env.slack_channel 43 | 44 | data = json.dumps(payload) 45 | 46 | if not _check_slack_sendbox(data): 47 | return 48 | 49 | def _call_slack_webhook(data): 50 | requests.post(env.slack_webhook, data=data, timeout=10) 51 | 52 | @atexit.register 53 | def send(): 54 | for data in slack_sendbox: 55 | _call_slack_webhook(data) 56 | -------------------------------------------------------------------------------- /fapistrano/plugins/supervisorctl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from fabric.api import env, show, hide, abort 4 | from fabric.contrib.files import exists 5 | from .. import signal, configuration 6 | from ..utils import run 7 | 8 | def init(): 9 | configuration.setdefault('supervisor_restart', True) 10 | configuration.setdefault('supervisor_refresh', False) 11 | configuration.setdefault('supervisor_output', False) 12 | configuration.setdefault('supervisor_check_status', False) 13 | configuration.setdefault('supervisor_program', '%(app_name)s') 14 | configuration.setdefault( 15 | 'supervisor_target', 16 | '/etc/supervisor/conf.d/%(supervisor_program)s.conf' 17 | ) 18 | configuration.setdefault( 19 | 'supervisor_conf', 20 | 'configs/supervisor_%(stage)s_%(role)s.conf' 21 | ) 22 | signal.register('deploy.started', _check_supervisor_config) 23 | signal.register('deploy.published', _restart_service_via_supervisor) 24 | signal.register('deploy.restarting', _restart_service_via_supervisor) 25 | 26 | def _check_supervisor_config(**kwargs): 27 | run('ln -nfs %(current_path)s/%(supervisor_conf)s %(supervisor_target)s' % env) 28 | 29 | def _restart_service_via_supervisor(**kwargs): 30 | output = show if env.supervisor_output else hide 31 | with output('output'): 32 | if env.supervisor_refresh: 33 | run('supervisorctl stop %(supervisor_program)s' % env) 34 | run('supervisorctl reread') 35 | if not run('supervisorctl update'): 36 | run('supervisorctl start %(supervisor_program)s' % env) 37 | elif env.supervisor_restart: 38 | run('supervisorctl restart %(supervisor_program)s' % env) 39 | 40 | # FIXME: refresh group need supervisor>=3.20 41 | if env.supervisor_check_status: 42 | with show('output'): 43 | run('supervisorctl status %(supervisor_program)s' % env) 44 | -------------------------------------------------------------------------------- /fapistrano/plugins/virtualenv.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | """ 4 | from fabric.api import env, prefix, cd 5 | from fabric.contrib.files import exists 6 | from .. import signal, configuration 7 | from ..utils import run 8 | 9 | def init(): 10 | configuration.setdefault('virtualenv_executive', '/usr/bin/env virtualenv') 11 | configuration.setdefault('virtualenv_requirements', '%(release_path)s/requirements.txt') 12 | configuration.setdefault('virtualenv_pip_upgrade', True) 13 | configuration.setdefault('virtualenv_venv_path', '%(release_path)s/venv') 14 | configuration.setdefault( 15 | 'virtualenv_activate', 'source %(virtualenv_venv_path)s/bin/activate' 16 | ) 17 | signal.register('deploy.updated', build_python_env) 18 | 19 | def build_python_env(): 20 | _check_virtualenv_env() 21 | 22 | if env.virtualenv_pip_upgrade: 23 | _upgrade_pip() 24 | 25 | _install_requirements() 26 | 27 | def _check_virtualenv_env(): 28 | if not exists(env.virtualenv_venv_path % env): 29 | run('%(virtualenv_executive)s %(virtualenv_venv_path)s' % env) 30 | 31 | def _upgrade_pip(): 32 | with prefix(env.virtualenv_activate): 33 | run('pip install -U pip setuptools wheel') 34 | 35 | def _install_requirements(): 36 | with prefix(env.virtualenv_activate): 37 | run('pip install -r %(virtualenv_requirements)s' % env) 38 | -------------------------------------------------------------------------------- /fapistrano/plugins/virtualenvtools.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from fabric.api import env, cd, show 4 | from .. import signal, configuration 5 | from ..utils import run 6 | 7 | def init(): 8 | configuration.setdefault('virtualenvtools_strategy', 'update-path') 9 | configuration.setdefault('virtualenvtools_executable', 'virtualenv-tools') 10 | configuration.setdefault('virtualenvtools_venv_path', '%(release_path)s/venv') 11 | signal.register('deploy.updated', relocate_env) 12 | 13 | def relocate_env(): 14 | with cd(env.release_path): 15 | with show('output'): 16 | if env.virtualenvtools_strategy == 'update-path': 17 | run('%(virtualenvtools_executable)s --update-path %(virtualenvtools_venv_path)s' % env) 18 | elif env.virtualenvtools_strategy == 'reinitialize': 19 | run('rm %(virtualenvtools_venv_path)s/bin/python*' % env) 20 | run('%(virtualenvtools_executable)s --reinitialize %(virtualenvtools_venv_path)s' % env) 21 | -------------------------------------------------------------------------------- /fapistrano/plugins/virtualenvwrapper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from fabric.api import env, prefix, cd 4 | from fabric.contrib.files import exists 5 | from .. import signal, configuration 6 | from ..utils import run 7 | 8 | def init(): 9 | configuration.setdefault('virtualenvwrapper_source', '/usr/local/bin/virtualenvwrapper.sh') 10 | configuration.setdefault('virtualenvwrapper_home', '/home/%(user)s/.virtualenvs') 11 | configuration.setdefault('virtualenvwrapper_project', '/%(virtualenvwrapper_home)s/%(app_name)s') 12 | configuration.setdefault('virtualenvwrapper_pip_upgrade', True) 13 | configuration.setdefault('virtualenvwrapper_requirements', '%(release_path)s/requirements.txt') 14 | configuration.setdefault( 15 | 'virtualenvwrapper_activate', 16 | 'source %(virtualenvwrapper_project)s/bin/activate' 17 | ) 18 | signal.register('deploy.updated', check_python_env) 19 | 20 | 21 | def check_python_env(): 22 | _check_virtualenvwrapper_env() 23 | if env.virtualenvwrapper_pip_upgrade: 24 | _upgrade_pip() 25 | _install_requirements() 26 | 27 | 28 | def _check_virtualenvwrapper_env(): 29 | if not exists(env.virtualenvwrapper_project): 30 | run('source %(virtualenvwrapper_source)s && mkvirtualenv %(app_name)s' % env) 31 | 32 | 33 | def _upgrade_pip(): 34 | with prefix(env.virtualenvwrapper_activate): 35 | run('pip install -q -U pip setuptools wheel || pip install -U pip setuptools wheel') 36 | 37 | 38 | def _install_requirements(): 39 | with prefix(env.virtualenvwrapper_activate): 40 | run('pip install -r %(virtualenvwrapper_requirements)s' % env) 41 | -------------------------------------------------------------------------------- /fapistrano/signal.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from functools import wraps 4 | from .utils import run_function 5 | 6 | class Signal(object): 7 | 8 | def __init__(self, name, doc=''): 9 | self.name = name 10 | self.doc = doc 11 | self.receivers = {} 12 | 13 | class Namespace(dict): 14 | 15 | def signal(self, name, doc=None): 16 | try: 17 | return self[name] 18 | except KeyError: 19 | return self.setdefault(name, Signal(name, doc)) 20 | 21 | namespace = Namespace() 22 | 23 | def clear(): 24 | namespace.clear() 25 | 26 | def emit(event, **data): 27 | if event not in namespace: 28 | return 29 | for id, func in namespace[event].receivers.items(): 30 | run_function(func, **data) 31 | 32 | def register(event, function): 33 | assert callable(function), 'Function must be callable.' 34 | namespace.signal(event).receivers[id(function)] = function 35 | 36 | def listen(event): 37 | def decorator(f): 38 | @wraps(f) 39 | def deco(*args, **kwargs): 40 | register(event, f) 41 | return f(*args, **kwargs) 42 | return deco 43 | return decorator 44 | 45 | 46 | if __name__ == '__main__': 47 | def handle_hello(**data): 48 | print 'received data:', data 49 | register('hello', handle_hello) 50 | emit('hello', keyword='world') 51 | -------------------------------------------------------------------------------- /fapistrano/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from fabric.api import env, run as fabrun, sudo, settings 4 | from fabric.colors import green, red, white, yellow 5 | 6 | 7 | def red_alert(msg, bold=True): 8 | print red('===>', bold=bold), white(msg, bold=bold) 9 | 10 | 11 | def green_alert(msg, bold=True): 12 | print green('===>', bold=bold), white(msg, bold=bold) 13 | 14 | 15 | def yellow_alert(msg, bold=True): 16 | print yellow('===>', bold=bold), white(msg, bold=bold) 17 | 18 | def dry_run_function(function, **data): 19 | green_alert(' '.join(function.__name__.split('_'))) 20 | 21 | def run_function(function, **data): 22 | if env.dry_run: 23 | dry_run_function(function, **data) 24 | else: 25 | function(**data) 26 | 27 | def run(command, sudo_user=None): 28 | sudo_user = sudo_user or env.sudo_user 29 | if not sudo_user or sudo_user == env.user: 30 | fabrun(command % env) 31 | else: 32 | with settings(sudo_user=sudo_user): 33 | sudo(command % env) 34 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.9.5 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | 8 | [bumpversion:file:fapistrano/__init__.py] 9 | 10 | [bumpversion:file:docs/conf.py] 11 | 12 | [wheel] 13 | universal = 1 14 | 15 | [flake8] 16 | exclude = docs 17 | 18 | [build_sphinx] 19 | source-dir = docs/ 20 | build-dir = docs/_build 21 | all_files = 1 22 | 23 | [upload_sphinx] 24 | upload-dir = docs/_build/html 25 | 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | from setuptools import setup, find_packages 6 | 7 | with open('README.md') as readme_file: 8 | readme = readme_file.read() 9 | 10 | with open('HISTORY.rst') as history_file: 11 | history = history_file.read() 12 | 13 | requirements = [ 14 | 'Fabric', 15 | 'requests', 16 | 'PyYaml', 17 | 'click', 18 | ] 19 | 20 | test_requirements = [ 21 | # TODO: put package test requirements here 22 | ] 23 | 24 | setup( 25 | name='fapistrano', 26 | version='0.9.5', 27 | license='MIT', 28 | description="A remote server automation and deployment tool.", 29 | long_description=readme + '\n\n' + history, 30 | zip_safe=False, 31 | include_package_data=True, 32 | install_requires=requirements, 33 | platforms='any', 34 | author="Ju Lin", 35 | author_email='soasme@gmail.com', 36 | url='https://github.com/liwushuo/fapistrano', 37 | packages=find_packages(), 38 | package_dir={'fapistrano': 'fapistrano'}, 39 | entry_points=""" 40 | [console_scripts] 41 | fap=fapistrano.cli:fap 42 | """, 43 | keywords='fapistrano, deploy, deployment, automate, automation, fabric, remote, production, staging, development', 44 | classifiers=[ 45 | 'Development Status :: 2 - Pre-Alpha', 46 | 'Intended Audience :: Developers', 47 | 'License :: OSI Approved :: MIT License', 48 | 'Operating System :: OS Independent', 49 | 'Programming Language :: Python', 50 | "Programming Language :: Python :: 2", 51 | 'Programming Language :: Python :: 2.7', 52 | ], 53 | test_suite='tests', 54 | tests_require=test_requirements 55 | ) 56 | --------------------------------------------------------------------------------