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
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)
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())
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
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))
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
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)