blob: 1ba92bf8152a1446411423f812e8039cc5b4e02a [file] [log] [blame]
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
'''Factory Update Server.
The factory update server is implemented as a thread to be started by shop floor
server. It monitors the given state_dir and detects autotest.tar.bz2 file
changes and then sets up the new update files into update_dir (under state_dir).
It also starts an rsync server to serve update_dir for clients to fetch update
files.
'''
import logging
import os
import pyinotify
import shutil
import subprocess
import threading
UPDATE_DIR = 'autotest'
TARBALL_NAME = 'autotest.tar.bz2'
LATEST_SYMLINK = 'latest'
LATEST_MD5SUM = 'latest.md5sum'
DEFAULT_RSYNCD_PORT = 8083
RSYNCD_CONFIG_TEMPLATE = '''port = %(port)d
pid file = %(pidfile)s
log file = %(logfile)s
use chroot = no
[autotest]
path = %(update_dir)s
read only = yes
'''
def StartRsyncServer(port, state_dir, update_dir):
configfile = os.path.join(state_dir, 'rsyncd.conf')
pidfile = os.path.join(state_dir, 'rsyncd.pid')
logfile = os.path.join(state_dir, 'rsyncd.log')
data = RSYNCD_CONFIG_TEMPLATE % dict(port=port,
pidfile=pidfile,
logfile=logfile,
update_dir=update_dir)
with open(configfile, 'w') as f:
f.write(data)
p = subprocess.Popen(('rsync', '--daemon', '--no-detach',
'--config=%s' % configfile))
logging.debug('Rsync server (pid %d) started on port %d', p.pid, port)
return p
def StopRsyncServer(rsyncd_process):
logging.debug('Stopping rsync server (pid %d)', rsyncd_process.pid)
rsyncd_process.terminate()
rsyncd_process.wait()
logging.debug('Rsync server stopped')
def CalculateMd5sum(filename):
p = subprocess.Popen(('md5sum', filename), stdout=subprocess.PIPE)
output, _ = p.communicate()
return output.split()[0]
class HandleEvents(pyinotify.ProcessEvent):
def __init__(self, update_dir):
self.update_dir = update_dir
def process_IN_CLOSE_WRITE(self, event):
if event.name == TARBALL_NAME:
# Calculate MD5.
md5sum = CalculateMd5sum(event.pathname)
logging.info('Found new ' + TARBALL_NAME + ' (%s)', md5sum)
# Create subfolder to hold tarball contents.
subfolder = os.path.join(self.update_dir, md5sum)
if os.path.exists(subfolder):
logging.error('Subfolder %s already exists', subfolder)
return
try:
os.mkdir(subfolder)
except Exception:
logging.error('Unable to create subfolder %s', subfolder)
return
# Extract tarball.
try:
subprocess.check_call(('tar', '-xjf', event.pathname, '-C', subfolder))
except subprocess.CalledProcessError:
logging.error('Failed to extract update files to subfolder %s',
subfolder)
shutil.rmtree(subfolder) # Clean up on error.
return
# Update symlink and latest.md5sum.
linkname = os.path.join(self.update_dir, LATEST_SYMLINK)
if os.path.islink(linkname):
os.remove(linkname)
os.symlink(md5sum, linkname)
with open(os.path.join(self.update_dir, LATEST_MD5SUM), 'w') as f:
f.write(md5sum)
logging.info('New update files (%s) setup complete', md5sum)
class FactoryUpdateServer(threading.Thread):
def __init__(self, state_dir, rsyncd_port=DEFAULT_RSYNCD_PORT):
threading.Thread.__init__(self)
self.state_dir = state_dir
self.update_dir = os.path.join(state_dir, UPDATE_DIR)
if not os.path.exists(self.update_dir):
os.mkdir(self.update_dir)
self._stop_event = threading.Event()
self._rsyncd = StartRsyncServer(rsyncd_port, state_dir, self.update_dir)
def run(self):
wm = pyinotify.WatchManager()
notifier = pyinotify.Notifier(wm, HandleEvents(self.update_dir))
mask = pyinotify.IN_CLOSE_WRITE
wm.add_watch(self.state_dir, mask)
try:
while True:
notifier.process_events()
if self._stop_event.is_set():
break
if notifier.check_events(500):
notifier.read_events()
finally:
notifier.stop()
logging.debug('Factory update server stopped')
def stop(self):
StopRsyncServer(self._rsyncd)
self._stop_event.set()
self.join()