blob: eda0c0ad553b083ca65e719912f025d08840c8f8 [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
19import select
Joshua Hesketh2e4b6112013-08-12 13:03:06 +100020import shutil
Joshua Hesketh0ddd6382013-07-26 10:33:36 +100021import subprocess
Joshua Hesketh11ed32c2013-08-09 10:42:36 +100022import swiftclient
Joshua Hesketh0ddd6382013-07-26 10:33:36 +100023import time
24
25
26class GitRepository(object):
27
28 """ Manage a git repository for our uses """
Joshua Hesketh363d0042013-07-26 11:44:07 +100029 log = logging.getLogger("lib.utils.GitRepository")
Joshua Hesketh0ddd6382013-07-26 10:33:36 +100030
31 def __init__(self, remote_url, local_path):
32 self.remote_url = remote_url
33 self.local_path = local_path
34 self._ensure_cloned()
35
36 self.repo = git.Repo(self.local_path)
37
Joshua Hesketh11ed32c2013-08-09 10:42:36 +100038 def _ensure_cloned(self):
39 if not os.path.exists(self.local_path):
40 self.log.debug("Cloning from %s to %s" % (self.remote_url,
41 self.local_path))
42 git.Repo.clone_from(self.remote_url, self.local_path)
43
Joshua Hesketh0ddd6382013-07-26 10:33:36 +100044 def fetch(self, ref):
45 # The git.remote.fetch method may read in git progress info and
46 # interpret it improperly causing an AssertionError. Because the
47 # data was fetched properly subsequent fetches don't seem to fail.
48 # So try again if an AssertionError is caught.
49 origin = self.repo.remotes.origin
50 self.log.debug("Fetching %s from %s" % (ref, origin))
51
52 try:
53 origin.fetch(ref)
54 except AssertionError:
55 origin.fetch(ref)
56
57 def checkout(self, ref):
58 self.log.debug("Checking out %s" % ref)
59 return self.repo.git.checkout(ref)
60
Joshua Hesketh11ed32c2013-08-09 10:42:36 +100061 def reset(self):
62 self._ensure_cloned()
63 self.log.debug("Resetting repository %s" % self.local_path)
64 self.update()
65 origin = self.repo.remotes.origin
66 for ref in origin.refs:
67 if ref.remote_head == 'HEAD':
68 continue
69 self.repo.create_head(ref.remote_head, ref, force=True)
70
71 # Reset to remote HEAD (usually origin/master)
72 self.repo.head.reference = origin.refs['HEAD']
73 self.repo.head.reset(index=True, working_tree=True)
74 self.repo.git.clean('-x', '-f', '-d')
75
76 def update(self):
77 self._ensure_cloned()
78 self.log.debug("Updating repository %s" % self.local_path)
79 origin = self.repo.remotes.origin
80 origin.update()
81 # If the remote repository is repacked, the repo object's
82 # cache may be out of date. Specifically, it caches whether
83 # to check the loose or packed DB for a given SHA. Further,
84 # if there was no pack or lose directory to start with, the
85 # repo object may not even have a database for it. Avoid
86 # these problems by recreating the repo object.
87 self.repo = git.Repo(self.local_path)
Joshua Hesketh0ddd6382013-07-26 10:33:36 +100088
Joshua Hesketh0ddd6382013-07-26 10:33:36 +100089
90def execute_to_log(cmd, logfile, timeout=-1,
91 watch_logs=[
92 ('[syslog]', '/var/log/syslog'),
93 ('[sqlslo]', '/var/log/mysql/slow-queries.log'),
94 ('[sqlerr]', '/var/log/mysql/error.log')
95 ],
96 heartbeat=True
97 ):
98 """ Executes a command and logs the STDOUT/STDERR and output of any
99 supplied watch_logs from logs into a new logfile
100
101 watch_logs is a list of tuples with (name,file) """
102
103 if not os.path.isdir(os.path.dirname(logfile)):
104 os.makedirs(os.path.dirname(logfile))
105
Joshua Heskethc7e963b2013-09-11 14:11:31 +1000106 logger = logging.getLogger(logfile)
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000107 log_hanlder = logging.FileHandler(logfile)
108 log_formatter = logging.Formatter('%(asctime)s %(message)s')
109 log_hanlder.setFormatter(log_formatter)
110 logger.addHandler(log_hanlder)
111
112 descriptors = {}
113
114 for watch_file in watch_logs:
115 fd = os.open(watch_file[1], os.O_RDONLY)
116 os.lseek(fd, 0, os.SEEK_END)
117 descriptors[fd] = dict(
118 name=watch_file[0],
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000119 poll=select.POLLIN,
120 lines=''
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000121 )
122
123 cmd += ' 2>&1'
124 start_time = time.time()
125 p = subprocess.Popen(
126 cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
127
128 descriptors[p.stdout.fileno()] = dict(
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000129 name='[output]',
Joshua Hesketh09b2f7f2013-07-29 09:05:58 +1000130 poll=(select.POLLIN | select.POLLHUP),
131 lines=''
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000132 )
133
134 poll_obj = select.poll()
135 for fd, descriptor in descriptors.items():
136 poll_obj.register(fd, descriptor['poll'])
137
138 last_heartbeat = time.time()
139
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000140 def process(fd):
141 """ Write the fd to log """
Joshua Hesketh3c0490b2013-08-12 10:33:40 +1000142 global last_heartbeat
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000143 descriptors[fd]['lines'] += os.read(fd, 1024 * 1024)
144 # Avoid partial lines by only processing input with breaks
Joshua Hesketh09b2f7f2013-07-29 09:05:58 +1000145 if descriptors[fd]['lines'].find('\n') != -1:
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000146 elems = descriptors[fd]['lines'].split('\n')
147 # Take all but the partial line
148 for l in elems[:-1]:
149 if len(l) > 0:
150 l = '%s %s' % (descriptors[fd]['name'], l)
151 logger.info(l)
152 last_heartbeat = time.time()
153 # Place the partial line back into lines to be processed
154 descriptors[fd]['lines'] = elems[-1]
155
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000156 while p.poll() is None:
157 if timeout > 0 and time.time() - start_time > timeout:
158 # Append to logfile
159 logger.info("[timeout]")
160 os.kill(p.pid, 9)
161
162 for fd, flag in poll_obj.poll(0):
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000163 process(fd)
Joshua Hesketh0ddd6382013-07-26 10:33:36 +1000164
165 if time.time() - last_heartbeat > 30:
166 # Append to logfile
167 logger.info("[heartbeat]")
168 last_heartbeat = time.time()
169
Joshua Hesketh1ab465f2013-07-26 13:57:28 +1000170 # Do one last write to get the remaining lines
171 for fd, flag in poll_obj.poll(0):
172 process(fd)
173
Joshua Hesketh86ab0642013-08-30 13:41:58 +1000174 # Clean up
175 for fd, descriptor in descriptors.items():
Joshua Hesketh8ca96fb2013-08-30 18:17:19 +1000176 poll_obj.unregister(fd)
Joshua Hesketh105af412013-09-02 10:24:36 +1000177 os.close(fd)
Joshua Hesketh721781d2013-09-02 16:06:01 +1000178 try:
179 p.kill()
180 except OSError:
181 pass
Joshua Hesketh86ab0642013-08-30 13:41:58 +1000182
Joshua Hesketh363d0042013-07-26 11:44:07 +1000183 logger.info('[script exit code = %d]' % p.returncode)
Joshua Heskethc7e963b2013-09-11 14:11:31 +1000184 logger.removeHandler(log_hanlder)
185 log_hanlder.flush()
186 log_hanlder.close()
Joshua Hesketh926502f2013-07-31 11:56:40 +1000187
Joshua Hesketh9f898052013-08-09 10:52:34 +1000188
Joshua Hesketh0c1d0c62013-09-24 22:33:18 +1000189def push_file(job_log_dir, file_path, publish_config):
Joshua Hesketh926502f2013-07-31 11:56:40 +1000190 """ Push a log file to a server. Returns the public URL """
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000191 method = publish_config['type'] + '_push_file'
Joshua Hesketh2e4b6112013-08-12 13:03:06 +1000192 if method in globals() and hasattr(globals()[method], '__call__'):
Joshua Hesketh25006962013-09-24 16:22:40 +1000193 return globals()[method](dest_dir, file_path, publish_config)
Joshua Hesketh9f898052013-08-09 10:52:34 +1000194
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000195
Joshua Hesketh0c1d0c62013-09-24 22:33:18 +1000196def swift_push_file(job_log_dir, file_path, swift_config):
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000197 """ Push a log file to a swift server. """
198 with open(file_path, 'r') as fd:
Joshua Hesketh0c1d0c62013-09-24 22:33:18 +1000199 name = job_log_dir + '_' + os.path.basename(file_path)
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000200 con = swiftclient.client.Connection(swift_config['authurl'],
201 swift_config['user'],
202 swift_config['apikey'])
203 obj = con.put_object(swift_config['container'], name, fd)
Joshua Hesketh27a0e272013-08-12 10:21:09 +1000204 return obj
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000205
Joshua Hesketh9f898052013-08-09 10:52:34 +1000206
Joshua Hesketh0c1d0c62013-09-24 22:33:18 +1000207def local_push_file(job_log_dir, file_path, local_config):
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000208 """ Copy the file locally somewhere sensible """
Joshua Hesketh0c1d0c62013-09-24 22:33:18 +1000209 dest_dir = os.path.join(local_config['path'], job_log_dir)
Joshua Heskethc76ecde2013-08-12 13:46:54 +1000210 dest_filename = os.path.basename(file_path)
211 if not os.path.isdir(dest_dir):
212 os.makedirs(dest_dir)
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000213
Joshua Heskethc76ecde2013-08-12 13:46:54 +1000214 dest_file = os.path.join(dest_dir, dest_filename)
Joshua Hesketh2e4b6112013-08-12 13:03:06 +1000215
216 shutil.copyfile(file_path, dest_file)
Joshua Hesketh25006962013-09-24 16:22:40 +1000217 return local_config['prepend_url'] + os.path.join(dest_dir, dest_filename)
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000218
Joshua Hesketh9f898052013-08-09 10:52:34 +1000219
Joshua Hesketh0c1d0c62013-09-24 22:33:18 +1000220def scp_push_file(job_log_dir, file_path, local_config):
Joshua Hesketh11ed32c2013-08-09 10:42:36 +1000221 """ Copy the file remotely over ssh """
Joshua Hesketh926502f2013-07-31 11:56:40 +1000222 pass
Joshua Hesketh25006962013-09-24 16:22:40 +1000223
224
225def determine_job_identifier(zuul_arguments, job, unique):
226 return os.path.join(zuul_arguments['ZUUL_CHANGE'][:2],
227 zuul_arguments['ZUUL_CHANGE'],
228 zuul_arguments['ZUUL_PATCHSET'],
229 zuul_arguments['ZUUL_PIPELINE'],
230 job,
231 unique[:7])