def _copy_from(self, copy_spec): from_role_name = copy_spec.get('role', None) if not from_role_name: return None # get the last known good build from the source machine. # note: we could alternatively get this from an instance tag. inst, role = ctx().get_host_in_role(from_role_name) with settings(host_string=inst.public_dns_name, user=role.user): message('Getting last good build-name from: "{0}"'.format(from_role_name)) src_build_name = BuildInfo().get_last_good() # copy it from the source machine. note that all machines must have been provisioned # properly to allow the current machine access to the source machine. tarball = self._tarball_name(src_build_name) path = ctx().build_path(tarball) copy_file_from(role.user, inst.private_dns_name, path, path) with cd(ctx().builds_root()): # untar it. command = 'tar -x --file={tarball}'.format(**locals()) result = run(command) if result.failed: raise HaltError('Failed to untar: "{0}"'.format(path)) # delete the tar. if copy_spec.get('delete_tar', True): run('rm {tarball}'.format(**locals())) # update the build information. BuildInfo().set_last_good(src_build_name) succeed_msg('Successfully copied build: "{0}"'.format(src_build_name)) return src_build_name
def _get_repos(self, repo_names): if repo_names == '__all__': return ctx().repos() if isinstance(repo_names, basestring): repo_names = [repo_names] return [ctx().get_repo(name) for name in repo_names]
def _create_dirs(self): # repos directory. result = sudo('mkdir -p -m 0777 {0}'.format(ctx().repos_root())) if result.failed: HaltError('Unable to create root directory for repo storage.') # builds directory. result = sudo('mkdir -p -m 0777 {0}'.format(ctx().builds_root())) if result.failed: HaltError('Unable to create root directory for builds.')
def start(self, spec, build_name, prog_name): """Starts a gunicorn server running. Writes a supervisor configuration for the program, starts the program, and optionally verifies the service is running by making an HTTP request. :param spec: a dictionary containing the gunicorn specification. may contain the keys: 'script': optional; the gunicorn command (e.g., 'gunicorn' or 'gunicorn_django') default: 'gunicorn' 'options': optional; gunicorn "long-name" options. these are the same as the option names preceded with double-dash, i.e., use "name" and not "n". "bind" cannot be set because it's set automatically. "name" and "workers" have intelligent defaults, but can be overridden. 'app_module': required; the python module:variable to run. 'http_test_path': optional; if specified, a HTTP HEAD request is made to the gunicorn server using this path. if a 200, 300 or 400 response is received, the server is considered to be running, otherwise it is considered not running and an exception will be raised. :param build_name: name of the build directory/virtualenv containing gunicorn. :param prog_name: name of the gunicorn program. :return: the port number on which the server is running. """ # create the command and write a new supervisor config for this build. # (this creates a [program:<prog_name>] config section for supervisor). cmd, port = self._get_cmd(spec, build_name, prog_name) log_root = path.join(ctx().build_path(build_name), 'logs') self._supervisor.write_config(prog_name, cmd, ctx().builds_root(), log_root) # start it and wait until supervisor thinks its up and running, then test the # service by sending it the specified HTTP request. self._supervisor.start(prog_name) http_path = spec.get('http_test_path', None) if not (self._supervisor.wait_until_running(prog_name) and self._http_test(http_path, port)): # cleanup and fail. self._supervisor.stop_and_remove(prog_name) raise HaltError('Failed to start local server at: "{0}"'.format(self._get_bind(port))) message('Successfully started local server.') return port, self._get_bind(port)
def _increment_name(self, ref_repo_name): # some projects have more than one repo. in this case one is designated as the "reference". # the reference repo gives it's most recent commit ID that's used in the new build name. # if no reference is given, just use the first (hopefully, the only) repo in the Context. if ref_repo_name: ref_repo = ctx().get_repo(ref_repo_name) else: ref_repo = ctx().repos()[0] name = BuildInfo.next(ref_repo.dir) succeed_msg('Created new build name: "{0}"'.format(name)) return name
def _tarball(self, build_name): tarball = self._tarball_name(build_name) dir_to_tar = ctx().build_path(build_name) with cd(ctx().builds_root()): options = '--create --gzip --format=ustar --owner=0 --group=0' command = 'tar {options} --file={tarball} {build_name}'.format(**locals()) result = run(command) if result.failed: raise HaltError('Failed to create tarball for: "{0}"'.format(dir_to_tar)) succeed_msg('Created build tarball: "{0}"'.format(tarball)) return self
def _execute_git_spec(self, git_spec): if git_spec.get('install_key_file', False): tool.git.install_key_file(ctx().get_key('git').local_file) clone_spec = git_spec.get('clone', []) if clone_spec == '__all__': repos = ctx().repos() else: if isinstance(clone_spec, basestring): clone_spec = [clone_spec] repos = [ctx().get_repo(name) for name in clone_spec] for repo in repos: tool.git.clone(repo.url, ctx().name, ctx().repos_root(), repo.dir)
def _get_cmd(self, spec, build_name, prog_name): """Builds the gunicorn command. See documentation of start() for parameter descriptions. :return: the gunicorn command as a string. """ cmd = spec.get('script', 'gunicorn') app = spec.get('app_module', None) cmd_path = path.join(ctx().build_path(build_name), 'bin') # allow overrides of some gunicorn options. port = unused_port() options = dict(spec.get('options', {})) options['bind'] = self._get_bind(port) if 'workers' not in options: options['workers'] = (2 * cpu_count()) + 1 if 'name' not in options: options['name'] = prog_name debug = options.pop('debug', False) options = ' '.join(['--{0} {1}'.format(k,v) for k,v in options.iteritems()]) if debug: options += ' --debug' return '{cmd_path}/{cmd} {options} {app}'.format(**locals()), port
def clone_all(): """ Clones all repos defined in the current context. :return: None """ for repo in ctx().repos(): clone(repo['url'], repo_name=repo['dir'])
def pull_all(): """ Performs a "pull" for all repos defined in the current context. :return: None """ for repo in ctx().repos(): pull(repo['dir'])
def copy_from(self, role_name, post_build=None, delete_tar=True): """Copies an existing build from an instance in the specified role. Instead of building itself, a build is copied from another instance to the current instance. :param role_name: the role of the instance to copy the build tarball from. :param post_build: list of post-build commands to execute. :param delete_tar: True to delete the tarball, False otherwise. :return: the name of the copied build. """ # get the last known good build from the source machine. # note: we could alternatively get this from an instance tag. message('Copying build from instance in role: "{0}"'.format(role_name)) inst, role = ctx().get_host_in_role(role_name) with settings(host_string=inst.public_dns_name, user=role.user): message('Getting last good build-name from: "{0}"'.format(role_name)) src_build_name = BuildInfo().get_last_good() # copy it from the source machine. note that all machines must have been provisioned # properly to allow the current machine access to the source machine. tarball = self._tarball_name(src_build_name) path = ctx().build_path(tarball) copy_file_from(role.user, inst.private_dns_name, path, path) with cd(ctx().builds_root()): # untar it. command = 'tar -x --file={tarball}'.format(**locals()) result = run(command) if result.failed: raise HaltError('Failed to untar: "{0}"'.format(path)) # delete the tar. if delete_tar: run('rm {tarball}'.format(**locals())) # update the build information. BuildInfo().set_last_good(src_build_name) # execute any post-build commands. if post_build: self._execute_post_build(post_build, src_build_name) succeed_msg('Successfully copied build: "{0}"'.format(src_build_name)) return src_build_name
def _execute_post_build(self, post_spec, build_name): message('Running post-build commands:') with prefix(virtualenv.activate_prefix(ctx().build_path(build_name))): for desc in post_spec: f = sudo if desc.get('sudo', False) else run result = f(desc['command']) if result.failed and not desc.get('ignore_fail', False): raise HaltError('Post-build command failed: "{0}"'.format(desc['command'])) message('Completed post-build commands.')
def _execute_access_spec(self, spec): access = spec.get('roles', []) if isinstance(access, basestring): access = [access] if access: public_key = get_public_key() for role_name in access: # check if access is allowed. target_role = ctx().get_role(role_name) if not target_role.allows_access_to(self._role.name): raise RuntimeError('Role "{0}" does not allow access to role "{1}"' .format(target_role.name, self._role.name)) # it is; put this host's public key in the target host's authorized_keys file. inst, role = ctx().get_host_in_role(role_name) with settings(host_string=inst.public_dns_name, user=role.user): authorize_key(public_key)
def clone(url, name=None, parent_dir=None, repo_name=''): start_msg('----- Cloning git repo: "{url}"'.format(**locals())) if not name: name = ctx().name message('Using context name: "{0}"'.format(name)) if not parent_dir: parent_dir = ctx().repos_root() message('Using parent directory: "{0}"'.format(parent_dir)) # make sure the parent directory exists. result = sudo('mkdir -p -m 0777 {0}'.format(parent_dir)) if result.failed: raise HaltError('Unable to create repo parent directory: {0}'.format(parent_dir)) with cd(parent_dir): result = run('git clone {url} {repo_name}'.format(**locals())) if result.return_code != 0: raise HaltError('Failed to clone repo: "{url}"'.format(**locals())) succeed_msg('Clone successful.')
def _increment_name(self, plan): # some projects have more than one repo. in this case one is designated as the "reference". # the reference repo gives it's most recent commit ID that's used in the new build name. ref_name = plan.get('reference_repo', None) if ref_name: ref_repo = ctx().get_repo(ref_name) else: ref_repo = self._get_repos(plan.get('repos', []))[0] name = BuildInfo.next(ref_repo.dir) succeed_msg('Created new build name: "{0}"'.format(name)) return name
def _get_nginx_static(self, options, build_name): static_spec = options.get('static', None) if not static_spec: return '' dir = site_packages_dir(ctx().build_path(build_name)) static = ''.join([ "location {0} {{\n" " alias {1};\n" "}}\n".format(d['url'], path.join(dir, d['local'])) for d in static_spec ]) return static
def install(self, roles): if isinstance(roles, basestring): roles = [roles] if not roles: raise HaltError('No roles specified for request_access.') public_key = self._key_pair.get_public_key() for role_name in roles: # check if access is allowed. target_role = ctx().get_role(role_name) if not target_role.allows_access_to(env.role_name): raise RuntimeError('Role "{0}" does not allow access to role "{1}"' .format(target_role.name, env.role_name)) # it is; put this host's public key in the target host's authorized_keys file. inst, role = ctx().get_host_in_role(role_name) with role.and_instance(inst): self._authorize_key(public_key) succeed_msg('Access granted to instances in role(s): {0}'.format(roles)) return self
def pull(repo_name=None, repo_dir=None): if not repo_dir: if not repo_name: raise HaltError('Either "repo_name" or "repo_dir" must be specified.') repo_dir = ctx().repo_path(repo_name) start_msg('Executing git pull in repo: "{0}"'.format(repo_dir)) with cd(repo_dir): result = run('git pull') if result.failed: raise HaltError('Error during "git pull" ({0})'.format(result)) succeed_msg('Pull successful ({0}).'.format(result))
def create_instance(self, image_id=None, key_name=None, instance_type=None, security_groups=None, **kwargs): # default to values specified in the role definition, but allow to be overridden. if image_id is None: image_id = self.aws.ami_id if key_name is None: key_name = self.aws.key_name if security_groups is None: security_groups = self.aws.security_groups if instance_type is None: instance_type = self.aws.instance_type # create the instance. conn = EC2Connection(ctx().aws_key, ctx().aws_secret) result = conn.run_instances(image_id, key_name=key_name, security_groups=security_groups, instance_type=instance_type, **kwargs) # wait until it's running. inst = result.instances[0] while inst.state != 'running': time.sleep(5) inst.update() return self.init_instance(inst)
def head_commit(repo_name=None, repo_dir=None): if not repo_dir: if not repo_name: raise HaltError('Either "repo_name" or "repo_dir" must be specified.') repo_dir = ctx().repo_path(repo_name) start_msg('Getting commit ID in git repo: "{0}":'.format(repo_dir)) with cd(repo_dir): # pipe the result through cat, otherwise the result that comes back from run() # is garbled and requires extensive weird parsing to extract the commit ID. result = run('git log -1 --pretty=format:%h | cat') if result.failed: raise HaltError('Error during "git log" ({0})'.format(result)) succeed_msg('Got head commit ID ({0}).'.format(result)) return result
def _build(self, plan): # increment the build name and create a new virtualenv for the build. build_name = self._increment_name(plan) build_env_dir = ctx().build_path(build_name) virtualenv.ensure(build_env_dir, plan.get('interpreter', None)) # run "setup.py install" in each repo. for repo in self._get_repos(plan.get('repos', [])): build_repo(build_env_dir, repo) # run tests. self._unittest(plan, build_name) # save the last known good build-name. BuildInfo.set_last_good(build_name) return build_name
def _start_gunicorn(self, spec, build_name): # create the command and write a new supervisor config for this build. # (this creates a [program:<build_name>] config section for supervisor). cmd, port = self._build_gunicorn_cmd(spec.get('gunicorn', {}), build_name) log_root = self._log_root(build_name) supervisord.write_program_config(build_name, cmd, ctx().builds_root(), log_root) # start it and wait until supervisor thinks its up and running, then test the # service by sending it the specified HTTP request. supervisord.start(build_name) if not supervisord.wait_until_running(build_name) or not self._http_test(spec, port): # cleanup and fail. supervisord.stop_and_remove(build_name) raise HaltError('Failed to start local server at: "{0}"'.format(self._gunicorn_bind(port))) message('Successfully started local server.') return port
def _build_gunicorn_cmd(self, spec, build_name): cmd = spec.get('script', 'gunicorn') app = spec.app_module cmd_path = path.join(ctx().build_path(build_name), 'bin') # allow overrides of some gunicorn options. port = unused_port() dct = dict(spec.get('options', {})) dct['bind'] = self._gunicorn_bind(port) if 'workers' not in dct: dct['workers'] = (2 * cpu_count()) + 1 if 'name' not in dct: dct['name'] = build_name debug = dct.pop('debug', False) options = ' '.join(['--{0} {1}'.format(k,v) for k,v in dct.iteritems()]) if debug: options += ' --debug' return '{cmd_path}/{cmd} {options} {app}'.format(**locals()), port
def build_repo(build_env_dir, repo): full_repo_dir = ctx().repo_path(repo.dir) dist_dir = path.join(full_repo_dir, 'dist') # with the build virtualenv activated, and within the repo directory. with prefix(VirtualEnvTool.activate_prefix(build_env_dir)), cd(full_repo_dir): start_msg('Running "python setup.py install" for repo "{0}"'.format(repo.dir)) # first create a source distribution using setup.py in this repo. result = run('python setup.py sdist --formats=gztar') if result.failed: raise HaltError('"python setup.py sdist" failed in repo: "{0}"'.format(repo.dir)) # now use pip to install. couple of things to note: # a) pip does a "flat" (not versioned) install, no eggs, and consistent package directory names. # b) we're still allowing pip to grab packages from pypi; this should be fixed in a later version # where packages can (optionally) be picked up only from a local directory. result = run('pip install --quiet --find-links=file://{dist_dir} {repo.package_name}'.format(**locals())) if result.failed: raise HaltError('"pip install" failed in repo: "{0}"'.format(repo.dir)) succeed_msg('Build successful.')
def init_instance(self, inst): inst.add_tag(cfg().fck_role, self.name) ctx().add_instance(inst) return inst
def _file_path(self): return ctx().repo_path(self.BUILD_INFO_FILE)
def __init__(self, context_name=None): self._context_name = ctx().name if not context_name else context_name self._dct = None
def _log_root(self, build_name): return path.join(ctx().build_path(build_name), 'logs')
def build(self, repos, reference_repo=None, post_build=None, interpreter=None, tarball=False, unittest=None): """Performs a 'python' build. Performs a python build by running setup.py in each identified repo. If desired, repos can be refreshed first (e.g., via git pull). :param repos: specifies the list of repos in which to run setup.py. :param reference_repo: optional; the reference repo from which to retrieve the head commit id. this id used as a component of the build name. if not specified, the first repo in the context is used. :param post_build: a list of post-build commands. a list of dictionaries. each dict must contain the key "command" that specifies the command to execute. optionally, it may include a "sudo" value of [True|False], and an "ignore_fail" value of [True|False]. :param interpreter: specifies the Python interpreter to use in the build's virtualenv. if not specified, the operating system default interpreter is used. note that the interpreter must already exist on the system. :param tarball: True to create a tarball of the build; this is required if any other instance will use "copy_from". :param unittest: TBD :return: the new build name """ start_msg('Executing build for instance in role "{0}":'.format(env.role_name)) # increment the build name and create a new virtualenv for the build. build_name = self._increment_name(reference_repo) build_env_dir = ctx().build_path(build_name) VirtualEnvTool().ensure(build_env_dir, interpreter) # run "setup.py install" in each repo. for repo_name in ([repos] if isinstance(repos, basestring) else repos): build_repo(build_env_dir, ctx().get_repo(repo_name)) # run tests. self._unittest(unittest, build_name) # save the last known good build-name. BuildInfo.set_last_good(build_name) if tarball: self._tarball(build_name) # execute any post-build commands. if post_build: self._execute_post_build(post_build, build_name) # make the build_name available to the caller; it'll be set as an instance-tag. succeed_msg('Build completed successfully for role "{0}".'.format(env.role_name)) env.role.set_env(build_result=build_name) return self