# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP from collections import defaultdict from glob import glob import os import time try: from xml.etree.ElementTree import ElementTree except ImportError: try: from elementtree import ElementTree except ImportError: pass from monasca_agent.collector.checks import AgentCheck from monasca_agent.common.util import get_hostname class Skip(Exception): """Raised by :class:`Jenkins` when it comes across a build or job that should be excluded from being checked. """ def __init__(self, reason, dir_name): message = 'skipping build or job at %s because %s' % (dir_name, reason) Exception.__init__(self, message) class Jenkins(AgentCheck): datetime_format = '%Y-%m-%d_%H-%M-%S' def __init__(self, name, init_config, agent_config): AgentCheck.__init__(self, name, init_config, agent_config) self.high_watermarks = {} def _extract_timestamp(self, dir_name): if not os.path.isdir(dir_name): raise Skip('its not a build directory', dir_name) try: # Parse the timestamp from the directory name date_str = os.path.basename(dir_name) time_tuple = time.strptime(date_str, self.datetime_format) return time.mktime(time_tuple) except ValueError: raise Exception("Error with build directory name, not a parsable date: %s" % (dir_name)) def _get_build_metadata(self, dir_name): if os.path.exists(os.path.join(dir_name, 'jenkins_build.tar.gz')): raise Skip('the build has already been archived', dir_name) # Read the build.xml metadata file that Jenkins generates build_metadata = os.path.join(dir_name, 'build.xml') if not os.access(build_metadata, os.R_OK): self.log.debug("Can't read build file at %s" % (build_metadata)) raise Exception("Can't access build.xml at %s" % (build_metadata)) else: tree = ElementTree() tree.parse(build_metadata) keys = ['result', 'number', 'duration'] kv_pairs = ((k, tree.find(k)) for k in keys) d = dict([(k, v.text) for k, v in kv_pairs if v is not None]) try: d['branch'] = tree.find('actions') \ .find('hudson.plugins.git.util.BuildData') \ .find('buildsByBranchName') \ .find('entry') \ .find('hudson.plugins.git.util.Build') \ .find('revision') \ .find('branches') \ .find('hudson.plugins.git.Branch') \ .find('name') \ .text except Exception: pass return d def _get_build_results(self, instance_key, job_dir): job_name = os.path.basename(job_dir) try: dirs = glob(os.path.join(job_dir, 'builds', '*_*')) if len(dirs) > 0: dirs = sorted(dirs, reverse=True) # We try to get the last valid build for index in xrange(0, len(dirs) - 1): dir_name = dirs[index] try: timestamp = self._extract_timestamp(dir_name) except Skip: continue # Check if it's a new build if timestamp > self.high_watermarks[instance_key][job_name]: # If we can't get build metadata, we try the previous one try: build_metadata = self._get_build_metadata(dir_name) except Exception: continue output = { 'job_name': job_name, 'timestamp': timestamp, 'event_type': 'build result' } output.update(build_metadata) self.high_watermarks[instance_key][job_name] = timestamp yield output # If it not a new build, stop here else: break except Exception as e: self.log.error("Error while working on job %s, exception: %s" % (job_name, e)) def check(self, instance, create_event=True): dimensions = self._set_dimensions(None, instance) if self.high_watermarks.get(instance.get('name'), None) is None: # On the first run of check(), prime the high_watermarks dict # so that we only send events that occurred after the agent # started. # (Setting high_watermarks in the next statement prevents # any kind of infinite loop (assuming nothing ever sets # high_watermarks to None again!)) self.high_watermarks[instance.get('name')] = defaultdict(lambda: 0) self.check(instance, create_event=False) jenkins_home = instance.get('jenkins_home', None) if not jenkins_home: raise Exception("No jenkins_home directory set in the config file") jenkins_jobs_dir = os.path.join(jenkins_home, 'jobs', '*') job_dirs = glob(jenkins_jobs_dir) if not job_dirs: raise Exception('No jobs found in `%s`! ' 'Check `jenkins_home` in your config' % (jenkins_jobs_dir)) for job_dir in job_dirs: for output in self._get_build_results(instance.get('name'), job_dir): output['host'] = get_hostname(self.agent_config) if create_event: self.log.debug("Creating event for job: %s" % output['job_name']) self.event(output) dimensions.update({'job_name': output['job_name']}) if 'branch' in output: dimensions.update({'branch': output['branch']}) self.gauge("jenkins.job.duration", float( output['duration']) / 1000.0, dimensions=dimensions) if output['result'] == 'SUCCESS': self.increment('jenkins.job.success', dimensions=dimensions) else: self.increment('jenkins.job.failure', dimensions=dimensions)