blob: 3a8fc2cc9ee993b384823e8f28dce8c57a9629ad [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
Joshua Hesketh96052bf2014-04-05 19:48:06 +110094def execute_to_log(cmd, logfile, timeout=-1, watch_logs=[], heartbeat=30,
95 env=None, cwd=None):
Joshua Hesketh0ddd6382013-07-26 10:33:36 +100096 """ Executes a command and logs the STDOUT/STDERR and output of any
97 supplied watch_logs from logs into a new logfile
98
99 watch_logs is a list of tuples with (name,file) """
100
101 if not os.path.isdir(os.path.dirname(logfile)):
102 os.makedirs(os.path.dirname(logfile))
103
Joshua Heskethc7e963b2013-09-11 14:11:31 +1000104 logger = logging.getLogger(logfile)
Michael Still732d25c2013-12-05 04:17:25 +1100105 log_handler = logging.FileHandler(logfile)
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000106 log_formatter = logging.Formatter('%(asctime)s %(message)s')
Michael Still732d25c2013-12-05 04:17:25 +1100107 log_handler.setFormatter(log_formatter)
108 logger.addHandler(log_handler)
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000109
110 descriptors = {}
111
112 for watch_file in watch_logs:
Michael Stillbe745262014-01-06 19:51:06 +1100113 if not os.path.exists(watch_file[1]):
114 logger.warning('Failed to monitor log file %s: file not found'
115 % watch_file[1])
116 continue
117
118 try:
119 fd = os.open(watch_file[1], os.O_RDONLY)
120 os.lseek(fd, 0, os.SEEK_END)
121 descriptors[fd] = {'name': watch_file[0],
122 'poll': select.POLLIN,
123 'lines': ''}
124 except Exception as e:
125 logger.warning('Failed to monitor log file %s: %s'
126 % (watch_file[1], e))
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000127
128 cmd += ' 2>&1'
Joshua Hesketh96052bf2014-04-05 19:48:06 +1100129 logger.info("[running %s]" % cmd)
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000130 start_time = time.time()
131 p = subprocess.Popen(
Michael Stille8cadae2014-01-06 19:47:27 +1100132 cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
133 env=env, cwd=cwd)
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000134
135 descriptors[p.stdout.fileno()] = dict(
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000136 name='[output]',
Joshua Hesketh09b2f7f2013-07-29 09:05:58 +1000137 poll=(select.POLLIN | select.POLLHUP),
138 lines=''
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000139 )
140
141 poll_obj = select.poll()
142 for fd, descriptor in descriptors.items():
143 poll_obj.register(fd, descriptor['poll'])
144
145 last_heartbeat = time.time()
146
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000147 def process(fd):
148 """ Write the fd to log """
Joshua Hesketh3c0490b2013-08-12 10:33:40 +1000149 global last_heartbeat
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000150 descriptors[fd]['lines'] += os.read(fd, 1024 * 1024)
151 # Avoid partial lines by only processing input with breaks
Joshua Hesketh09b2f7f2013-07-29 09:05:58 +1000152 if descriptors[fd]['lines'].find('\n') != -1:
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000153 elems = descriptors[fd]['lines'].split('\n')
154 # Take all but the partial line
155 for l in elems[:-1]:
156 if len(l) > 0:
157 l = '%s %s' % (descriptors[fd]['name'], l)
158 logger.info(l)
159 last_heartbeat = time.time()
160 # Place the partial line back into lines to be processed
161 descriptors[fd]['lines'] = elems[-1]
162
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000163 while p.poll() is None:
164 if timeout > 0 and time.time() - start_time > timeout:
165 # Append to logfile
166 logger.info("[timeout]")
167 os.kill(p.pid, 9)
168
169 for fd, flag in poll_obj.poll(0):
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000170 process(fd)
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000171
Joshua Hesketh96052bf2014-04-05 19:48:06 +1100172 if heartbeat and (time.time() - last_heartbeat > heartbeat):
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000173 # Append to logfile
174 logger.info("[heartbeat]")
175 last_heartbeat = time.time()
176
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000177 # Do one last write to get the remaining lines
178 for fd, flag in poll_obj.poll(0):
179 process(fd)
180
Joshua Hesketh86ab0642013-08-30 13:41:58 +1000181 # Clean up
182 for fd, descriptor in descriptors.items():
Joshua Hesketh8ca96fb2013-08-30 18:17:19 +1000183 poll_obj.unregister(fd)
Joshua Hesketh6ad492c2014-04-08 17:12:02 +1000184 if fd == p.stdout.fileno():
185 # Don't try and close the process, it'll clean itself up
186 continue
Joshua Hesketh105af412013-09-02 10:24:36 +1000187 os.close(fd)
Joshua Hesketh721781d2013-09-02 16:06:01 +1000188 try:
189 p.kill()
190 except OSError:
191 pass
Joshua Hesketh86ab0642013-08-30 13:41:58 +1000192
Joshua Hesketh363d0042013-07-26 11:44:07 +1000193 logger.info('[script exit code = %d]' % p.returncode)
Michael Still732d25c2013-12-05 04:17:25 +1100194 logger.removeHandler(log_handler)
195 log_handler.flush()
196 log_handler.close()
Michael Still5231d4c2013-12-24 17:47:59 +1100197 return p.returncode
Joshua Hesketh926502f2013-07-31 11:56:40 +1000198
Joshua Hesketh9f898052013-08-09 10:52:34 +1000199
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100200def push_file(results_set_name, file_path, publish_config):
Joshua Hesketh926502f2013-07-31 11:56:40 +1000201 """ Push a log file to a server. Returns the public URL """
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000202 method = publish_config['type'] + '_push_file'
Joshua Hesketh2e4b6112013-08-12 13:03:06 +1000203 if method in globals() and hasattr(globals()[method], '__call__'):
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100204 return globals()[method](results_set_name, file_path, publish_config)
Joshua Hesketh9f898052013-08-09 10:52:34 +1000205
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000206
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100207def swift_push_file(results_set_name, file_path, swift_config):
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000208 """ Push a log file to a swift server. """
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100209 def _push_individual_file(results_set_name, file_path, swift_config):
Joshua Hesketh7859fde2014-01-22 14:53:17 +1100210 with open(file_path, 'r') as fd:
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100211 name = os.path.join(results_set_name, os.path.basename(file_path))
Joshua Hesketh7859fde2014-01-22 14:53:17 +1100212 con = swiftclient.client.Connection(
213 authurl=swift_config['authurl'],
214 user=swift_config['user'],
215 key=swift_config['password'],
216 os_options={'region_name': swift_config['region']},
217 tenant_name=swift_config['tenant'],
218 auth_version=2.0)
219 con.put_object(swift_config['container'], name, fd)
220
221 if os.path.isfile(file_path):
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100222 _push_individual_file(results_set_name, file_path, swift_config)
Joshua Hesketh7859fde2014-01-22 14:53:17 +1100223 elif os.path.isdir(file_path):
224 for path, folders, files in os.walk(file_path):
225 for f in files:
226 f_path = os.path.join(path, f)
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100227 _push_individual_file(results_set_name, f_path, swift_config)
Joshua Hesketh7859fde2014-01-22 14:53:17 +1100228
229 return (swift_config['prepend_url'] +
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100230 os.path.join(results_set_name, os.path.basename(file_path)))
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000231
Joshua Hesketh9f898052013-08-09 10:52:34 +1000232
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100233def local_push_file(results_set_name, file_path, local_config):
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000234 """ Copy the file locally somewhere sensible """
Joshua Heskethd5d7a212014-10-29 17:42:59 +1100235 def _push_file_or_dir(results_set_name, file_path, local_config):
236 dest_dir = os.path.join(local_config['path'], results_set_name)
237 dest_filename = os.path.basename(file_path)
238 if not os.path.isdir(dest_dir):
239 os.makedirs(dest_dir)
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000240
Joshua Heskethd5d7a212014-10-29 17:42:59 +1100241 dest_file = os.path.join(dest_dir, dest_filename)
242
243 if os.path.isfile(file_path):
244 shutil.copyfile(file_path, dest_file)
245 elif os.path.isdir(file_path):
246 shutil.copytree(file_path, dest_file)
Joshua Hesketh2e4b6112013-08-12 13:03:06 +1000247
Joshua Hesketh7859fde2014-01-22 14:53:17 +1100248 if os.path.isfile(file_path):
Joshua Heskethd5d7a212014-10-29 17:42:59 +1100249 _push_file_or_dir(results_set_name, file_path, local_config)
Joshua Hesketh7859fde2014-01-22 14:53:17 +1100250 elif os.path.isdir(file_path):
Joshua Heskethd5d7a212014-10-29 17:42:59 +1100251 for f in os.listdir(file_path):
252 f_path = os.path.join(file_path, f)
253 _push_file_or_dir(results_set_name, f_path, local_config)
254
255 dest_filename = os.path.basename(file_path)
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100256 return local_config['prepend_url'] + os.path.join(results_set_name,
Joshua Hesketh0b3fe582013-09-27 14:52:35 +1000257 dest_filename)
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000258
Joshua Hesketh9f898052013-08-09 10:52:34 +1000259
Joshua Hesketh5a2edd42014-01-22 15:02:45 +1100260def scp_push_file(results_set_name, file_path, local_config):
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000261 """ Copy the file remotely over ssh """
Joshua Hesketh7859fde2014-01-22 14:53:17 +1100262 # TODO!
Joshua Hesketh926502f2013-07-31 11:56:40 +1000263 pass
Joshua Hesketh25006962013-09-24 16:22:40 +1000264
265
Joshua Hesketh221ae742014-01-22 16:09:58 +1100266def zuul_swift_upload(file_path, job_arguments):
267 """Upload working_dir to swift as per zuul's instructions"""
268 # NOTE(jhesketh): Zuul specifies an object prefix in the destination so
269 # we don't need to be concerned with results_set_name
270
271 file_list = []
272 if os.path.isfile(file_path):
273 file_list.append(file_path)
274 elif os.path.isdir(file_path):
275 for path, folders, files in os.walk(file_path):
276 for f in files:
277 f_path = os.path.join(path, f)
278 file_list.append(f_path)
279
280 # We are uploading the file_list as an HTTP POST multipart encoded.
281 # First grab out the information we need to send back from the hmac_body
282 payload = {}
283 (object_prefix,
284 payload['redirect'],
285 payload['max_file_size'],
286 payload['max_file_count'],
287 payload['expires']) = \
288 job_arguments['ZUUL_EXTRA_SWIFT_HMAC_BODY'].split('\n')
289
290 url = job_arguments['ZUUL_EXTRA_SWIFT_URL']
291 payload['signature'] = job_arguments['ZUUL_EXTRA_SWIFT_SIGNATURE']
292 logserver_prefix = job_arguments['ZUUL_EXTRA_SWIFT_LOGSERVER_PREFIX']
293
294 files = {}
295 for i, f in enumerate(file_list):
296 files['file%d' % (i + 1)] = open(f, 'rb')
297
298 requests.post(url, data=payload, files=files)
299
300 return (logserver_prefix +
301 job_arguments['ZUUL_EXTRA_SWIFT_DESTINATION_PREFIX'])