blob: f0603af6273489afb99596dacc2be2644dd85ce3 [file] [log] [blame]
Joshua Hesketh39a0fee2013-07-31 12:00:53 +10001# Copyright 2013 Rackspace Australia
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
Joshua Hesketh0ddd6382013-07-26 10:33:36 +100015
16import git
17import logging
18import os
Joshua Hesketh221ae742014-01-22 16:09:58 +110019import requests
Joshua Hesketh0ddd6382013-07-26 10:33:36 +100020import select
Joshua Hesketh2e4b6112013-08-12 13:03:06 +100021import shutil
Joshua Hesketh0ddd6382013-07-26 10:33:36 +100022import subprocess
Joshua Hesketh11ed32c2013-08-09 10:42:36 +100023import swiftclient
Joshua Hesketh0ddd6382013-07-26 10:33:36 +100024import time
25
26
Michael Still9abb2a42014-01-10 14:13:15 +110027log = logging.getLogger('lib.utils')
28
29
Joshua Hesketh0ddd6382013-07-26 10:33:36 +100030class GitRepository(object):
31
32 """ Manage a git repository for our uses """
Joshua Hesketh363d0042013-07-26 11:44:07 +100033 log = logging.getLogger("lib.utils.GitRepository")
Joshua Hesketh0ddd6382013-07-26 10:33:36 +100034
35 def __init__(self, remote_url, local_path):
36 self.remote_url = remote_url
37 self.local_path = local_path
38 self._ensure_cloned()
39
40 self.repo = git.Repo(self.local_path)
41
Joshua Hesketh11ed32c2013-08-09 10:42:36 +100042 def _ensure_cloned(self):
43 if not os.path.exists(self.local_path):
44 self.log.debug("Cloning from %s to %s" % (self.remote_url,
45 self.local_path))
46 git.Repo.clone_from(self.remote_url, self.local_path)
47
Joshua Hesketh0ddd6382013-07-26 10:33:36 +100048 def fetch(self, ref):
49 # The git.remote.fetch method may read in git progress info and
50 # interpret it improperly causing an AssertionError. Because the
51 # data was fetched properly subsequent fetches don't seem to fail.
52 # So try again if an AssertionError is caught.
53 origin = self.repo.remotes.origin
54 self.log.debug("Fetching %s from %s" % (ref, origin))
55
56 try:
57 origin.fetch(ref)
58 except AssertionError:
59 origin.fetch(ref)
60
61 def checkout(self, ref):
62 self.log.debug("Checking out %s" % ref)
63 return self.repo.git.checkout(ref)
64
Joshua Hesketh11ed32c2013-08-09 10:42:36 +100065 def reset(self):
66 self._ensure_cloned()
67 self.log.debug("Resetting repository %s" % self.local_path)
68 self.update()
69 origin = self.repo.remotes.origin
70 for ref in origin.refs:
71 if ref.remote_head == 'HEAD':
72 continue
73 self.repo.create_head(ref.remote_head, ref, force=True)
74
75 # Reset to remote HEAD (usually origin/master)
76 self.repo.head.reference = origin.refs['HEAD']
77 self.repo.head.reset(index=True, working_tree=True)
78 self.repo.git.clean('-x', '-f', '-d')
79
80 def update(self):
81 self._ensure_cloned()
82 self.log.debug("Updating repository %s" % self.local_path)
83 origin = self.repo.remotes.origin
84 origin.update()
85 # If the remote repository is repacked, the repo object's
86 # cache may be out of date. Specifically, it caches whether
87 # to check the loose or packed DB for a given SHA. Further,
88 # if there was no pack or lose directory to start with, the
89 # repo object may not even have a database for it. Avoid
90 # these problems by recreating the repo object.
91 self.repo = git.Repo(self.local_path)
Joshua Hesketh0ddd6382013-07-26 10:33:36 +100092
Joshua Hesketh0ddd6382013-07-26 10:33:36 +100093
94def execute_to_log(cmd, logfile, timeout=-1,
95 watch_logs=[
96 ('[syslog]', '/var/log/syslog'),
97 ('[sqlslo]', '/var/log/mysql/slow-queries.log'),
98 ('[sqlerr]', '/var/log/mysql/error.log')
99 ],
Michael Stille8cadae2014-01-06 19:47:27 +1100100 heartbeat=True, env=None, cwd=None
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000101 ):
102 """ Executes a command and logs the STDOUT/STDERR and output of any
103 supplied watch_logs from logs into a new logfile
104
105 watch_logs is a list of tuples with (name,file) """
106
107 if not os.path.isdir(os.path.dirname(logfile)):
108 os.makedirs(os.path.dirname(logfile))
109
Joshua Heskethc7e963b2013-09-11 14:11:31 +1000110 logger = logging.getLogger(logfile)
Michael Still732d25c2013-12-05 04:17:25 +1100111 log_handler = logging.FileHandler(logfile)
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000112 log_formatter = logging.Formatter('%(asctime)s %(message)s')
Michael Still732d25c2013-12-05 04:17:25 +1100113 log_handler.setFormatter(log_formatter)
114 logger.addHandler(log_handler)
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000115
116 descriptors = {}
117
118 for watch_file in watch_logs:
Michael Stillbe745262014-01-06 19:51:06 +1100119 if not os.path.exists(watch_file[1]):
120 logger.warning('Failed to monitor log file %s: file not found'
121 % watch_file[1])
122 continue
123
124 try:
125 fd = os.open(watch_file[1], os.O_RDONLY)
126 os.lseek(fd, 0, os.SEEK_END)
127 descriptors[fd] = {'name': watch_file[0],
128 'poll': select.POLLIN,
129 'lines': ''}
130 except Exception as e:
131 logger.warning('Failed to monitor log file %s: %s'
132 % (watch_file[1], e))
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000133
134 cmd += ' 2>&1'
135 start_time = time.time()
136 p = subprocess.Popen(
Michael Stille8cadae2014-01-06 19:47:27 +1100137 cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
138 env=env, cwd=cwd)
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000139
140 descriptors[p.stdout.fileno()] = dict(
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000141 name='[output]',
Joshua Hesketh09b2f7f2013-07-29 09:05:58 +1000142 poll=(select.POLLIN | select.POLLHUP),
143 lines=''
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000144 )
145
146 poll_obj = select.poll()
147 for fd, descriptor in descriptors.items():
148 poll_obj.register(fd, descriptor['poll'])
149
150 last_heartbeat = time.time()
151
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000152 def process(fd):
153 """ Write the fd to log """
Joshua Hesketh3c0490b2013-08-12 10:33:40 +1000154 global last_heartbeat
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000155 descriptors[fd]['lines'] += os.read(fd, 1024 * 1024)
156 # Avoid partial lines by only processing input with breaks
Joshua Hesketh09b2f7f2013-07-29 09:05:58 +1000157 if descriptors[fd]['lines'].find('\n') != -1:
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000158 elems = descriptors[fd]['lines'].split('\n')
159 # Take all but the partial line
160 for l in elems[:-1]:
161 if len(l) > 0:
162 l = '%s %s' % (descriptors[fd]['name'], l)
163 logger.info(l)
164 last_heartbeat = time.time()
165 # Place the partial line back into lines to be processed
166 descriptors[fd]['lines'] = elems[-1]
167
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000168 while p.poll() is None:
169 if timeout > 0 and time.time() - start_time > timeout:
170 # Append to logfile
171 logger.info("[timeout]")
172 os.kill(p.pid, 9)
173
174 for fd, flag in poll_obj.poll(0):
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000175 process(fd)
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000176
177 if time.time() - last_heartbeat > 30:
178 # Append to logfile
179 logger.info("[heartbeat]")
180 last_heartbeat = time.time()
181
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000182 # Do one last write to get the remaining lines
183 for fd, flag in poll_obj.poll(0):
184 process(fd)
185
Joshua Hesketh86ab0642013-08-30 13:41:58 +1000186 # Clean up
187 for fd, descriptor in descriptors.items():
Joshua Hesketh8ca96fb2013-08-30 18:17:19 +1000188 poll_obj.unregister(fd)
Joshua Hesketh6ad492c2014-04-08 17:12:02 +1000189 if fd == p.stdout.fileno():
190 # Don't try and close the process, it'll clean itself up
191 continue
Joshua Hesketh105af412013-09-02 10:24:36 +1000192 os.close(fd)
Joshua Hesketh721781d2013-09-02 16:06:01 +1000193 try:
194 p.kill()
195 except OSError:
196 pass
Joshua Hesketh86ab0642013-08-30 13:41:58 +1000197
Joshua Hesketh363d0042013-07-26 11:44:07 +1000198 logger.info('[script exit code = %d]' % p.returncode)
Michael Still732d25c2013-12-05 04:17:25 +1100199 logger.removeHandler(log_handler)
200 log_handler.flush()
201 log_handler.close()
Michael Still5231d4c2013-12-24 17:47:59 +1100202 return p.returncode
Joshua Hesketh926502f2013-07-31 11:56:40 +1000203
Joshua Hesketh9f898052013-08-09 10:52:34 +1000204
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100205def push_file(results_set_name, file_path, publish_config):
Joshua Hesketh926502f2013-07-31 11:56:40 +1000206 """ Push a log file to a server. Returns the public URL """
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000207 method = publish_config['type'] + '_push_file'
Joshua Hesketh2e4b6112013-08-12 13:03:06 +1000208 if method in globals() and hasattr(globals()[method], '__call__'):
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100209 return globals()[method](results_set_name, file_path, publish_config)
Joshua Hesketh9f898052013-08-09 10:52:34 +1000210
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000211
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100212def swift_push_file(results_set_name, file_path, swift_config):
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000213 """ Push a log file to a swift server. """
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100214 def _push_individual_file(results_set_name, file_path, swift_config):
Joshua Hesketh7859fde2014-01-22 14:53:17 +1100215 with open(file_path, 'r') as fd:
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100216 name = os.path.join(results_set_name, os.path.basename(file_path))
Joshua Hesketh7859fde2014-01-22 14:53:17 +1100217 con = swiftclient.client.Connection(
218 authurl=swift_config['authurl'],
219 user=swift_config['user'],
220 key=swift_config['password'],
221 os_options={'region_name': swift_config['region']},
222 tenant_name=swift_config['tenant'],
223 auth_version=2.0)
224 con.put_object(swift_config['container'], name, fd)
225
226 if os.path.isfile(file_path):
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100227 _push_individual_file(results_set_name, file_path, swift_config)
Joshua Hesketh7859fde2014-01-22 14:53:17 +1100228 elif os.path.isdir(file_path):
229 for path, folders, files in os.walk(file_path):
230 for f in files:
231 f_path = os.path.join(path, f)
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100232 _push_individual_file(results_set_name, f_path, swift_config)
Joshua Hesketh7859fde2014-01-22 14:53:17 +1100233
234 return (swift_config['prepend_url'] +
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100235 os.path.join(results_set_name, os.path.basename(file_path)))
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000236
Joshua Hesketh9f898052013-08-09 10:52:34 +1000237
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100238def local_push_file(results_set_name, file_path, local_config):
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000239 """ Copy the file locally somewhere sensible """
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100240 dest_dir = os.path.join(local_config['path'], results_set_name)
Joshua Heskethc76ecde2013-08-12 13:46:54 +1000241 dest_filename = os.path.basename(file_path)
242 if not os.path.isdir(dest_dir):
243 os.makedirs(dest_dir)
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000244
Joshua Heskethc76ecde2013-08-12 13:46:54 +1000245 dest_file = os.path.join(dest_dir, dest_filename)
Joshua Hesketh2e4b6112013-08-12 13:03:06 +1000246
Joshua Hesketh7859fde2014-01-22 14:53:17 +1100247 if os.path.isfile(file_path):
248 shutil.copyfile(file_path, dest_file)
249 elif os.path.isdir(file_path):
250 shutil.copytree(file_path, dest_file)
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100251 return local_config['prepend_url'] + os.path.join(results_set_name,
Joshua Hesketh0b3fe582013-09-27 14:52:35 +1000252 dest_filename)
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000253
Joshua Hesketh9f898052013-08-09 10:52:34 +1000254
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100255def scp_push_file(results_set_name, file_path, local_config):
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000256 """ Copy the file remotely over ssh """
Joshua Hesketh7859fde2014-01-22 14:53:17 +1100257 # TODO!
Joshua Hesketh926502f2013-07-31 11:56:40 +1000258 pass
Joshua Hesketh25006962013-09-24 16:22:40 +1000259
260
261def determine_job_identifier(zuul_arguments, job, unique):
Joshua Hesketh96adb282014-03-25 16:26:45 +1100262 # use new determined path from zuul
263 path = zuul_arguments['LOG_PATH']
Michael Still9abb2a42014-01-10 14:13:15 +1100264 return path
Joshua Hesketh221ae742014-01-22 16:09:58 +1100265
266
267def zuul_swift_upload(file_path, job_arguments):
268 """Upload working_dir to swift as per zuul's instructions"""
269 # NOTE(jhesketh): Zuul specifies an object prefix in the destination so
270 # we don't need to be concerned with results_set_name
271
272 file_list = []
273 if os.path.isfile(file_path):
274 file_list.append(file_path)
275 elif os.path.isdir(file_path):
276 for path, folders, files in os.walk(file_path):
277 for f in files:
278 f_path = os.path.join(path, f)
279 file_list.append(f_path)
280
281 # We are uploading the file_list as an HTTP POST multipart encoded.
282 # First grab out the information we need to send back from the hmac_body
283 payload = {}
284 (object_prefix,
285 payload['redirect'],
286 payload['max_file_size'],
287 payload['max_file_count'],
288 payload['expires']) = \
289 job_arguments['ZUUL_EXTRA_SWIFT_HMAC_BODY'].split('\n')
290
291 url = job_arguments['ZUUL_EXTRA_SWIFT_URL']
292 payload['signature'] = job_arguments['ZUUL_EXTRA_SWIFT_SIGNATURE']
293 logserver_prefix = job_arguments['ZUUL_EXTRA_SWIFT_LOGSERVER_PREFIX']
294
295 files = {}
296 for i, f in enumerate(file_list):
297 files['file%d' % (i + 1)] = open(f, 'rb')
298
299 requests.post(url, data=payload, files=files)
300
301 return (logserver_prefix +
302 job_arguments['ZUUL_EXTRA_SWIFT_DESTINATION_PREFIX'])