Example #1
0
class Repolist(Repotool):
    """ Return git repository list manually maintained in the settings file. """

    def __init__(self):
        current_dir = os.path.dirname(os.path.realpath(__file__))
        settings_file = current_dir + '/settings/default.yaml'
        self.settings = Settings()
        self.settings.add(settings_file)

    def git_repos(self):
        """Return a  list of repos from the Settings.

        The expected format is a dictionary whose keys are the folder names and
        values are the ssh connection strings to the repo.

        Example:
        {
        'drupal': 'http://git.drupal.org/project/drupal.git'
        }

        """
        repo_dict = self.settings.get('repoDict')
        if (not repo_dict) or (not isinstance(repo_dict, dict)):
            return {}
        else:
            return repo_dict
Example #2
0
class Jira(Pmtool):
    """ Plugin to wotk with JIRA. """

    def __init__(self):
        current_dir = os.path.dirname(os.path.realpath(__file__))
        settings_file = current_dir + "/settings/default.yaml"
        self.settings = Settings()
        self.settings.add(settings_file)
        base = self.settings.get("jiraBaseURL")
        api = self.settings.get("jiraAPIVersion")
        if not api.endswith("/"):
            api += "/"
        jira_api_base = urljoin(base, "rest/api/")
        self._jira_api_url = urljoin(jira_api_base, api)

    def submit_deploy_ticket(self, site, environments, description, target_date):
        """ Submit a deployment ticket to JIRA/Agile Ready. """
        issue_uri = self.settings.get("jiraIssueURL")
        issue_url = urljoin(self._jira_api_url, issue_uri)
        jira_user = self.settings.get("jiraUser")
        jira_pword = self.settings.get("jiraPword")
        message = {}
        for environment in environments:
            request = self.build_reqest(site, environment, description, target_date)
            headers = {"content-type": "application/json"}
            response = Utils.api_call(
                issue_url, "Jira", "post", data=request, auth=(jira_user, jira_pword), headers=headers
            )
            if not response == False:
                url = response["key"]
                message[environment] = "The {0} deploy ticket is {1}".format(environment, url)
            else:
                message[environment] = "JIRA ticket submission failed for {0}".format(environment)
        return message

    def build_reqest(self, site, environment, description, target_date):
        """ Build the JSON request to be submitted to JIRA. """
        summary = "{0} Deployment for {1} w.e. {2}".format(environment, site, target_date)
        request = {}
        request["fields"] = {}
        request["fields"]["project"] = {"key": self.settings.get("jiraProjectID")}
        request["fields"]["summary"] = summary
        request["fields"]["description"] = description
        request["fields"]["issuetype"] = {"name": self.settings.get("jiraIssueType")}
        return json.dumps(request)
Example #3
0
class Sendmail(Report):
    """ sendmail report plugin. """

    def __init__(self):
        current_dir = os.path.dirname(os.path.realpath(__file__))
        settings_file = current_dir + '/settings/default.yaml'
        self.settings = Settings()
        self.settings.add(settings_file)

    def send_message(self, report_text):
        """ Send the report via email using sendmail."""
        today = str(datetime.date.today())

        msg = MIMEText(report_text)
        msg["From"] = self.settings.get('reportSender')
        msg["To"] = self.settings.get('reportRecipient')
        msg["Subject"] = "Drupdates report {0}.".format(today)
        mail = Popen(["sendmail", "-t"], stdin=PIPE)
        mail.communicate(msg.as_string())
Example #4
0
class Stash(Repotool):
    """ Return git repository list from Stash project. """

    def __init__(self):
        current_dir = os.path.dirname(os.path.realpath(__file__))
        settings_file = current_dir + '/settings/default.yaml'
        self.settings = Settings()
        self.settings.add(settings_file)

    def git_repos(self):
        """ Get list of Stash repos from a specific Project.

            Note: this request will only bring back 9,999 repos, which should
            suffice, if it doesn't updaqte the stashLimit setting.
        """
        stash_url = self.settings.get('stashURL')
        git_repo_name = self.settings.get('gitRepoName')
        stash_user = self.settings.get('stashUser')
        stash_pword = self.settings.get('stashPword')
        stash_cert_verify = self.settings.get('stashCertVerify')
        stash_limit = self.settings.get('stashLimit')
        stash_params = {'limit' : stash_limit}
        response = Utils.api_call(stash_url, git_repo_name, 'get',
                                  auth=(stash_user, stash_pword),
                                  verify=stash_cert_verify,
                                  params=stash_params)
        if not response == False:
            repos = Stash.parse_repos(response['values'])
            return repos
        else:
            return {}

    @staticmethod
    def parse_repos(raw):
        """ Parse repo list returned form Stash."""
        repos = {}
        for repo in raw:
            for link in repo['links']['clone']:
                if link['name'] == 'ssh':
                    repos[repo['slug']] = link['href']
        return repos
Example #5
0
class Slack(Report):
    """ Slack report plugin. """

    def __init__(self):
        current_dir = os.path.dirname(os.path.realpath(__file__))
        settings_file = current_dir + '/settings/default.yaml'
        self.settings = Settings()
        self.settings.add(settings_file)

    def send_message(self, report_text):
        """ Post the report to a Slack channel or DM a specific user."""
        url = self.settings.get('slackURL')
        user = self.settings.get('slackUser')
        payload = {}
        payload['text'] = report_text
        payload['new-bot-name'] = user
        direct = self.settings.get('slackRecipient')
        channel = self.settings.get('slackChannel')
        if direct:
            payload['channel'] = '@' + direct
        elif channel:
            payload['channel'] = '#' + direct
        Utils.api_call(url, 'Slack', 'post', data=json.dumps(payload))
Example #6
0
class Attask(Pmtool):
    """ Plugin to wotk with AtTask. """

    def __init__(self):
        current_dir = os.path.dirname(os.path.realpath(__file__))
        settings_file = current_dir + '/settings/default.yaml'
        self.settings = Settings()
        self.settings.add(settings_file)
        self._pm_label = self.settings.get('pmName')
        base = self.settings.get('attaskBaseURL')
        api = self.settings.get('attaskAPIVersion')
        api_base = urljoin(base, 'attask/api/')
        self._attask_api_url = urljoin(api_base, api)

    def get_session_id(self):
        """ Get a session ID from AtTask. """
        attask_pword = self.settings.get('attaskPword')
        attask_user = self.settings.get('attaskUser')
        at_params = {'username': attask_user, 'password': attask_pword}
        login_url = urljoin(self._attask_api_url, self.settings.get('attaskLoginUrl'))
        response = Utils.api_call(login_url, self._pm_label, 'post', params=at_params)
        if response == False:
            return False
        else:
            return response['data']['sessionID']

    def submit_deploy_ticket(self, site, environments, description, target_date):
        """ Submit a Deployment request to AtTask.

        site -- The site the ticket is for
        environments -- The name(s) of the environments to deploy to
        description -- The description text to go in the task
        targetDate -- The date to put in the label fo the ticket

        """
        sessparam = {}
        session_id = self.get_session_id()
        # Make sure you can get a Session ID
        if session_id:
            sessparam['sessionID'] = session_id
        else:
            return False
        # Set-up AtTask request
        attask_project_id = self.settings.get('attaskProjectID')
        dev_ops_team_id = self.settings.get('devOpsTeamID')
        attask_base_url = self.settings.get('attaskBaseURL')
        attask_assignee_type = self.settings.get('attaskAssigneeType')
        task_url = urljoin(self._attask_api_url, self.settings.get('attaskTaskURL'))
        message = {}
        for environment in environments:
            title = environment + ' Deployment for ' + site +' w.e. ' + target_date
            at_params = {'name': title, 'projectID': attask_project_id,
                         attask_assignee_type: dev_ops_team_id, 'description': description}
            response = Utils.api_call(task_url, self._pm_label, 'post',
                                      params=at_params, headers=sessparam)
            if not response == False:
                data = response['data']
                msg = "The {0} deploy ticket is".format(environment)
                msg += " <{0}task/view?ID={1}>".format(attask_base_url, data['ID'])
                message[environment] = msg
            else:
                msg = "The {0} deploy ticket did not submit to".format(environment)
                msg += "{0} properly".format(self._pm_label)
                message[environment] = msg
        return message
Example #7
0
class Utils(object):
    """ Class of utilities used throughout the module. """

    def __init__(self):
        self.settings = Settings()

    @staticmethod
    def detect_home_dir(directory):
        """ If dir is relative to home dir rewrite as OS agnostic path. """
        parts = directory.split('/')
        if parts[0] == '~' or parts[0].upper() == '$HOME':
            del parts[0]
            directory = os.path.join(expanduser('~'), '/'.join(parts))
        return directory

    @staticmethod
    def check_dir(directory):
        """ Ensure the directory is writable. """
        directory = Utils.detect_home_dir(directory)
        if not os.path.isdir(directory):
            try:
                os.makedirs(directory)
            except OSError as error:
                msg = 'Unable to create non-existant directory {0} \n'.format(directory)
                msg += 'Error: {0}\n'.format(error.strerror)
                msg += 'Moving to next working directory, if applicable'
                raise DrupdatesError(20, msg)
        filepath = os.path.join(directory, "text.txt")
        try:
            open(filepath, "w")
        except IOError:
            msg = 'Unable to write to directory {0} \n'.format(directory)
            raise DrupdatesError(20, msg)
        os.remove(filepath)
        return directory

    @staticmethod
    def remove_dir(directory):
        """ Try and remove the directory. """
        if os.path.isdir(directory):
            try:
                shutil.rmtree(directory)
            except OSError as error:
                msg = "Can't remove site dir {0}\n Error: {1}".format(directory, error.strerror)
                raise DrupdatesError(20, msg)
        return True

    def find_make_file(self, site_name, directory):
        """ Find the make file and test to ensure it exists. """
        make_format = self.settings.get('makeFormat')
        make_folder = self.settings.get('makeFolder')
        file_name = self.settings.get('makeFileName')
        make_file = site_name + '.make'
        if file_name:
            make_file_short = file_name
        else:
            make_file_short = site_name
        if make_format == 'yaml':
            make_file += '.yaml'
            make_file_short += '.yaml'
        if make_folder:
            directory = os.path.join(directory, make_folder)
        file_name = os.path.join(directory, make_file)
        file_name_short = os.path.join(directory, make_file_short)
        if os.path.isfile(file_name):
            return file_name
        if os.path.isfile(file_name_short):
            return file_name_short
        return False

    def make_site(self, site_name, site_dir):
        """ Build a webroot based on a make file. """
        web_root = self.settings.get('webrootDir')
        folder = os.path.join(site_dir, web_root)
        make_file = self.find_make_file(site_name, site_dir)
        Utils.remove_dir(folder)
        if make_file and web_root:
            # Run drush make
            # Get the repo webroot
            make_opts = self.settings.get('makeOpts')
            make_cmds = ['make', make_file, folder]
            make_cmds += make_opts
            make = Drush.call(make_cmds)
            return make

    @staticmethod
    def api_call(uri, name, method='get', **kwargs):
        """ Perform and API call, expecting a JSON response.

        Largely a wrapper around the request module

        Keyword arguments:
        uri -- the uri of the Restful Web Service (required)
        name -- the human readable label for the service being called (required)
        method -- HTTP method to use (default = 'get')
        kwargs -- dictionary of arguments passed directly to requests module method

        """
        # Ensure uri is valid
        if not bool(urlparse(uri).netloc):
            msg = ("Error: {0} is not a valid url").format(uri)
            raise DrupdatesAPIError(20, msg)
        func = getattr(requests, method)
        args = {}
        args['timeout'] = (10, 10)
        for key, value in kwargs.items():
            args[key] = value
        try:
            response = func(uri, **args)
        except requests.exceptions.Timeout:
            msg = "The api call to {0} timed out".format(uri)
            raise DrupdatesAPIError(20, msg)
        except requests.exceptions.TooManyRedirects:
            msg = "The api call to {0} appears incorrect, returned: too many re-directs".format(uri)
            raise DrupdatesAPIError(20, msg)
        except requests.exceptions.RequestException as error:
            msg = "The api call to {0} failed\n Error {1}".format(uri, error)
            raise DrupdatesAPIError(20, msg)
        try:
            response_dictionary = response.json()
        except ValueError:
            return response
        #If API call errors out print the error and quit the script
        if response.status_code not in [200, 201]:
            if 'errors' in response_dictionary:
                errors = response_dictionary.pop('errors')
                first_error = errors.pop()
            elif 'error' in response_dictionary:
                first_error = response_dictionary.pop('error')
            else:
                first_error['message'] = "No error message provided by response"
            msg = "{0} returned an error, exiting the script.\n".format(name)
            msg += "Status Code: {0} \n".format(response.status_code)
            msg += "Error: {0}".format(first_error['message'])
            raise DrupdatesAPIError(20, msg)
        else:
            return response_dictionary

    def sys_commands(self, obj, phase=''):
        """ Run a system command based on the subprocess.popen method.

        For example: maybe you want a symbolic link, on a unix box,
        from /opt/drupal to /var/www/drupal you would add the command(s)
        to the appropriate phase setting in you yaml settings files.

        Note: the format of the setting is a multi-dimensional list

        Example (from Sitebuild.build():
          postBuildCmds:
            value:
              -
                - ln
                - -s
                - /var/www/drupal
                - /opt/drupal

        Note: You can refer to an attribute in the calling class, assuming they are
        set, by prefixing them with "att_" in the settings yaml above,
        ex. att_site_dir would pass the Sitebuild.site_dir attribute

        Keyword arguments:
        phase -- the phase the script is at when sysCommands is called (default "")
        object -- the object the call to sysCommand is housed within
        """
        commands = self.settings.get(phase)
        if commands and isinstance(commands, list):
            for command in commands:
                if isinstance(command, list):
                    # Find list items that match the string after "att_",
                    # these are names names of attribute in the calling class
                    for key, item in enumerate(command):
                        if item[:4] == 'att_':
                            attribute = item[4:]
                            try:
                                command[key] = getattr(obj, attribute)
                            except AttributeError:
                                continue
                    try:
                        popen = subprocess.Popen(command, stdout=subprocess.PIPE,
                                                 stderr=subprocess.PIPE)
                    except OSError as error:
                        msg = "Cannot run {0} the command doesn't exist,\n".format(command.pop(0))
                        msg += "Error: {1}".format(error.strerror)
                        print(msg)
                    results = popen.communicate()
                    if results[1]:
                        print("Running {0}, \n Error: {1}".format(command, results[1]))
                else:
                    continue

    def rm_common(self, dir_delete, dir_compare):
        """ Delete files in dir_delete that are in dir_compare.

        Iterate over the sites directory and delete any files/folders not in the
        commonIgnore setting.

        keyword arguments:
        dir_delete -- The directory to have it's file/folders deleted.
        dir_compare -- The directory to compare dirDelete with.
        """
        ignore = self.settings.get('commonIgnore')
        if isinstance(ignore, str):
            ignore = [ignore]
        dcmp = dircmp(dir_delete, dir_compare, ignore)
        for file_name in dcmp.common_files:
            os.remove(dir_delete + '/' + file_name)
        for directory in dcmp.common_dirs:
            shutil.rmtree(dir_delete + '/' + directory)

    def write_debug_file(self):
        """ Write debug file for this run.

        Write file containing your system settings to be used to record python
        and Drupdates state at the time Drupdates was run.
        """

        base_dir = self.settings.get('baseDir')
        directory = Utils.check_dir(base_dir)
        debug_file_name = os.path.join(directory, 'drupdates.debug')
        debug_file = open(debug_file_name, 'w')
        debug_file.write("Python Version:\n")
        python_version = "{0}\n\n".format(sys.version)
        debug_file.write(python_version)
        # Get version data for system dependancies
        dependancies = ['sqlite3', 'drush', 'git', 'php']
        for dependancy in dependancies:
            commands = [dependancy, '--version']
            popen = subprocess.Popen(commands,
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE)
            results = popen.communicate()
            if popen.returncode != 0:
                stdout = "Check returned error."
            else:
                stdout = results[0]
            debug_file.write("{0} Version:\n".format(dependancy.title()))
            debug_file.write("{0}\n".format(stdout.decode()))
        installed_packages = pip.get_installed_distributions()
        if len(installed_packages):
            debug_file.write("Installed Packages:\n\n")
            for i in installed_packages:
                package = "{0}\n".format(str(i))
                debug_file.write(package)
        settings = self.settings.list()
        debug_file.write("\nDrupdates Settings:\n\n")
        for name, setting in settings.items():
            line = "{0} : {1}\n".format(name, str(setting['value']))
            debug_file.write(line)

    def load_dir_settings(self, dir):
        """ Add custom settings for the a given directory. """
        settings_file = os.path.join(dir, '.drupdates/settings.yaml')
        if os.path.isfile(settings_file):
            self.settings.add(settings_file, True)

    @staticmethod
    def copytree(src, dst, symlinks = False, ignore = None):
        """ Recursively copy a directory tree from src to dst.

        Taken from http://stackoverflow.com/a/22331852/1120125.

        Needed because distutils.dir_util.copy_tree will only copy a given
        directory one time.  Which is annoying!

        """
        if not os.path.exists(dst):
            os.makedirs(dst)
            shutil.copystat(src, dst)
        lst = os.listdir(src)
        if ignore:
            excl = ignore(src, lst)
            lst = [x for x in lst if x not in excl]
        for item in lst:
            s = os.path.join(src, item)
            d = os.path.join(dst, item)
            if symlinks and os.path.islink(s):
                if os.path.lexists(d):
                    os.remove(d)
                os.symlink(os.readlink(s), d)
                try:
                    st = os.lstat(s)
                    mode = stat.S_IMODE(st.st_mode)
                    os.lchmod(d, mode)
                except:
                    pass # lchmod not available
            elif os.path.isdir(s):
                Utils.copytree(s, d, symlinks, ignore)
            else:
                shutil.copy2(s, d)