示例#1
0
class Sitebuild(object):
    """ Build out the repository folder. """

    def __init__(self, site_name, ssh, working_dir):
        self.settings = Settings()
        self._site_name = site_name
        self.site_dir = os.path.join(working_dir, self._site_name)
        self.ssh = ssh
        self.utilities = Utils()
        self.si_files = copy.copy(self.settings.get('drushSiFiles'))

    def build(self):
        """ Core build method. """
        working_branch = self.settings.get('workingBranch')
        try:
            Utils.remove_dir(self.site_dir)
        except DrupdatesError as remove_error:
            raise DrupdatesBuildError(20, remove_error.msg)
        self.utilities.sys_commands(self, 'preBuildCmds')
        repository = Repo.init(self.site_dir)
        remote = git.Remote.create(repository, self._site_name, self.ssh)
        try:
            remote.fetch(working_branch, depth=1)
        except git.exc.GitCommandError as error:
            msg = "{0}: Could not checkout {1}. \n".format(self._site_name, working_branch)
            msg += "Error: {0}".format(error)
            raise DrupdatesBuildError(20, msg)
        git_repo = repository.git
        git_repo.checkout('FETCH_HEAD', b=working_branch)
        self.utilities.load_dir_settings(self.site_dir)
        self.standup_site()
        try:
            repo_status = Drush.call(['st'], self._site_name, True)
        except DrupdatesError as st_error:
            raise DrupdatesBuildError(20, st_error.msg)
        finally:
            self.file_cleanup()
        if not 'bootstrap' in repo_status:
            msg = "{0} failed to Stand-up properly after running drush qd".format(self._site_name)
            raise DrupdatesBuildError(20, msg)
        self.utilities.sys_commands(self, 'postBuildCmds')
        return "Site build for {0} successful".format(self._site_name)

    def standup_site(self):
        """ Using the drush core-quick-drupal (qd) command stand-up a Drupal site.

        This will:
        - Perform site install with sqlite.
        - If needed, build webroot from a make file.
        - Install any sub sites (ie multi-sites)
        - Ensure that all the files in the web root are writable.

        """
        qd_settings = self.settings.get('qdCmds')
        qd_cmds = copy.copy(qd_settings)
        backup_dir = Utils.check_dir(self.settings.get('backupDir'))
        qd_cmds += ['--backup-dir=' + backup_dir]
        try:
            qd_cmds.remove('--no-backup')
        except ValueError:
            pass
        if self.settings.get('useMakeFile'):
            make_file = self.utilities.find_make_file(self._site_name, self.site_dir)
            if make_file:
                qd_cmds += ['--makefile=' + make_file]
            else:
                msg = "Can't find make file in {0} for {1}".format(self.site_dir, self._site_name)
                raise DrupdatesBuildError(20, msg)
            if self.settings.get('buildSource') == 'make':
                qd_cmds.remove('--use-existing')
        try:
            Drush.call(qd_cmds, self._site_name)
            sub_sites = Drush.get_sub_site_aliases(self._site_name)
            for alias, data in sub_sites.items():
                Drush.call(qd_cmds, alias)
                # Add sub site settings.php to list of file_cleanup() files.
                sub_site_st = Drush.call(['st'], alias, True)
                self.si_files.append(sub_site_st['site'] + '/settings.php')
                self.si_files.append(sub_site_st['files'] + '/.htaccess')
                self.si_files.append(sub_site_st['site'])
        except DrupdatesError as standup_error:
            raise standup_error

    def file_cleanup(self):
        """ Drush sets the folder permissions for some file to be 0444, convert to 0777. """
        drush_dd = Drush.call(['dd', '@drupdates.' + self._site_name])
        site_webroot = drush_dd[0]
        for name in self.si_files:
            complete_name = os.path.join(site_webroot, name)
            if os.path.isfile(complete_name) or os.path.isdir(complete_name):
                try:
                    os.chmod(complete_name, 0o777)
                except OSError:
                    msg = "Couldn't change file permission for {0}".format(complete_name)
                    raise DrupdatesBuildError(20, msg)
示例#2
0
class Siteupdate(object):
    """ Update the modules and/or core in a completely built Drupal site. """

    def __init__(self, site_name, ssh, working_dir):
        self.settings = Settings()
        self.working_branch = self.settings.get('workingBranch')
        self._site_name = site_name
        self.working_dir = working_dir
        self.site_dir = os.path.join(working_dir, self._site_name)
        self.ssh = ssh
        self.utilities = Utils()
        self.site_web_root = None
        self._commit_hash = None
        self.repo_status = None
        self.sub_sites = Drush.get_sub_site_aliases(self._site_name)

    @property
    def commit_hash(self):
        """ commit_hash getter. """
        return self._commit_hash

    @commit_hash.setter
    def commit_hash(self, value):
        """ commit_hash setter. """
        self._commit_hash = value

    def update(self):
        """ Set-up to and run Drush update(s) (i.e. up or ups). """
        report = {}
        self.utilities.sys_commands(self, 'preUpdateCmds')
        self.repo_status = Drush.call(['st'], self._site_name, True)
        try:
            updates = self.run_updates()
        except DrupdatesError as updates_error:
            raise DrupdatesUpdateError(20, updates_error.msg)
        # If no updates move to the next repo
        if not updates:
            self.commit_hash = ""
            report['status'] = "Did not have any updates to apply"
            return report
        report['status'] = "The following updates were applied"
        report['updates'] = updates
        report['commit'] = "The commit hash is {0}".format(self.commit_hash)
        self.utilities.sys_commands(self, 'postUpdateCmds')
        if self.settings.get('submitDeployTicket') and self.commit_hash:
            report[self._site_name] = {}
            pm_name = self.settings.get('pmName').title()
            try:
                report[self._site_name][pm_name] = Pmtools().deploy_ticket(self._site_name,
                                                                           self.commit_hash)
            except DrupdatesError as api_error:
                report[self._site_name][pm_name] = api_error.msg
        return report

    def run_updates(self):
        """ Run the site updates.

        The updates are done either by downloading the updates, updating the
        make file or both.

        - First, run drush pm-updatestatus to get a list of eligible updates for
        the site/sub-sites.
        - Second, build the report to return to Updates().
        - Third, apply the updates.

        """
        updates = {}
        try:
            sites = self.get_sites_to_update()
        except DrupdatesError as update_status_error:
            raise DrupdatesUpdateError(20, update_status_error)
        if not sites['count']:
            return updates
        else:
            sites.pop('count')
        # Note: call Drush.call() without site alias as alias comes after dd argument.
        drush_dd = Drush.call(['dd', '@drupdates.' + self._site_name])
        self.site_web_root = drush_dd[0]
        # Create seperate commits for each project (ie module/theme)
        one_commit_per_project = self.settings.get('oneCommitPerProject')
        # Iterate through the site/sub-sites and perform updates, update files etc...
        sites_copy = copy.copy(sites)
        for site, data in sites.items():
            if 'modules' not in data:
                sites_copy.pop(site)
                continue
            modules = copy.copy(data['modules'])
            x = 0
            for project, descriptions in data['modules'].items():
                if self.settings.get('useMakeFile'):
                    self.update_make_file(project, descriptions['current'], descriptions['candidate'])
                if one_commit_per_project:
                    if x:
                        build = Sitebuild(self._site_name, self.ssh, self.working_dir)
                        build.build()
                    self._update_code(site, [project])
                    modules.pop(project)
                    updates = self._build_commit_message(sites_copy, site, project)
                    self._cleanup_and_commit(updates)
                x += 1
            if self.settings.get('buildSource') == 'make' and self.settings.get('useMakeFile'):
                self.utilities.make_site(self._site_name, self.site_dir)
            elif len(modules):
                self._update_code(site, modules.keys())
        if not one_commit_per_project:
            updates = self._build_commit_message(sites_copy)
            self._cleanup_and_commit(updates)
        return updates

    def get_sites_to_update(self):
        """ Build dictionary of sites/sub-sites and modules needing updated. """
        ups_cmds = self.settings.get('upsCmds')
        updates_ret = {}
        count = 0
        sites = {}
        sites[self._site_name] = {}
        for alias, data in self.sub_sites.items():
            sites[alias] = {}
        for site in sites:
            try:
                updates_ret = Drush.call(ups_cmds, site, True)
            except DrupdatesError as updates_error:
                parse_error = updates_error.msg.split('\n')
                if parse_error[2][0:14] == "Drush message:":
                    # If there are not updates to apply.
                    continue
                else:
                    raise updates_error
            else:
                # Parse the results of drush pm-updatestatus
                count += len(updates_ret)
                modules = {}
                for module, update in updates_ret.items():
                    modules[module] = {}
                    api = update['api_version']
                    modules[module]['current'] = update['existing_version'].replace(api + '-', '')
                    modules[module]['candidate'] = update['candidate_version'].replace(api + '-', '')
                    msg = "Update {0} from {1} to {2}"
                    modules[module]['report_txt'] = msg.format(module.title(),
                                                               modules[module]['current'],
                                                               modules[module]['candidate'])
                    sites[site]['modules'] = modules
        sites['count'] = count
        return sites

    def update_make_file(self, module, current, candidate):
        """ Update the make file.

        Keyword arguments:
        module -- the drupal module or core (required)
        current -- the current version
        candidate -- the version to update two

        """
        make_file = self.utilities.find_make_file(self._site_name, self.site_dir)
        make_format = self.settings.get('makeFormat')
        if make_format == 'make':
            openfile = open(make_file)
            makef = openfile.read()
            openfile.close()
            current_str = 'projects[{0}][version] = \"{1}\"'.format(module, current)
            candidate_str = 'projects[{0}][version] = \"{1}\"'.format(module, candidate)
            newdata = makef.replace(current_str, candidate_str)
            openfile = open(make_file, 'w')
            openfile.write(newdata)
            openfile.close()
        elif make_format == 'yaml':
            make = open(make_file)
            makef = yaml.load(make)
            make.close()
            makef['projects'][module]['version'] = str(candidate)
            openfile = open(make_file, 'w')
            yaml.dump(makef, openfile, default_flow_style=False)

    def _update_code(self, site, modules):
        """ Run drush make or pm-update to make te actual code updates.

        Keyword arguments:
        site -- site alias of the site to update.
        modules -- list containing modules to update.

        """
        up_cmds = copy.copy(self.settings.get('upCmds'))
        up_cmds += modules
        try:
            Drush.call(up_cmds, site)
        except DrupdatesError as updates_error:
            raise updates_error

    def _build_commit_message(self, sites, site = '', module = ''):
        """ Build a commit message for one project update or multiple.

        Keyword arguments:
        sites -- dictionary containing meta data about update for each site.
        site -- if only one site needs updated.
        module -- if only one module needs updated.

        """
        msg = {}
        if module and site:
            msg[site] = [sites[site]['modules'][module]['report_txt']]
        else:
            for site, data in sites.items():
                msg[site] = []
                for module, status in data['modules'].items():
                    msg[site].append(status['report_txt'])
        return msg

    def _cleanup_and_commit(self, updates):
        """ Clean-up webroot and commit changes.

        Keyword arguments:
        updates -- list of update message to put in commit message.

        """
        self._clean_up_web_root()
        self._git_apply_changes(updates)

    def _git_apply_changes(self, updates):
        """ add/remove changed files.

        Keyword arguments:
        updates -- list of update message to put in commit message.

        notes:
        - Will ignore file mode changes and anything in the commonIgnore setting.

        """
        os.chdir(self.site_dir)
        repo = Repo(self.site_dir)
        for ignore_file in self.settings.get('commonIgnore'):
            try:
                repo.git.checkout(os.path.join(self.site_web_root, ignore_file))
            except git.exc.GitCommandError:
                pass
        if self.repo_status['modules'] and self.settings.get('ignoreCustomModules'):
            custom_module_dir = os.path.join(self.site_web_root,
                                             self.repo_status['modules'], 'custom')
            try:
                repo.git.checkout(custom_module_dir)
            except git.exc.GitCommandError:
                pass
        # Instruct Git to ignore file mode changes.
        cwriter = repo.config_writer('global')
        cwriter.set_value('core', 'fileMode', 'false')
        cwriter.release()
        # Add new/changed files to Git's index
        try:
            repo.git.add('--all')
        except git.exc.GitCommandError as git_add_error:
            raise DrupdatesUpdateError(20, git_add_error)
        # Remove deleted files from Git's index.
        deleted = repo.git.ls_files('--deleted')
        for filepath in deleted.split():
            repo.git.rm(filepath)
        # Commit all the changes.
        if self.settings.get('useFeatureBranch'):
            if self.settings.get('featureBranchName'):
                branch_name = self.settings.get('featureBranchName')
            else:
                ts = time.time()
                stamp = datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S')
                branch_name = "drupdates-{0}".format(stamp)
            repo.git.checkout(self.working_branch, b=branch_name)
        else:
            branch_name  = self.settings.get('workingBranch')
            repo.git.checkout(self.working_branch)
        msg = ''
        for site, update in updates.items():
            msg += "\n{0} \n {1}".format(site, '\n'.join(update))
        commit_author = Actor(self.settings.get('commitAuthorName'), self.settings.get('commitAuthorEmail'))
        repo.index.commit(message=msg, author=commit_author)
        # Save the commit hash for the Drupdates report to use.
        heads = repo.heads
        branch = heads[branch_name]
        self.commit_hash = branch.commit
        # Push the changes to the origin repo.
        repo.git.push(self._site_name, branch_name)

    def _clean_up_web_root(self):
        """ Clean-up artifacts from drush pm-update/core-quick-drupal. """
        use_make_file = self.settings.get('useMakeFile')
        if self.settings.get('buildSource') == 'make' and use_make_file:
            # Remove web root folder if repo only ships a make file.
            shutil.rmtree(self.site_web_root)
        else:
            rebuilt = self._rebuild_web_root()
            if not rebuilt:
                report['status'] = "The webroot re-build failed."
                if use_make_file:
                    make_err = " Ensure the make file format is correct "
                    make_err += "and Drush make didn't fail on a bad patch."
                    report['status'] += make_err
                return report
        # Remove <webroot>/drush folder
        drush_path = os.path.join(self.site_web_root, 'drush')
        if os.path.isdir(drush_path):
            self.utilities.remove_dir(drush_path)
        try:
            # Remove all SQLite files
            os.remove(self.repo_status['db-name'])
            for alias, data in self.sub_sites.items():
                db_file = data['databases']['default']['default']['database']
                if os.path.isfile(db_file):
                    os.remove(db_file)
        except OSError:
            pass

    def _rebuild_web_root(self):
        """ Rebuild the web root folder completely after running pm-update.

        Drush pm-update of Drupal Core deletes the .git folder therefore need to
        move the updated folder to a temp dir and re-build the webroot folder.
        """
        temp_dir = tempfile.mkdtemp(self._site_name)
        shutil.move(self.site_web_root, temp_dir)
        add_dir = self.settings.get('webrootDir')
        if add_dir:
            repo = Repo(self.site_dir)
            repo.git.checkout(add_dir)
        else:
            repo = Repo.init(self.site_dir)
            try:
                remote = git.Remote.create(repo, self._site_name, self.ssh)
            except git.exc.GitCommandError as error:
                if not error.status == 128:
                    msg = "Could not establish a remote for the {0} repo".format(self._site_name)
                    print(msg)
            remote.fetch(self.working_branch)
            try:
                repo.git.checkout('FETCH_HEAD', b=self.working_branch)
            except git.exc.GitCommandError as error:
                repo.git.checkout(self.working_branch)
            add_dir = self._site_name
        if 'modules' in self.repo_status:
            module_dir = self.repo_status['modules']
            shutil.rmtree(os.path.join(self.site_web_root, module_dir))
        if 'themes' in self.repo_status:
            theme_dir = self.repo_status['themes']
            shutil.rmtree(os.path.join(self.site_web_root, theme_dir))
        self.utilities.rm_common(self.site_web_root, os.path.join(temp_dir, add_dir))
        try:
            Utils.copytree(os.path.join(temp_dir, add_dir),
                                         self.site_web_root,
                                         symlinks=True)
        except OSError as copy_error:
            raise DrupdatesUpdateError(20, copy_error)
        except IOError as error:
            msg = "Can't copy updates from: \n"
            msg += "{0} temp dir to {1}\n".format(temp_dir, self.site_web_root)
            msg += "Error: {0}".format(error.strerror)
            raise DrupdatesUpdateError(20, msg)
        shutil.rmtree(temp_dir)
        return True