|
| 1 | +""" |
| 2 | +This fab file takes the QIPR project and deploys it using a provided settings file. |
| 3 | +The settings files contains secrets, so it should be kept locally somewhere outside |
| 4 | +the git directory. The secrets file path in this script needs to be changed to match your installation. |
| 5 | +
|
| 6 | +
|
| 7 | +The basic deployment command (for staging) is "fab s deploy:0.7.0" |
| 8 | +fab - calling the fab command |
| 9 | +s - using the alias for stage, but could also be invoked using the whole word "stage" |
| 10 | +deploy - function to run called deploy |
| 11 | +:0.7.0 - run deploy and pass in the argument 0.7.0 |
| 12 | +
|
| 13 | +The settings file needs to include the following fields with the correct settings: |
| 14 | +[fabric_deploy] |
| 15 | +deploy_user = deploy #user account associated with deploying |
| 16 | +deploy_user_group = www-data #group that the deploy account is part of |
| 17 | +staging_host = my.staging.host.com #staging server |
| 18 | +production_host = my.production.host.com #production server |
| 19 | +admin_user = superman #account with root privilege on the server |
| 20 | +live_pre_path = /var/www #directory path leading up to the actual project directory |
| 21 | +backup_pre_path = /var/www.backup #directory path leading up to the backup project directory |
| 22 | +project_path = qipr/approver #where the project files are going to sit |
| 23 | +ssh_keyfile_path = /my/user/.ssh/id_rsa #path to a local ssh private key for easy login |
| 24 | +
|
| 25 | +In order to keep deployments easy, the setups across all environments will remain |
| 26 | +constant, with the only difference being the server ip. The directory paths will |
| 27 | +remain constant. |
| 28 | +
|
| 29 | +There are 3 environments this script will be able to deploy to: |
| 30 | +vagrant: includes the default admin user name and ssh key location, ip, and port |
| 31 | +staging: this is the test live server. |
| 32 | +production: this is the functional live server. |
| 33 | +Each environment includes a boolean flag to enable running as root for certain |
| 34 | +functions in this script. |
| 35 | +
|
| 36 | +Running deploy is the main function to get a version sent off to the server. |
| 37 | +
|
| 38 | +If the server is not setup for deployment yet, there are some additional helper |
| 39 | +functions to create a deployment user and set up its permissions. |
| 40 | +setup_server - creates the user and creates directories |
| 41 | +setup_webspace - just creates the directories for the deploy user |
| 42 | +
|
| 43 | +There are also functions to add ssh keys to the deploy user for simple |
| 44 | +ssh commands using a passed in string or pub file location. |
| 45 | +""" |
| 46 | + |
| 47 | +from fabric.api import * |
| 48 | +from fabric.contrib.files import exists |
| 49 | +from fabric.utils import abort |
| 50 | +from datetime import datetime |
| 51 | +import configparser, string, random, os |
| 52 | + |
| 53 | +config = configparser.ConfigParser() |
| 54 | +settings_file_path = 'qipr/deploy/settings.ini' #path to where app is looking for settings.ini |
| 55 | + |
| 56 | +def get_config(key): |
| 57 | + return config.get("fabric_deploy", key) |
| 58 | + |
| 59 | +def define_env(): |
| 60 | + """ |
| 61 | + This function sets up some global variables |
| 62 | + """ |
| 63 | + |
| 64 | + #first, copy the secrets file into the deploy directory |
| 65 | + if os.path.exists(settings_file_path): |
| 66 | + config.read(settings_file_path) |
| 67 | + else: |
| 68 | + print("The secrets file path cannot be found. It is set to: %s" % settings_file_path) |
| 69 | + abort("Secrets File not set") |
| 70 | + |
| 71 | + env.user = get_config('deploy_user') #default ssh deploy user account |
| 72 | + env.project_name = get_config('project_name') |
| 73 | + env.project_settings_path = get_config('project_settings_path') |
| 74 | + env.live_project_full_path = get_config('live_pre_path') + "/" + get_config('project_path') # |
| 75 | + env.backup_project_full_path = get_config('backup_pre_path') + "/" + get_config('project_path') |
| 76 | + env.key_filename = get_config('ssh_keyfile_path') |
| 77 | + |
| 78 | +@task(alias='v') |
| 79 | +def vagrant(admin=False): |
| 80 | + """ |
| 81 | + Set up deployment for vagrant |
| 82 | +
|
| 83 | + admin: True or False depending on if running as admin |
| 84 | + """ |
| 85 | + #TODO: vagrant ssh-config gives these details, we can read them and strip them out automatically |
| 86 | + |
| 87 | + define_env() |
| 88 | + if admin: |
| 89 | + env.user = 'vagrant' |
| 90 | + env.key_filename = '../vagrant/.vagrant/machines/qipr/virtualbox/private_key' #This is the hardcoded vagrant key based on this projects file structure |
| 91 | + |
| 92 | + env.hosts = ['127.0.0.1'] |
| 93 | + env.port = '2222' |
| 94 | + |
| 95 | +@task(alias='s') |
| 96 | +def stage(admin=False): |
| 97 | + """ |
| 98 | + Set up deployment for staging server |
| 99 | +
|
| 100 | + admin: True or False depending on if running as admin |
| 101 | + """ |
| 102 | + |
| 103 | + define_env() |
| 104 | + if admin: |
| 105 | + env.user = get_config('admin_user') |
| 106 | + |
| 107 | + env.hosts = get_config('staging_host') |
| 108 | + #env.port = ### #Uncomment this line if a specific port is required |
| 109 | + |
| 110 | +@task(alias='p') |
| 111 | +def prod(admin=False): |
| 112 | + """ |
| 113 | + Set up deployment for production server |
| 114 | +
|
| 115 | + admin: True or False depending on if running as admin |
| 116 | + """ |
| 117 | + |
| 118 | + define_env() |
| 119 | + if admin: |
| 120 | + env.user = get_config('admin_user') |
| 121 | + |
| 122 | + env.hosts = get_config('production_host') |
| 123 | + #env.port = ### #Uncomment this line if a specific port is required |
| 124 | + |
| 125 | +@task |
| 126 | +def git_version(version): |
| 127 | + """ |
| 128 | + Pulls the requested version from Master for deployment |
| 129 | +
|
| 130 | + version: the version applied to the tag of the release |
| 131 | + Assumptions: |
| 132 | + git is installed and configured |
| 133 | + Master branch contains releases with the version number as a tag |
| 134 | + """ |
| 135 | + |
| 136 | + # local("git stash save 'Stashing current changes while releasing version %s'" % version) |
| 137 | + local("git fetch --all; git checkout %s" % (version)) |
| 138 | + |
| 139 | +def package_files(): |
| 140 | + """ |
| 141 | + This function will go into the project directory and zip all |
| 142 | + of the required files |
| 143 | + """ |
| 144 | + #pull out file name for reuse |
| 145 | + env.package_name = '%(project_name)s-%(project_version)s.tar.gzip' % env |
| 146 | + |
| 147 | + #create the package |
| 148 | + local("tar -cz --exclude='__pycache__' --exclude='.DS_Store' \ |
| 149 | + -f %(package_name)s \ |
| 150 | + manage.py requirements.txt \ |
| 151 | + qipr/deploy/settings.ini \ |
| 152 | + qipr/__init__.py \ |
| 153 | + qipr/settings.py \ |
| 154 | + qipr/urls.py \ |
| 155 | + qipr/wsgi.py \ |
| 156 | + registry/ \ |
| 157 | + static/" % env) |
| 158 | + |
| 159 | +def create_backup(): |
| 160 | + """ |
| 161 | + This function creates a backup of the current live directory using the project name and current time |
| 162 | + """ |
| 163 | + with cd("%(backup_project_full_path)s" % env): |
| 164 | + run("mkdir -p backup") |
| 165 | + if exists('live'): |
| 166 | + run("tar -cz -f backup/%s-%s.tar.gzip live" % (env.project_name, datetime.now().strftime("%Y%m%dT%H%M%Z"))) |
| 167 | + |
| 168 | +#TODO create restore backup function |
| 169 | + |
| 170 | +def unpackage_files(): |
| 171 | + """ |
| 172 | + Unpackage the file in the remote backup directory |
| 173 | + """ |
| 174 | + with cd("%(backup_project_full_path)s" % env): |
| 175 | + run("mkdir -p archive/%(project_name)s-%(project_version)s" % env) |
| 176 | + run("tar -x -C archive/%(project_name)s-%(project_version)s \ |
| 177 | + -f %(project_name)s-%(project_version)s.tar.gzip" % env) |
| 178 | + |
| 179 | +def create_venv(): |
| 180 | + """ |
| 181 | + Create a new virtual environment and install requirements |
| 182 | + """ |
| 183 | + |
| 184 | + with cd("%(backup_project_full_path)s/archive/%(project_name)s-%(project_version)s" % env): |
| 185 | + run("virtualenv venv") |
| 186 | + run("source venv/bin/activate && pip install -r requirements.txt && deactivate") |
| 187 | + |
| 188 | +def link_to_live(): |
| 189 | + """ |
| 190 | + This function creates a symlink from the backup to the live www directory for the server |
| 191 | + to run the app from. |
| 192 | + """ |
| 193 | + run("ln -sf -T %(backup_project_full_path)s/archive/%(project_name)s-%(project_version)s \ |
| 194 | + %(live_project_full_path)s" % env) |
| 195 | + |
| 196 | +def refresh_server(): |
| 197 | + """ |
| 198 | + Touchs the wsgi file to have the server reload the necessary files |
| 199 | + This may not be needed since we are overwriting the files anyways |
| 200 | + """ |
| 201 | + run("touch %(live_project_full_path)s/%(project_settings_path)s/wsgi.py" % env) |
| 202 | + |
| 203 | +def ship_to_host(): |
| 204 | + """ |
| 205 | + Move the zip file to the correct remote directory |
| 206 | + """ |
| 207 | + put('%(project_name)s-%(project_version)s.tar.gzip' % env, '%(backup_project_full_path)s' % env) |
| 208 | + |
| 209 | +def clean_up(): |
| 210 | + #clean up local files |
| 211 | + local('rm %(project_name)s-%(project_version)s.tar.gzip' % env) |
| 212 | + |
| 213 | + #clean up remote files |
| 214 | + run('rm %(backup_project_full_path)s/%(project_name)s-%(project_version)s.tar.gzip' % env) |
| 215 | + |
| 216 | +@task |
| 217 | +def deploy(version): |
| 218 | + """ |
| 219 | + This function does all the work required to ship code to the |
| 220 | + server being deployed to. |
| 221 | +
|
| 222 | + version: the version applied to the tag of the release |
| 223 | + """ |
| 224 | + |
| 225 | + env.project_version = version |
| 226 | + |
| 227 | + git_version(version) |
| 228 | + package_files() |
| 229 | + ship_to_host() |
| 230 | + create_backup() |
| 231 | + unpackage_files() |
| 232 | + create_venv() |
| 233 | + link_to_live() |
| 234 | + refresh_server() |
| 235 | + #TODO: Run tests, run django validation |
| 236 | + clean_up() |
| 237 | + |
| 238 | +""" |
| 239 | +The following functions are admin level functions which will alter the server. |
| 240 | +They will need to be run with the admin=True flag when setting up the env |
| 241 | +""" |
| 242 | + |
| 243 | +@task |
| 244 | +def setup_webspace(): |
| 245 | + """ |
| 246 | + make www and www.backup directory as admin or root user |
| 247 | +
|
| 248 | + Note: Admin must be true in the environment |
| 249 | + """ |
| 250 | + sudo("mkdir -p %(live_project_full_path)s" % env) |
| 251 | + sudo("mkdir -p %(backup_project_full_path)s/archive" % env) |
| 252 | + |
| 253 | + #Change the permissions to match the correct user and group |
| 254 | + sudo("chown -R %s.%s /var/www.backup" % (get_config('deploy_user'), get_config('deploy_user_group'))) |
| 255 | + sudo("chmod -R 755 /var/www.backup") |
| 256 | + |
| 257 | + sudo("chown -R %s.%s /var/www" % (get_config('deploy_user'), get_config('deploy_user_group'))) |
| 258 | + sudo("chmod -R 755 /var/www/") |
| 259 | + |
| 260 | +@task |
| 261 | +def setup_server(): |
| 262 | + """ |
| 263 | + This function creates the deploy user and sets up the directories being used for the project |
| 264 | + Note: Admin must be true in the environment |
| 265 | + """ |
| 266 | + create_deploy_user_with_ssh() |
| 267 | + setup_webspace() |
| 268 | + |
| 269 | +@task |
| 270 | +def create_deploy_user_with_ssh(): |
| 271 | + """ |
| 272 | + This function will ssh in with the assigned admin user account, |
| 273 | + prompt for password, and create the deployment user. It will place |
| 274 | + this user in the group assigned. |
| 275 | +
|
| 276 | + Note: If ssh is locked to specific users, make sure to add this |
| 277 | + new user to that list |
| 278 | + Note: Admin must be true in the environment |
| 279 | + """ |
| 280 | + random_password = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(32)) |
| 281 | + deploy_user = get_config('deploy_user') |
| 282 | + deploy_user_group = get_config('deploy_user_group') |
| 283 | + sudo('useradd -m -b /home -u 800 -g %s -s /bin/bash -c "deployment user" %s -p %s' % (deploy_user_group, deploy_user, random_password)) |
| 284 | + |
| 285 | + #create SSH directory |
| 286 | + sudo('mkdir -p /home/%s/.ssh/keys' % deploy_user) |
| 287 | + update_ssh_permissions() |
| 288 | + |
| 289 | + #TODO: automatically add ssh key or prompt |
| 290 | + |
| 291 | +def update_ssh_permissions(): |
| 292 | + """ |
| 293 | + This function makes the deploy user owner of these documents and locks down |
| 294 | + other permissions |
| 295 | + """ |
| 296 | + deploy_user = get_config('deploy_user') |
| 297 | + |
| 298 | + sudo('chmod 700 /home/%s/.ssh' % deploy_user) |
| 299 | + sudo('chmod 644 /home/%s/.ssh/authorized_keys' % deploy_user) |
| 300 | + sudo('chmod -R 700 /home/%s/.ssh/keys' % deploy_user) |
| 301 | + sudo('chown -R %s.%s /home/%s' % (deploy_user, get_config('deploy_user_group'), deploy_user)) |
| 302 | + |
| 303 | +@task(alias='ssh_string') |
| 304 | +def add_new_ssh_key_as_string(ssh_public_key_string, name): |
| 305 | + """ |
| 306 | + This function will add the passed ssh key string to the deploy user to enable |
| 307 | + passwordless login |
| 308 | + Note: Admin must be true in the environment |
| 309 | + TODO: validate string is valid ssh key |
| 310 | +
|
| 311 | + ssh_public_key_string: the actual public key string |
| 312 | + name: the name of the user this key is tied to |
| 313 | + """ |
| 314 | + |
| 315 | + ssh_key = ssh_public_key_string |
| 316 | + copy_ssh_key_to_host(ssh_key,name) |
| 317 | + rebuild_authorized_keys() |
| 318 | + update_ssh_permissions() |
| 319 | + |
| 320 | +@task(alias='ssh_file') |
| 321 | +def add_new_ssh_key_as_file(ssh_public_key_path, name): |
| 322 | + """ |
| 323 | + This function will copy the ssh key from a local file to the deploy user to enable |
| 324 | + passwordless login |
| 325 | + Note: Admin must be true in the environment |
| 326 | +
|
| 327 | + ssh_public_key_string: the actual public key full file path |
| 328 | + name: the name of the user this key is tied to |
| 329 | + """ |
| 330 | + ssh_key = open(ssh_public_key_path, 'r').read() |
| 331 | + copy_ssh_key_to_host(ssh_key, name) |
| 332 | + rebuild_authorized_keys() |
| 333 | + update_ssh_permissions() |
| 334 | + |
| 335 | +def copy_ssh_key_to_host(ssh_key, name): |
| 336 | + """ |
| 337 | + Creates a new pub file with the name provided and |
| 338 | + ssh key inside. Ships that pub file to the deploy |
| 339 | + users ssh directory |
| 340 | +
|
| 341 | + ssh_key: String of ssh_key to create a new pub file from |
| 342 | + name: the name of the user this key is tied to |
| 343 | + """ |
| 344 | + pub_file = open('%s.pub' % name, 'w') |
| 345 | + pub_file.write(ssh_key) |
| 346 | + pub_file.close() |
| 347 | + put('%s.pub' % name, '/home/%s/.ssh/keys/' % get_config('deploy_user'), use_sudo=True) |
| 348 | + |
| 349 | +def rebuild_authorized_keys(): |
| 350 | + """ |
| 351 | + Take all the current pub files and recreate authorized_keys from them. |
| 352 | + If any of the pub files are removed, they get removed from the authorized keys |
| 353 | + and can no longer ssh in. |
| 354 | + """ |
| 355 | + sudo('sudo cat `sudo find /home/%s/.ssh/keys/ -type f` > tmpfile' % get_config('deploy_user')) |
| 356 | + sudo('cp tmpfile /home/%s/.ssh/authorized_keys' % get_config('deploy_user')) |
| 357 | + sudo('rm tmpfile') |
0 commit comments