Jakub Josef | e380798 | 2016-12-15 11:54:51 +0100 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | |
| 3 | # Import Python libs |
| 4 | from __future__ import absolute_import |
| 5 | import difflib |
| 6 | import logging |
| 7 | |
| 8 | # Import Salt libs |
| 9 | import salt.ext.six as six |
| 10 | import salt.utils |
| 11 | |
| 12 | # Import XML parser |
| 13 | import xml.etree.ElementTree as ET |
chnyda | 90f133f | 2017-08-02 10:46:13 +0200 | [diff] [blame] | 14 | import hashlib |
| 15 | |
| 16 | # Jenkins |
| 17 | try: |
| 18 | import jenkins |
| 19 | HAS_JENKINS = True |
| 20 | except ImportError: |
| 21 | HAS_JENKINS = False |
Jakub Josef | e380798 | 2016-12-15 11:54:51 +0100 | [diff] [blame] | 22 | |
| 23 | log = logging.getLogger(__name__) |
| 24 | |
| 25 | |
Ilya Kharin | 3d8bffe | 2017-06-22 17:40:31 +0400 | [diff] [blame] | 26 | def __virtual__(): |
| 27 | ''' |
| 28 | Only load if jenkins_common module exist. |
| 29 | ''' |
chnyda | 90f133f | 2017-08-02 10:46:13 +0200 | [diff] [blame] | 30 | if HAS_JENKINS and 'jenkins_common.call_groovy_script' not in __salt__: |
Ilya Kharin | 3d8bffe | 2017-06-22 17:40:31 +0400 | [diff] [blame] | 31 | return ( |
| 32 | False, |
| 33 | 'The jenkins_job state module cannot be loaded: ' |
| 34 | 'jenkins_common not found') |
| 35 | return True |
| 36 | |
| 37 | |
Jakub Josef | e380798 | 2016-12-15 11:54:51 +0100 | [diff] [blame] | 38 | def _elements_equal(e1, e2): |
chnyda | 90f133f | 2017-08-02 10:46:13 +0200 | [diff] [blame] | 39 | return hashlib.md5(e1).hexdigest() == hashlib.md5(e2).hexdigest() |
Jakub Josef | e380798 | 2016-12-15 11:54:51 +0100 | [diff] [blame] | 40 | |
| 41 | |
| 42 | def present(name, |
| 43 | config=None, |
| 44 | **kwargs): |
| 45 | ''' |
| 46 | Ensure the job is present in the Jenkins |
| 47 | configured jobs |
| 48 | name |
| 49 | The unique name for the Jenkins job |
| 50 | config |
| 51 | The Salt URL for the file to use for |
| 52 | configuring the job. |
| 53 | ''' |
Jakub Josef | 2a7739b | 2017-01-24 18:33:44 +0100 | [diff] [blame] | 54 | test = __opts__['test'] |
Jakub Josef | e380798 | 2016-12-15 11:54:51 +0100 | [diff] [blame] | 55 | ret = {'name': name, |
| 56 | 'result': True, |
| 57 | 'changes': {}, |
| 58 | 'comment': ['Job {0} is up to date.'.format(name)]} |
Jakub Josef | 2a7739b | 2017-01-24 18:33:44 +0100 | [diff] [blame] | 59 | if test: |
| 60 | status = 'CREATED' |
| 61 | ret['changes'][name] = status |
| 62 | ret['comment'] = 'Job %s %s' % (name, status.lower()) |
Jakub Josef | e380798 | 2016-12-15 11:54:51 +0100 | [diff] [blame] | 63 | else: |
chnyda | 90f133f | 2017-08-02 10:46:13 +0200 | [diff] [blame] | 64 | _current_job_config = '' |
| 65 | _job_exists = True |
| 66 | try: |
Jakub Josef | 2a7739b | 2017-01-24 18:33:44 +0100 | [diff] [blame] | 67 | _current_job_config = __salt__['jenkins.get_job_config'](name) |
chnyda | 2d78731 | 2017-08-07 20:07:56 +0200 | [diff] [blame^] | 68 | except salt.exceptions.SaltInvocationError as e: |
| 69 | if 'does not exists.' in str(e): |
| 70 | _job_exists = False |
| 71 | else: |
| 72 | raise e |
| 73 | |
chnyda | 90f133f | 2017-08-02 10:46:13 +0200 | [diff] [blame] | 74 | |
| 75 | if _job_exists: |
Jakub Josef | 2a7739b | 2017-01-24 18:33:44 +0100 | [diff] [blame] | 76 | buf = six.moves.StringIO(_current_job_config) |
chnyda | 90f133f | 2017-08-02 10:46:13 +0200 | [diff] [blame] | 77 | oldXMLstring = buf.read() |
Jakub Josef | e380798 | 2016-12-15 11:54:51 +0100 | [diff] [blame] | 78 | |
Jakub Josef | 2a7739b | 2017-01-24 18:33:44 +0100 | [diff] [blame] | 79 | cached_source_path = __salt__['cp.cache_file'](config, __env__) |
| 80 | with salt.utils.fopen(cached_source_path) as _fp: |
chnyda | 90f133f | 2017-08-02 10:46:13 +0200 | [diff] [blame] | 81 | newXMLstring = _fp.read() |
| 82 | if not _elements_equal(oldXMLstring.strip(), newXMLstring.strip()): |
| 83 | oldXML = ET.fromstring(oldXMLstring) |
| 84 | newXML = ET.fromstring(newXMLstring) |
Jakub Josef | 2a7739b | 2017-01-24 18:33:44 +0100 | [diff] [blame] | 85 | diff = difflib.unified_diff( |
| 86 | ET.tostringlist(oldXML, encoding='utf8', method='xml'), |
| 87 | ET.tostringlist(newXML, encoding='utf8', method='xml'), lineterm='') |
| 88 | __salt__['jenkins.update_job'](name, config, __env__) |
Jakub Josef | 654a148 | 2017-01-26 17:41:16 +0100 | [diff] [blame] | 89 | ret['changes'][name] = ''.join(diff) |
chnyda | 90f133f | 2017-08-02 10:46:13 +0200 | [diff] [blame] | 90 | ret['comment'] = 'Job {0} updated.'.format(name) |
Jakub Josef | e380798 | 2016-12-15 11:54:51 +0100 | [diff] [blame] | 91 | |
Jakub Josef | 2a7739b | 2017-01-24 18:33:44 +0100 | [diff] [blame] | 92 | else: |
| 93 | cached_source_path = __salt__['cp.cache_file'](config, __env__) |
| 94 | with salt.utils.fopen(cached_source_path) as _fp: |
| 95 | new_config_xml = _fp.read() |
Jakub Josef | e380798 | 2016-12-15 11:54:51 +0100 | [diff] [blame] | 96 | |
Jakub Josef | 2a7739b | 2017-01-24 18:33:44 +0100 | [diff] [blame] | 97 | __salt__['jenkins.create_job'](name, config, __env__) |
Jakub Josef | e380798 | 2016-12-15 11:54:51 +0100 | [diff] [blame] | 98 | |
Jakub Josef | 2a7739b | 2017-01-24 18:33:44 +0100 | [diff] [blame] | 99 | buf = six.moves.StringIO(new_config_xml) |
Jakub Josef | 654a148 | 2017-01-26 17:41:16 +0100 | [diff] [blame] | 100 | diff = difflib.unified_diff('', buf.readlines(), lineterm='') |
| 101 | ret['changes'][name] = ''.join(diff) |
Jakub Josef | 2a7739b | 2017-01-24 18:33:44 +0100 | [diff] [blame] | 102 | ret['comment'].append('Job {0} added.'.format(name)) |
| 103 | |
| 104 | ret['comment'] = '\n'.join(ret['comment']) |
Jakub Josef | e380798 | 2016-12-15 11:54:51 +0100 | [diff] [blame] | 105 | return ret |
| 106 | |
| 107 | |
| 108 | def absent(name, |
| 109 | **kwargs): |
| 110 | ''' |
| 111 | Ensure the job is present in the Jenkins |
| 112 | configured jobs |
| 113 | |
| 114 | name |
| 115 | The name of the Jenkins job to remove. |
| 116 | |
| 117 | ''' |
Jakub Josef | 2a7739b | 2017-01-24 18:33:44 +0100 | [diff] [blame] | 118 | test = __opts__['test'] |
Jakub Josef | e380798 | 2016-12-15 11:54:51 +0100 | [diff] [blame] | 119 | ret = {'name': name, |
| 120 | 'result': True, |
| 121 | 'changes': {}, |
| 122 | 'comment': []} |
Jakub Josef | 2a7739b | 2017-01-24 18:33:44 +0100 | [diff] [blame] | 123 | if test: |
| 124 | status = 'DELETED' |
| 125 | ret['changes'][name] = status |
| 126 | ret['comment'] = 'Node %s %s' % (name, status.lower()) |
Jakub Josef | e380798 | 2016-12-15 11:54:51 +0100 | [diff] [blame] | 127 | else: |
Jakub Josef | 2a7739b | 2017-01-24 18:33:44 +0100 | [diff] [blame] | 128 | _job_exists = __salt__['jenkins.job_exists'](name) |
| 129 | |
| 130 | if _job_exists: |
| 131 | __salt__['jenkins.delete_job'](name) |
| 132 | ret['comment'] = 'Job {0} deleted.'.format(name) |
| 133 | else: |
| 134 | ret['comment'] = 'Job {0} already absent.'.format(name) |
| 135 | return ret |
| 136 | |
| 137 | |
| 138 | def cleanup(name, jobs, **kwargs): |
| 139 | ''' |
| 140 | Perform a cleanup - uninstall any installed job absents in given jobs list |
| 141 | |
| 142 | name |
| 143 | The name of the Jenkins job to remove. |
| 144 | jobs |
| 145 | List of jobs which may NOT be uninstalled |
| 146 | |
| 147 | ''' |
| 148 | test = __opts__['test'] |
| 149 | ret = {'name': name, |
| 150 | 'result': True, |
| 151 | 'changes': {}, |
| 152 | 'comment': "Cleanup not necessary"} |
| 153 | list_jobs_groovy = """\ |
| 154 | print(Jenkins.instance.items.collect{{it -> it.name}}) |
| 155 | """ |
| 156 | deleted_jobs = [] |
| 157 | if test: |
| 158 | status = 'CLEANED' |
| 159 | ret['changes'][name] = status |
| 160 | ret['comment'] = 'Jobs %s' % status.lower() |
| 161 | else: |
| 162 | call_result = __salt__['jenkins_common.call_groovy_script'](list_jobs_groovy,{}) |
| 163 | if call_result["code"] == 200: |
| 164 | existing_jobs = call_result["msg"] |
| 165 | if existing_jobs: |
| 166 | for job in existing_jobs[1:-1].split(","): |
| 167 | if job: |
| 168 | job = job.strip() |
| 169 | if job not in jobs: |
| 170 | __salt__['jenkins.delete_job'](job) |
| 171 | deleted_jobs.append(job) |
| 172 | else: |
| 173 | log.error("Cannot get existing jobs list from Jenkins") |
| 174 | if len(deleted_jobs) > 0: |
| 175 | for del_job in deleted_jobs: |
| 176 | ret['changes'][del_job] = "removed" |
| 177 | ret['comment'] = 'Jobs {} deleted.'.format(deleted_jobs) |
| 178 | else: |
| 179 | status = 'FAILED' |
| 180 | log.error( |
| 181 | "Jenkins jobs API call failure: %s", call_result["msg"]) |
| 182 | ret['comment'] = 'Jenkins jobs API call failure: %s' % ( |
| 183 | call_result["msg"]) |
Jakub Josef | e380798 | 2016-12-15 11:54:51 +0100 | [diff] [blame] | 184 | return ret |