aboutsummaryrefslogtreecommitdiff
path: root/deprecated/automation/server
diff options
context:
space:
mode:
authorZhizhou Yang <zhizhouy@google.com>2020-02-11 14:30:39 -0800
committerZhizhou Yang <zhizhouy@google.com>2020-02-12 01:35:08 +0000
commit7bda3eb62f8125f641cc9e3defcc5278cde2ba7b (patch)
tree36a0efdb898c22612d03591461136f680cd0a1a0 /deprecated/automation/server
parent81d651f89ac91819a77b8bd2ca720646326bf89a (diff)
downloadtoolchain-utils-7bda3eb62f8125f641cc9e3defcc5278cde2ba7b.tar.gz
toolchain-utils: move no longer used scripts to deprecated
This patch moves all scripts that are not used any more to deprecated directory. We do not need to migrated those scripts to python 3. BUG=chromium:1011676, chromium:1051236 TEST=None Change-Id: I2caac6204c82dcd21b2a121875a2f9851eaca322 Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/toolchain-utils/+/2051285 Commit-Queue: Zhizhou Yang <zhizhouy@google.com> Tested-by: Zhizhou Yang <zhizhouy@google.com> Auto-Submit: Zhizhou Yang <zhizhouy@google.com> Reviewed-by: George Burgess <gbiv@chromium.org>
Diffstat (limited to 'deprecated/automation/server')
-rw-r--r--deprecated/automation/server/__init__.py1
-rw-r--r--deprecated/automation/server/job_executer.py138
-rw-r--r--deprecated/automation/server/job_group_manager.py118
-rw-r--r--deprecated/automation/server/job_manager.py194
-rw-r--r--deprecated/automation/server/machine_manager.py77
-rwxr-xr-xdeprecated/automation/server/machine_manager_test.py32
-rw-r--r--deprecated/automation/server/monitor/__init__.py1
-rw-r--r--deprecated/automation/server/monitor/dashboard.py259
-rwxr-xr-xdeprecated/automation/server/monitor/manage.py20
-rw-r--r--deprecated/automation/server/monitor/settings.py49
-rwxr-xr-xdeprecated/automation/server/monitor/start.sh7
-rw-r--r--deprecated/automation/server/monitor/static/style.css101
-rw-r--r--deprecated/automation/server/monitor/templates/base.html30
-rw-r--r--deprecated/automation/server/monitor/templates/job.html29
-rw-r--r--deprecated/automation/server/monitor/templates/job_group.html46
-rw-r--r--deprecated/automation/server/monitor/templates/job_group_list.html35
-rw-r--r--deprecated/automation/server/monitor/templates/job_log.html20
-rw-r--r--deprecated/automation/server/monitor/templates/machine_list.html39
-rw-r--r--deprecated/automation/server/monitor/templates/snippet_attribute_table.html36
-rw-r--r--deprecated/automation/server/monitor/templates/snippet_code.html10
-rw-r--r--deprecated/automation/server/monitor/templates/snippet_links.html7
-rw-r--r--deprecated/automation/server/monitor/urls.py21
-rwxr-xr-xdeprecated/automation/server/server.py125
-rwxr-xr-xdeprecated/automation/server/server_test.py26
-rw-r--r--deprecated/automation/server/test_pool.csv4
25 files changed, 1425 insertions, 0 deletions
diff --git a/deprecated/automation/server/__init__.py b/deprecated/automation/server/__init__.py
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/deprecated/automation/server/__init__.py
@@ -0,0 +1 @@
+
diff --git a/deprecated/automation/server/job_executer.py b/deprecated/automation/server/job_executer.py
new file mode 100644
index 00000000..30b59463
--- /dev/null
+++ b/deprecated/automation/server/job_executer.py
@@ -0,0 +1,138 @@
+# Copyright 2010 Google Inc. All Rights Reserved.
+#
+
+import logging
+import os.path
+import threading
+
+from automation.common import command as cmd
+from automation.common import job
+from automation.common import logger
+from automation.common.command_executer import LoggingCommandExecuter
+from automation.common.command_executer import CommandTerminator
+
+
+class JobExecuter(threading.Thread):
+
+ def __init__(self, job_to_execute, machines, listeners):
+ threading.Thread.__init__(self)
+
+ assert machines
+
+ self.job = job_to_execute
+ self.listeners = listeners
+ self.machines = machines
+
+ # Set Thread name.
+ self.name = '%s-%s' % (self.__class__.__name__, self.job.id)
+
+ self._logger = logging.getLogger(self.__class__.__name__)
+ self._executer = LoggingCommandExecuter(self.job.dry_run)
+ self._terminator = CommandTerminator()
+
+ def _RunRemotely(self, command, fail_msg, command_timeout=1 * 60 * 60):
+ exit_code = self._executer.RunCommand(command,
+ self.job.primary_machine.hostname,
+ self.job.primary_machine.username,
+ command_terminator=self._terminator,
+ command_timeout=command_timeout)
+ if exit_code:
+ raise job.JobFailure(fail_msg, exit_code)
+
+ def _RunLocally(self, command, fail_msg, command_timeout=1 * 60 * 60):
+ exit_code = self._executer.RunCommand(command,
+ command_terminator=self._terminator,
+ command_timeout=command_timeout)
+ if exit_code:
+ raise job.JobFailure(fail_msg, exit_code)
+
+ def Kill(self):
+ self._terminator.Terminate()
+
+ def CleanUpWorkDir(self):
+ self._logger.debug('Cleaning up %r work directory.', self.job)
+ self._RunRemotely(cmd.RmTree(self.job.work_dir), 'Cleanup workdir failed.')
+
+ def CleanUpHomeDir(self):
+ self._logger.debug('Cleaning up %r home directory.', self.job)
+ self._RunLocally(cmd.RmTree(self.job.home_dir), 'Cleanup homedir failed.')
+
+ def _PrepareRuntimeEnvironment(self):
+ self._RunRemotely(
+ cmd.MakeDir(self.job.work_dir, self.job.logs_dir, self.job.results_dir),
+ 'Creating new job directory failed.')
+
+ # The log directory is ready, so we can prepare to log command's output.
+ self._executer.OpenLog(os.path.join(self.job.logs_dir,
+ self.job.log_filename_prefix))
+
+ def _SatisfyFolderDependencies(self):
+ for dependency in self.job.folder_dependencies:
+ to_folder = os.path.join(self.job.work_dir, dependency.dest)
+ from_folder = os.path.join(dependency.job.work_dir, dependency.src)
+ from_machine = dependency.job.primary_machine
+
+ if from_machine == self.job.primary_machine and dependency.read_only:
+ # No need to make a copy, just symlink it
+ self._RunRemotely(
+ cmd.MakeSymlink(from_folder, to_folder),
+ 'Failed to create symlink to required directory.')
+ else:
+ self._RunRemotely(
+ cmd.RemoteCopyFrom(from_machine.hostname,
+ from_folder,
+ to_folder,
+ username=from_machine.username),
+ 'Failed to copy required files.')
+
+ def _LaunchJobCommand(self):
+ command = self.job.GetCommand()
+
+ self._RunRemotely('%s; %s' % ('PS1=. TERM=linux source ~/.bashrc',
+ cmd.Wrapper(command,
+ cwd=self.job.work_dir)),
+ "Command failed to execute: '%s'." % command,
+ self.job.timeout)
+
+ def _CopyJobResults(self):
+ """Copy test results back to directory."""
+ self._RunLocally(
+ cmd.RemoteCopyFrom(self.job.primary_machine.hostname,
+ self.job.results_dir,
+ self.job.home_dir,
+ username=self.job.primary_machine.username),
+ 'Failed to copy results.')
+
+ def run(self):
+ self.job.status = job.STATUS_SETUP
+ self.job.machines = self.machines
+ self._logger.debug('Executing %r on %r in directory %s.', self.job,
+ self.job.primary_machine.hostname, self.job.work_dir)
+
+ try:
+ self.CleanUpWorkDir()
+
+ self._PrepareRuntimeEnvironment()
+
+ self.job.status = job.STATUS_COPYING
+
+ self._SatisfyFolderDependencies()
+
+ self.job.status = job.STATUS_RUNNING
+
+ self._LaunchJobCommand()
+ self._CopyJobResults()
+
+ # If we get here, the job succeeded.
+ self.job.status = job.STATUS_SUCCEEDED
+ except job.JobFailure as ex:
+ self._logger.error('Job failed. Exit code %s. %s', ex.exit_code, ex)
+ if self._terminator.IsTerminated():
+ self._logger.info('%r was killed', self.job)
+
+ self.job.status = job.STATUS_FAILED
+
+ self._executer.CloseLog()
+
+ for listener in self.listeners:
+ listener.NotifyJobComplete(self.job)
diff --git a/deprecated/automation/server/job_group_manager.py b/deprecated/automation/server/job_group_manager.py
new file mode 100644
index 00000000..d66f5e07
--- /dev/null
+++ b/deprecated/automation/server/job_group_manager.py
@@ -0,0 +1,118 @@
+# Copyright 2010 Google Inc. All Rights Reserved.
+#
+
+import copy
+import logging
+import threading
+
+from automation.common import command as cmd
+from automation.common import logger
+from automation.common.command_executer import CommandExecuter
+from automation.common import job
+from automation.common import job_group
+from automation.server.job_manager import IdProducerPolicy
+
+
+class JobGroupManager(object):
+
+ def __init__(self, job_manager):
+ self.all_job_groups = []
+
+ self.job_manager = job_manager
+ self.job_manager.AddListener(self)
+
+ self._lock = threading.Lock()
+ self._job_group_finished = threading.Condition(self._lock)
+
+ self._id_producer = IdProducerPolicy()
+ self._id_producer.Initialize(job_group.JobGroup.HOMEDIR_PREFIX,
+ 'job-group-(?P<id>\d+)')
+
+ self._logger = logging.getLogger(self.__class__.__name__)
+
+ def GetJobGroup(self, group_id):
+ with self._lock:
+ for group in self.all_job_groups:
+ if group.id == group_id:
+ return group
+
+ return None
+
+ def GetAllJobGroups(self):
+ with self._lock:
+ return copy.deepcopy(self.all_job_groups)
+
+ def AddJobGroup(self, group):
+ with self._lock:
+ group.id = self._id_producer.GetNextId()
+
+ self._logger.debug('Creating runtime environment for %r.', group)
+
+ CommandExecuter().RunCommand(cmd.Chain(
+ cmd.RmTree(group.home_dir), cmd.MakeDir(group.home_dir)))
+
+ with self._lock:
+ self.all_job_groups.append(group)
+
+ for job_ in group.jobs:
+ self.job_manager.AddJob(job_)
+
+ group.status = job_group.STATUS_EXECUTING
+
+ self._logger.info('Added %r to queue.', group)
+
+ return group.id
+
+ def KillJobGroup(self, group):
+ with self._lock:
+ self._logger.debug('Killing all jobs that belong to %r.', group)
+
+ for job_ in group.jobs:
+ self.job_manager.KillJob(job_)
+
+ self._logger.debug('Waiting for jobs to quit.')
+
+ # Lets block until the group is killed so we know it is completed
+ # when we return.
+ while group.status not in [job_group.STATUS_SUCCEEDED,
+ job_group.STATUS_FAILED]:
+ self._job_group_finished.wait()
+
+ def NotifyJobComplete(self, job_):
+ self._logger.debug('Handling %r completion event.', job_)
+
+ group = job_.group
+
+ with self._lock:
+ # We need to perform an action only if the group hasn't already failed.
+ if group.status != job_group.STATUS_FAILED:
+ if job_.status == job.STATUS_FAILED:
+ # We have a failed job, abort the job group
+ group.status = job_group.STATUS_FAILED
+ if group.cleanup_on_failure:
+ for job_ in group.jobs:
+ # TODO(bjanakiraman): We should probably only kill dependent jobs
+ # instead of the whole job group.
+ self.job_manager.KillJob(job_)
+ self.job_manager.CleanUpJob(job_)
+ else:
+ # The job succeeded successfully -- lets check to see if we are done.
+ assert job_.status == job.STATUS_SUCCEEDED
+ finished = True
+ for other_job in group.jobs:
+ assert other_job.status != job.STATUS_FAILED
+ if other_job.status != job.STATUS_SUCCEEDED:
+ finished = False
+ break
+
+ if finished and group.status != job_group.STATUS_SUCCEEDED:
+ # TODO(kbaclawski): Without check performed above following code
+ # could be called more than once. This would trigger StateMachine
+ # crash, because it cannot transition from STATUS_SUCCEEDED to
+ # STATUS_SUCCEEDED. Need to address that bug in near future.
+ group.status = job_group.STATUS_SUCCEEDED
+ if group.cleanup_on_completion:
+ for job_ in group.jobs:
+ self.job_manager.CleanUpJob(job_)
+
+ self._job_group_finished.notifyAll()
diff --git a/deprecated/automation/server/job_manager.py b/deprecated/automation/server/job_manager.py
new file mode 100644
index 00000000..7a65b918
--- /dev/null
+++ b/deprecated/automation/server/job_manager.py
@@ -0,0 +1,194 @@
+# Copyright 2010 Google Inc. All Rights Reserved.
+#
+
+import logging
+import os
+import re
+import threading
+
+from automation.common import job
+from automation.common import logger
+from automation.server.job_executer import JobExecuter
+
+
+class IdProducerPolicy(object):
+ """Produces series of unique integer IDs.
+
+ Example:
+ id_producer = IdProducerPolicy()
+ id_a = id_producer.GetNextId()
+ id_b = id_producer.GetNextId()
+ assert id_a != id_b
+ """
+
+ def __init__(self):
+ self._counter = 1
+
+ def Initialize(self, home_prefix, home_pattern):
+ """Find first available ID based on a directory listing.
+
+ Args:
+ home_prefix: A directory to be traversed.
+ home_pattern: A regexp describing all files/directories that will be
+ considered. The regexp must contain exactly one match group with name
+ "id", which must match an integer number.
+
+ Example:
+ id_producer.Initialize(JOBDIR_PREFIX, 'job-(?P<id>\d+)')
+ """
+ harvested_ids = []
+
+ if os.path.isdir(home_prefix):
+ for filename in os.listdir(home_prefix):
+ path = os.path.join(home_prefix, filename)
+
+ if os.path.isdir(path):
+ match = re.match(home_pattern, filename)
+
+ if match:
+ harvested_ids.append(int(match.group('id')))
+
+ self._counter = max(harvested_ids or [0]) + 1
+
+ def GetNextId(self):
+ """Calculates another ID considered to be unique."""
+ new_id = self._counter
+ self._counter += 1
+ return new_id
+
+
+class JobManager(threading.Thread):
+
+ def __init__(self, machine_manager):
+ threading.Thread.__init__(self, name=self.__class__.__name__)
+ self.all_jobs = []
+ self.ready_jobs = []
+ self.job_executer_mapping = {}
+
+ self.machine_manager = machine_manager
+
+ self._lock = threading.Lock()
+ self._jobs_available = threading.Condition(self._lock)
+ self._exit_request = False
+
+ self.listeners = []
+ self.listeners.append(self)
+
+ self._id_producer = IdProducerPolicy()
+ self._id_producer.Initialize(job.Job.WORKDIR_PREFIX, 'job-(?P<id>\d+)')
+
+ self._logger = logging.getLogger(self.__class__.__name__)
+
+ def StartJobManager(self):
+ self._logger.info('Starting...')
+
+ with self._lock:
+ self.start()
+ self._jobs_available.notifyAll()
+
+ def StopJobManager(self):
+ self._logger.info('Shutdown request received.')
+
+ with self._lock:
+ for job_ in self.all_jobs:
+ self._KillJob(job_.id)
+
+ # Signal to die
+ self._exit_request = True
+ self._jobs_available.notifyAll()
+
+ # Wait for all job threads to finish
+ for executer in self.job_executer_mapping.values():
+ executer.join()
+
+ def KillJob(self, job_id):
+ """Kill a job by id.
+
+ Does not block until the job is completed.
+ """
+ with self._lock:
+ self._KillJob(job_id)
+
+ def GetJob(self, job_id):
+ for job_ in self.all_jobs:
+ if job_.id == job_id:
+ return job_
+ return None
+
+ def _KillJob(self, job_id):
+ self._logger.info('Killing [Job: %d].', job_id)
+
+ if job_id in self.job_executer_mapping:
+ self.job_executer_mapping[job_id].Kill()
+ for job_ in self.ready_jobs:
+ if job_.id == job_id:
+ self.ready_jobs.remove(job_)
+ break
+
+ def AddJob(self, job_):
+ with self._lock:
+ job_.id = self._id_producer.GetNextId()
+
+ self.all_jobs.append(job_)
+ # Only queue a job as ready if it has no dependencies
+ if job_.is_ready:
+ self.ready_jobs.append(job_)
+
+ self._jobs_available.notifyAll()
+
+ return job_.id
+
+ def CleanUpJob(self, job_):
+ with self._lock:
+ if job_.id in self.job_executer_mapping:
+ self.job_executer_mapping[job_.id].CleanUpWorkDir()
+ del self.job_executer_mapping[job_.id]
+ # TODO(raymes): remove job from self.all_jobs
+
+ def NotifyJobComplete(self, job_):
+ self.machine_manager.ReturnMachines(job_.machines)
+
+ with self._lock:
+ self._logger.debug('Handling %r completion event.', job_)
+
+ if job_.status == job.STATUS_SUCCEEDED:
+ for succ in job_.successors:
+ if succ.is_ready:
+ if succ not in self.ready_jobs:
+ self.ready_jobs.append(succ)
+
+ self._jobs_available.notifyAll()
+
+ def AddListener(self, listener):
+ self.listeners.append(listener)
+
+ @logger.HandleUncaughtExceptions
+ def run(self):
+ self._logger.info('Started.')
+
+ while not self._exit_request:
+ with self._lock:
+ # Get the next ready job, block if there are none
+ self._jobs_available.wait()
+
+ while self.ready_jobs:
+ ready_job = self.ready_jobs.pop()
+
+ required_machines = ready_job.machine_dependencies
+ for pred in ready_job.predecessors:
+ required_machines[0].AddPreferredMachine(
+ pred.primary_machine.hostname)
+
+ machines = self.machine_manager.GetMachines(required_machines)
+ if not machines:
+ # If we can't get the necessary machines right now, simply wait
+ # for some jobs to complete
+ self.ready_jobs.insert(0, ready_job)
+ break
+ else:
+ # Mark as executing
+ executer = JobExecuter(ready_job, machines, self.listeners)
+ executer.start()
+ self.job_executer_mapping[ready_job.id] = executer
+
+ self._logger.info('Stopped.')
diff --git a/deprecated/automation/server/machine_manager.py b/deprecated/automation/server/machine_manager.py
new file mode 100644
index 00000000..b7186077
--- /dev/null
+++ b/deprecated/automation/server/machine_manager.py
@@ -0,0 +1,77 @@
+# Copyright 2010 Google Inc. All Rights Reserved.
+
+__author__ = 'asharif@google.com (Ahmad Sharif)'
+
+from operator import attrgetter
+import copy
+import csv
+import threading
+import os.path
+
+from automation.common import machine
+
+DEFAULT_MACHINES_FILE = os.path.join(os.path.dirname(__file__), 'test_pool.csv')
+
+
+class MachineManager(object):
+ """Container for list of machines one can run jobs on."""
+
+ @classmethod
+ def FromMachineListFile(cls, filename):
+ # Read the file and skip header
+ csv_file = csv.reader(open(filename, 'rb'), delimiter=',', quotechar='"')
+ csv_file.next()
+
+ return cls([machine.Machine(hostname, label, cpu, int(cores), os, user)
+ for hostname, label, cpu, cores, os, user in csv_file])
+
+ def __init__(self, machines):
+ self._machine_pool = machines
+ self._lock = threading.RLock()
+
+ def _GetMachine(self, mach_spec):
+ available_pool = [m for m in self._machine_pool if mach_spec.IsMatch(m)]
+
+ if available_pool:
+ # find a machine with minimum uses
+ uses = attrgetter('uses')
+
+ mach = min(available_pool, key=uses)
+
+ if mach_spec.preferred_machines:
+ preferred_pool = [m
+ for m in available_pool
+ if m.hostname in mach_spec.preferred_machines]
+ if preferred_pool:
+ mach = min(preferred_pool, key=uses)
+
+ mach.Acquire(mach_spec.lock_required)
+
+ return mach
+
+ def GetMachines(self, required_machines):
+ """Acquire machines for use by a job."""
+
+ with self._lock:
+ acquired_machines = [self._GetMachine(ms) for ms in required_machines]
+
+ if not all(acquired_machines):
+ # Roll back acquires
+ while acquired_machines:
+ mach = acquired_machines.pop()
+ if mach:
+ mach.Release()
+
+ return acquired_machines
+
+ def GetMachineList(self):
+ with self._lock:
+ return copy.deepcopy(self._machine_pool)
+
+ def ReturnMachines(self, machines):
+ with self._lock:
+ for m in machines:
+ m.Release()
+
+ def __str__(self):
+ return str(self._machine_pool)
diff --git a/deprecated/automation/server/machine_manager_test.py b/deprecated/automation/server/machine_manager_test.py
new file mode 100755
index 00000000..2fa5bb4b
--- /dev/null
+++ b/deprecated/automation/server/machine_manager_test.py
@@ -0,0 +1,32 @@
+#!/usr/bin/python2
+#
+# Copyright 2010 Google Inc. All Rights Reserved.
+
+__author__ = 'asharif@google.com (Ahmad Sharif)'
+
+import unittest
+from automation.common import machine
+from automation.server import machine_manager
+
+
+class MachineManagerTest(unittest.TestCase):
+
+ def setUp(self):
+ self.machine_manager = machine_manager.MachineManager()
+
+ def testPrint(self):
+ print self.machine_manager
+
+ def testGetLinuxBox(self):
+ mach_spec_list = [machine.MachineSpecification(os='linux')]
+ machines = self.machine_manager.GetMachines(mach_spec_list)
+ self.assertTrue(machines)
+
+ def testGetChromeOSBox(self):
+ mach_spec_list = [machine.MachineSpecification(os='chromeos')]
+ machines = self.machine_manager.GetMachines(mach_spec_list)
+ self.assertTrue(machines)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/deprecated/automation/server/monitor/__init__.py b/deprecated/automation/server/monitor/__init__.py
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/deprecated/automation/server/monitor/__init__.py
@@ -0,0 +1 @@
+
diff --git a/deprecated/automation/server/monitor/dashboard.py b/deprecated/automation/server/monitor/dashboard.py
new file mode 100644
index 00000000..f6befed8
--- /dev/null
+++ b/deprecated/automation/server/monitor/dashboard.py
@@ -0,0 +1,259 @@
+# Copyright 2011 Google Inc. All Rights Reserved.
+#
+
+__author__ = 'kbaclawski@google.com (Krystian Baclawski)'
+
+from collections import namedtuple
+import glob
+import gzip
+import os.path
+import pickle
+import time
+import xmlrpclib
+
+from django import forms
+from django.http import HttpResponseRedirect
+from django.shortcuts import render_to_response
+from django.template import Context
+from django.views import static
+
+Link = namedtuple('Link', 'href name')
+
+
+def GetServerConnection():
+ return xmlrpclib.Server('http://localhost:8000')
+
+
+def MakeDefaultContext(*args):
+ context = Context({'links': [
+ Link('/job-group', 'Job Groups'), Link('/machine', 'Machines')
+ ]})
+
+ for arg in args:
+ context.update(arg)
+
+ return context
+
+
+class JobInfo(object):
+
+ def __init__(self, job_id):
+ self._job = pickle.loads(GetServerConnection().GetJob(job_id))
+
+ def GetAttributes(self):
+ job = self._job
+
+ group = [Link('/job-group/%d' % job.group.id, job.group.label)]
+
+ predecessors = [Link('/job/%d' % pred.id, pred.label)
+ for pred in job.predecessors]
+
+ successors = [Link('/job/%d' % succ.id, succ.label)
+ for succ in job.successors]
+
+ machines = [Link('/machine/%s' % mach.hostname, mach.hostname)
+ for mach in job.machines]
+
+ logs = [Link('/job/%d/log' % job.id, 'Log')]
+
+ commands = enumerate(job.PrettyFormatCommand().split('\n'), start=1)
+
+ return {'text': [('Label', job.label), ('Directory', job.work_dir)],
+ 'link': [('Group', group), ('Predecessors', predecessors),
+ ('Successors', successors), ('Machines', machines),
+ ('Logs', logs)],
+ 'code': [('Command', commands)]}
+
+ def GetTimeline(self):
+ return [{'started': evlog.GetTimeStartedFormatted(),
+ 'state_from': evlog.event.from_,
+ 'state_to': evlog.event.to_,
+ 'elapsed': evlog.GetTimeElapsedRounded()}
+ for evlog in self._job.timeline.GetTransitionEventHistory()]
+
+ def GetLog(self):
+ log_path = os.path.join(self._job.logs_dir,
+ '%s.gz' % self._job.log_filename_prefix)
+
+ try:
+ log = gzip.open(log_path, 'r')
+ except IOError:
+ content = []
+ else:
+ # There's a good chance that file is not closed yet, so EOF handling
+ # function and CRC calculation will fail, thus we need to monkey patch the
+ # _read_eof method.
+ log._read_eof = lambda: None
+
+ def SplitLine(line):
+ prefix, msg = line.split(': ', 1)
+ datetime, stream = prefix.rsplit(' ', 1)
+
+ return datetime, stream, msg
+
+ content = map(SplitLine, log.readlines())
+ finally:
+ log.close()
+
+ return content
+
+
+class JobGroupInfo(object):
+
+ def __init__(self, job_group_id):
+ self._job_group = pickle.loads(GetServerConnection().GetJobGroup(
+ job_group_id))
+
+ def GetAttributes(self):
+ group = self._job_group
+
+ home_dir = [Link('/job-group/%d/files/' % group.id, group.home_dir)]
+
+ return {'text': [('Label', group.label),
+ ('Time submitted', time.ctime(group.time_submitted)),
+ ('State', group.status),
+ ('Cleanup on completion', group.cleanup_on_completion),
+ ('Cleanup on failure', group.cleanup_on_failure)],
+ 'link': [('Directory', home_dir)]}
+
+ def _GetJobStatus(self, job):
+ status_map = {'SUCCEEDED': 'success', 'FAILED': 'failure'}
+ return status_map.get(str(job.status), None)
+
+ def GetJobList(self):
+ return [{'id': job.id,
+ 'label': job.label,
+ 'state': job.status,
+ 'status': self._GetJobStatus(job),
+ 'elapsed': job.timeline.GetTotalTime()}
+ for job in self._job_group.jobs]
+
+ def GetHomeDirectory(self):
+ return self._job_group.home_dir
+
+ def GetReportList(self):
+ job_dir_pattern = os.path.join(self._job_group.home_dir, 'job-*')
+
+ filenames = []
+
+ for job_dir in glob.glob(job_dir_pattern):
+ filename = os.path.join(job_dir, 'report.html')
+
+ if os.access(filename, os.F_OK):
+ filenames.append(filename)
+
+ reports = []
+
+ for filename in sorted(filenames, key=lambda f: os.stat(f).st_ctime):
+ try:
+ with open(filename, 'r') as report:
+ reports.append(report.read())
+ except IOError:
+ pass
+
+ return reports
+
+
+class JobGroupListInfo(object):
+
+ def __init__(self):
+ self._all_job_groups = pickle.loads(GetServerConnection().GetAllJobGroups())
+
+ def _GetJobGroupState(self, group):
+ return str(group.status)
+
+ def _GetJobGroupStatus(self, group):
+ status_map = {'SUCCEEDED': 'success', 'FAILED': 'failure'}
+ return status_map.get(self._GetJobGroupState(group), None)
+
+ def GetList(self):
+ return [{'id': group.id,
+ 'label': group.label,
+ 'submitted': time.ctime(group.time_submitted),
+ 'state': self._GetJobGroupState(group),
+ 'status': self._GetJobGroupStatus(group)}
+ for group in self._all_job_groups]
+
+ def GetLabelList(self):
+ return sorted(set(group.label for group in self._all_job_groups))
+
+
+def JobPageHandler(request, job_id):
+ job = JobInfo(int(job_id))
+
+ ctx = MakeDefaultContext({
+ 'job_id': job_id,
+ 'attributes': job.GetAttributes(),
+ 'timeline': job.GetTimeline()
+ })
+
+ return render_to_response('job.html', ctx)
+
+
+def LogPageHandler(request, job_id):
+ job = JobInfo(int(job_id))
+
+ ctx = MakeDefaultContext({'job_id': job_id, 'log_lines': job.GetLog()})
+
+ return render_to_response('job_log.html', ctx)
+
+
+def JobGroupPageHandler(request, job_group_id):
+ group = JobGroupInfo(int(job_group_id))
+
+ ctx = MakeDefaultContext({
+ 'group_id': job_group_id,
+ 'attributes': group.GetAttributes(),
+ 'job_list': group.GetJobList(),
+ 'reports': group.GetReportList()
+ })
+
+ return render_to_response('job_group.html', ctx)
+
+
+def JobGroupFilesPageHandler(request, job_group_id, path):
+ group = JobGroupInfo(int(job_group_id))
+
+ return static.serve(request,
+ path,
+ document_root=group.GetHomeDirectory(),
+ show_indexes=True)
+
+
+class FilterJobGroupsForm(forms.Form):
+ label = forms.ChoiceField(label='Filter by label:', required=False)
+
+
+def JobGroupListPageHandler(request):
+ groups = JobGroupListInfo()
+ group_list = groups.GetList()
+
+ field = FilterJobGroupsForm.base_fields['label']
+ field.choices = [('*', '--- no filtering ---')]
+ field.choices.extend([(label, label) for label in groups.GetLabelList()])
+
+ if request.method == 'POST':
+ form = FilterJobGroupsForm(request.POST)
+
+ if form.is_valid():
+ label = form.cleaned_data['label']
+
+ if label != '*':
+ group_list = [group for group in group_list if group['label'] == label]
+ else:
+ form = FilterJobGroupsForm({'initial': '*'})
+
+ ctx = MakeDefaultContext({'filter': form, 'groups': group_list})
+
+ return render_to_response('job_group_list.html', ctx)
+
+
+def MachineListPageHandler(request):
+ machine_list = pickle.loads(GetServerConnection().GetMachineList())
+
+ return render_to_response('machine_list.html',
+ MakeDefaultContext({'machines': machine_list}))
+
+
+def DefaultPageHandler(request):
+ return HttpResponseRedirect('/job-group')
diff --git a/deprecated/automation/server/monitor/manage.py b/deprecated/automation/server/monitor/manage.py
new file mode 100755
index 00000000..59f6e216
--- /dev/null
+++ b/deprecated/automation/server/monitor/manage.py
@@ -0,0 +1,20 @@
+#!/usr/bin/python2
+#
+# Copyright 2011 Google Inc. All Rights Reserved.
+#
+
+__author__ = 'kbaclawski@google.com (Krystian Baclawski)'
+
+from django.core.management import execute_manager
+
+try:
+ import settings # Assumed to be in the same directory.
+except ImportError:
+ import sys
+
+ sys.stderr.write('Error: Can\'t find settings.py file in the directory '
+ 'containing %r.' % __file__)
+ sys.exit(1)
+
+if __name__ == '__main__':
+ execute_manager(settings)
diff --git a/deprecated/automation/server/monitor/settings.py b/deprecated/automation/server/monitor/settings.py
new file mode 100644
index 00000000..8cd20e35
--- /dev/null
+++ b/deprecated/automation/server/monitor/settings.py
@@ -0,0 +1,49 @@
+# Copyright 2011 Google Inc. All Rights Reserved.
+#
+# Django settings for monitor project.
+#
+# For explanation look here: http://docs.djangoproject.com/en/dev/ref/settings
+#
+
+__author__ = 'kbaclawski@google.com (Krystian Baclawski)'
+
+import os.path
+import sys
+
+# Path to the root of application. It's a custom setting, not related to Django.
+ROOT_PATH = os.path.dirname(os.path.realpath(sys.argv[0]))
+
+# Print useful information during runtime if possible.
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+# Sqlite3 database configuration, though we don't use it right now.
+DATABASE_ENGINE = 'sqlite3'
+DATABASE_NAME = os.path.join(ROOT_PATH, 'monitor.db')
+
+# Local time zone for this installation.
+TIME_ZONE = 'America/Los_Angeles'
+
+# Language code for this installation.
+LANGUAGE_CODE = 'en-us'
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# Absolute path to the directory that holds media.
+MEDIA_ROOT = os.path.join(ROOT_PATH, 'static') + '/'
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash if there is a path component (optional in other cases).
+MEDIA_URL = '/static/'
+
+# Used to provide a seed in secret-key hashing algorithms. Make this unique,
+# and don't share it with anybody.
+SECRET_KEY = '13p5p_4q91*8@yo+tvvt#2k&6#d_&e_zvxdpdil53k419i5sop'
+
+# A string representing the full Python import path to your root URLconf.
+ROOT_URLCONF = 'monitor.urls'
+
+# List of locations of the template source files, in search order.
+TEMPLATE_DIRS = (os.path.join(ROOT_PATH, 'templates'),)
diff --git a/deprecated/automation/server/monitor/start.sh b/deprecated/automation/server/monitor/start.sh
new file mode 100755
index 00000000..4fc53bef
--- /dev/null
+++ b/deprecated/automation/server/monitor/start.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+#
+# Copyright 2011 Google Inc. All Rights Reserved.
+# Author: kbaclawski@google.com (Krystian Baclawski)
+#
+
+./manage.py runserver "$HOSTNAME":8080
diff --git a/deprecated/automation/server/monitor/static/style.css b/deprecated/automation/server/monitor/static/style.css
new file mode 100644
index 00000000..b571b059
--- /dev/null
+++ b/deprecated/automation/server/monitor/static/style.css
@@ -0,0 +1,101 @@
+* { font-family: sans-serif; }
+
+.left { text-align: left; }
+.right { text-align: right; }
+
+.code { font-family: monospace; text-align: left; }
+.line1 { background-color: Gainsboro; }
+.line2 { background-color: WhiteSmoke; }
+
+.title { margin-bottom: 0.25em; }
+
+.success { background-color: LightGreen; }
+.failure { background-color: LightPink; }
+
+pre.code { margin: 0px; }
+
+div.header p.title {
+ border: 1px solid black;
+ font-size: 32px;
+ font-style: bold;
+ background-color: LightBlue;
+ text-align: center;
+ margin: 0px;
+ padding: 10px;
+ font-weight: bold;
+}
+
+div.links {
+ background-color: Azure;
+ margin-top: 2px;
+ padding: 8px 4px 8px 4px;
+ border: solid 1px;
+}
+
+div.content {
+ margin-top: 2px;
+ padding: 8px;
+ border: solid 1px;
+}
+
+div.content p.title {
+ font-size: 28px;
+ text-align: left;
+ margin: 0px;
+ margin-bottom: 8px;
+ padding: 12px;
+ font-weight: bold;
+}
+
+table { border-collapse: collapse; }
+td, th { text-align: center; }
+
+table.list td, th { padding: 3px 8px 2px 8px; border:1px solid black; }
+table.list td { font-family: monospace; }
+table.list th { background-color: LightGray; }
+
+table.attributes td { text-align: left; }
+table.attributes > tbody > tr > td:first-child { font-family: sans-serif; }
+
+table.raw { border-style: none; }
+table.raw td {
+ padding: 0em 0.5em 0em 0.5em;
+ border-style: none;
+ vertical-align: top;
+ text-align: right;
+ font-family: monospace;
+}
+table.raw > tbody > tr > td:first-child { border-left: 0px; }
+table.raw > tbody > tr > td { border-left: 1px solid; }
+
+a.button {
+ background-color: PeachPuff;
+ text-decoration: underline;
+ text-align: center;
+ color: Black;
+ padding: 4px;
+ border: solid 1px;
+}
+
+a.small {
+ padding: 2px 4px 2px 4px;
+ font-size: small;
+ border-color: Gray;
+ background-color: PapayaWhip;
+}
+
+a.button:hover { background-color: LightYellow; }
+a.button:active { background-color: Yellow; }
+
+a.column {
+ border-style: none;
+ display: block;
+ margin: -3px -8px -2px -8px;
+}
+
+div.warning {
+ background-color: MistyRose;
+ border: 1px solid Crimson;
+ padding: 0.5em;
+ font-size: x-large;
+}
diff --git a/deprecated/automation/server/monitor/templates/base.html b/deprecated/automation/server/monitor/templates/base.html
new file mode 100644
index 00000000..95ffc222
--- /dev/null
+++ b/deprecated/automation/server/monitor/templates/base.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <head>
+ <link rel="stylesheet" href="/static/style.css" />
+ <title>{% block title %}Automation Dashboard{% endblock %}</title>
+ </head>
+
+ <body>
+ <div class="header">
+ {% block header %}
+ <p class="title">Automation Dashboard</p>
+ {% endblock %}
+ </div>
+
+ <div class="links">
+ <span>Subpages:</span>
+ {% block links %}
+ {% for link in links %}
+ <a class="button" href="{{ link.href }}">{{ link.name }}</a>
+ {% endfor %}
+ {% endblock %}
+ </div>
+
+ <div class="content">
+ {% block content %}
+ {% endblock %}
+ </div>
+ </body>
+</html>
diff --git a/deprecated/automation/server/monitor/templates/job.html b/deprecated/automation/server/monitor/templates/job.html
new file mode 100644
index 00000000..90acd969
--- /dev/null
+++ b/deprecated/automation/server/monitor/templates/job.html
@@ -0,0 +1,29 @@
+{% extends "base.html" %}
+
+{% block content %}
+<h1 class="title">Job {{ job_id }}</h1>
+
+<h2 class="title">General information</h2>
+{% include "snippet_attribute_table.html" %}
+
+<h2 class="title">Timeline of status events</h2>
+<table class="list">
+ <tbody>
+ <tr>
+ <th>Started</th>
+ <th>From State</th>
+ <th>To State</th>
+ <th>Elapsed</th>
+ </tr>
+ {% for entry in timeline %}
+ <tr>
+ <td>{{ entry.started }}</td>
+ <td>{{ entry.state_from }}</td>
+ <td>{{ entry.state_to }}</td>
+ <td>{{ entry.elapsed }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+
+{% endblock %}
diff --git a/deprecated/automation/server/monitor/templates/job_group.html b/deprecated/automation/server/monitor/templates/job_group.html
new file mode 100644
index 00000000..b6ed8ea8
--- /dev/null
+++ b/deprecated/automation/server/monitor/templates/job_group.html
@@ -0,0 +1,46 @@
+{% extends "base.html" %}
+
+{% block content %}
+<h1 class="title">Job Group {{ group_id }}</h1>
+
+<h2 class="title">General information</h2>
+{% include "snippet_attribute_table.html" %}
+
+<h2 class="title">Job Listing</h2>
+<table class="list">
+ <tbody>
+ <tr>
+ <th>Job ID</th>
+ <th>Label</th>
+ <th>Turnaround Time</th>
+ <th>State</th>
+ </tr>
+ {% for job in job_list %}
+ <tr>
+ <td>
+ <a class="button column" href="/job/{{ job.id }}">{{ job.id }}</a>
+ </td>
+ <td>{{ job.label }}</td>
+ <td>{{ job.elapsed }}</td>
+ {% if job.status %}
+ <td class="{{ job.status }}">{{ job.state }}</td>
+ {% else %}
+ <td>{{ job.state }}</td>
+ {% endif %}
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+
+<h2 class="title">Report</h2>
+{% if reports %}
+{% autoescape off %}
+{% for report in reports %}
+{{ report }}
+{% endfor %}
+{% endautoescape %}
+{% else %}
+<div class="warning">No reports found!</div>
+{% endif %}
+
+{% endblock %}
diff --git a/deprecated/automation/server/monitor/templates/job_group_list.html b/deprecated/automation/server/monitor/templates/job_group_list.html
new file mode 100644
index 00000000..b82fa730
--- /dev/null
+++ b/deprecated/automation/server/monitor/templates/job_group_list.html
@@ -0,0 +1,35 @@
+{% extends "base.html" %}
+
+{% block content %}
+<p class="title">Job Groups</p>
+
+<form action="/job-group" method="post">
+{{ filter.as_p }}
+<p><input type="submit" value="Filter!" /></p>
+</form>
+
+<table class="list">
+ <tbody>
+ <tr>
+ <th>Group ID</th>
+ <th>Label</th>
+ <th>Time Submitted</th>
+ <th>Status</th>
+ </tr>
+ {% for group in groups %}
+ <tr>
+ <td>
+ <a class="button column" href="/job-group/{{ group.id }}">{{ group.id }}</a>
+ </td>
+ <td>{{ group.label }}</td>
+ <td>{{ group.submitted }}</td>
+ {% if group.status %}
+ <td class="{{ group.status }}">{{ group.state }}</td>
+ {% else %}
+ <td>{{ group.state }}</td>
+ {% endif %}
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+{% endblock %}
diff --git a/deprecated/automation/server/monitor/templates/job_log.html b/deprecated/automation/server/monitor/templates/job_log.html
new file mode 100644
index 00000000..937b21b0
--- /dev/null
+++ b/deprecated/automation/server/monitor/templates/job_log.html
@@ -0,0 +1,20 @@
+{% extends "base.html" %}
+
+{% block content %}
+<h1 class="title">Job {{ job_id }}</h1>
+
+<h2 class="title">Command output:</h2>
+
+<table class="raw">
+<tbody>
+{% for datetime, stream, line in log_lines %}
+<tr class="{% cycle 'line1' 'line2' %}">
+ <td>{{ datetime }}</td>
+ <td>{{ stream }}</td>
+ <td><pre class="code">{{ line|wordwrap:80 }}</pre></td>
+</tr>
+{% endfor %}
+</tbody>
+</table>
+
+{% endblock %}
diff --git a/deprecated/automation/server/monitor/templates/machine_list.html b/deprecated/automation/server/monitor/templates/machine_list.html
new file mode 100644
index 00000000..f81422d3
--- /dev/null
+++ b/deprecated/automation/server/monitor/templates/machine_list.html
@@ -0,0 +1,39 @@
+{% extends "base.html" %}
+
+{% block content %}
+<p class="title">Machines</p>
+
+<table class="list">
+<tbody>
+<tr>
+ <th>Hostname</th>
+ <th>Label</th>
+ <th>CPU</th>
+ <th>Cores</th>
+ <th>Operating System</th>
+ <th>Jobs Running</th>
+ <th>Locked</th>
+</tr>
+{% for machine in machines %}
+<tr>
+ <td>
+ <a class="button column" href="/machine/{{ machine.hostname }}">
+ {{ machine.hostname }}
+ </a>
+ </td>
+ <td>{{ machine.label }}</td>
+ <td>{{ machine.cpu }}</td>
+ <td>{{ machine.cores }}</td>
+ <td>{{ machine.os }}</td>
+ <td>{{ machine.uses }}</td>
+ {% if machine.locked %}
+ <td class="failure">Yes</td>
+ {% else %}
+ <td class="success">No</td>
+ {% endif %}
+</tr>
+{% endfor %}
+</tbody>
+</table>
+
+{% endblock %}
diff --git a/deprecated/automation/server/monitor/templates/snippet_attribute_table.html b/deprecated/automation/server/monitor/templates/snippet_attribute_table.html
new file mode 100644
index 00000000..24bacc17
--- /dev/null
+++ b/deprecated/automation/server/monitor/templates/snippet_attribute_table.html
@@ -0,0 +1,36 @@
+<table class="list attributes">
+ <tbody>
+ <tr>
+ <th>Attribute</th>
+ <th>Value</th>
+ </tr>
+ {% for name, value in attributes.text %}
+ <tr>
+ <td>{{ name }}</td>
+ <td>{{ value }}</td>
+ </tr>
+ {% endfor %}
+
+ {% for name, links in attributes.link %}
+ <tr>
+ <td>{{ name }}</td>
+ <td>
+ {% if links %}
+ {% for link in links %}
+ <a class="button small" href="{{ link.href }}">{{ link.name }}</a>
+ {% endfor %}
+ {% else %}
+ None
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+
+ {% for name, code in attributes.code %}
+ <tr>
+ <td>{{ name }}</td>
+ <td>{% include "snippet_code.html" %}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
diff --git a/deprecated/automation/server/monitor/templates/snippet_code.html b/deprecated/automation/server/monitor/templates/snippet_code.html
new file mode 100644
index 00000000..281754d6
--- /dev/null
+++ b/deprecated/automation/server/monitor/templates/snippet_code.html
@@ -0,0 +1,10 @@
+<table class="raw">
+<tbody>
+{% for num, line in code %}
+<tr class="{% cycle 'line1' 'line2' %}">
+ <td>{{ num }}</td>
+ <td><pre class="code">{{ line|wordwrap:120 }}</pre></td>
+</tr>
+{% endfor %}
+</tbody>
+</table>
diff --git a/deprecated/automation/server/monitor/templates/snippet_links.html b/deprecated/automation/server/monitor/templates/snippet_links.html
new file mode 100644
index 00000000..f19fa6e5
--- /dev/null
+++ b/deprecated/automation/server/monitor/templates/snippet_links.html
@@ -0,0 +1,7 @@
+{% if param %}
+{% for link in param %}
+<a class="button small" href="{{ link.href }}">{{ link.name }}</a>
+{% endfor %}
+{% else %}
+None
+{% endif %}
diff --git a/deprecated/automation/server/monitor/urls.py b/deprecated/automation/server/monitor/urls.py
new file mode 100644
index 00000000..1a6b2485
--- /dev/null
+++ b/deprecated/automation/server/monitor/urls.py
@@ -0,0 +1,21 @@
+# Copyright 2011 Google Inc. All Rights Reserved.
+#
+
+__author__ = 'kbaclawski@google.com (Krystian Baclawski)'
+
+from django.conf import settings
+from django.conf.urls.defaults import patterns
+
+urlpatterns = patterns(
+ 'dashboard', (r'^job-group$', 'JobGroupListPageHandler'),
+ (r'^machine$', 'MachineListPageHandler'),
+ (r'^job/(?P<job_id>\d+)/log$', 'LogPageHandler'),
+ (r'^job/(?P<job_id>\d+)$', 'JobPageHandler'), (
+ r'^job-group/(?P<job_group_id>\d+)/files/(?P<path>.*)$',
+ 'JobGroupFilesPageHandler'),
+ (r'^job-group/(?P<job_group_id>\d+)$', 'JobGroupPageHandler'),
+ (r'^$', 'DefaultPageHandler'))
+
+urlpatterns += patterns('',
+ (r'^static/(?P<path>.*)$', 'django.views.static.serve',
+ {'document_root': settings.MEDIA_ROOT}))
diff --git a/deprecated/automation/server/server.py b/deprecated/automation/server/server.py
new file mode 100755
index 00000000..c8f22521
--- /dev/null
+++ b/deprecated/automation/server/server.py
@@ -0,0 +1,125 @@
+#!/usr/bin/python2
+#
+# Copyright 2010 Google Inc. All Rights Reserved.
+
+import logging
+import optparse
+import pickle
+import signal
+from SimpleXMLRPCServer import SimpleXMLRPCServer
+import sys
+
+from automation.common import logger
+from automation.common.command_executer import CommandExecuter
+from automation.server import machine_manager
+from automation.server.job_group_manager import JobGroupManager
+from automation.server.job_manager import JobManager
+
+
+class Server(object):
+ """Plays a role of external interface accessible over XMLRPC."""
+
+ def __init__(self, machines_file=None, dry_run=False):
+ """Default constructor.
+
+ Args:
+ machines_file: Path to file storing a list of machines.
+ dry_run: If True, the server only simulates command execution.
+ """
+ CommandExecuter.Configure(dry_run)
+
+ self.job_manager = JobManager(
+ machine_manager.MachineManager.FromMachineListFile(
+ machines_file or machine_manager.DEFAULT_MACHINES_FILE))
+
+ self.job_group_manager = JobGroupManager(self.job_manager)
+
+ self._logger = logging.getLogger(self.__class__.__name__)
+
+ def ExecuteJobGroup(self, job_group, dry_run=False):
+ job_group = pickle.loads(job_group)
+ self._logger.info('Received ExecuteJobGroup(%r, dry_run=%s) request.',
+ job_group, dry_run)
+
+ for job in job_group.jobs:
+ job.dry_run = dry_run
+ return self.job_group_manager.AddJobGroup(job_group)
+
+ def GetAllJobGroups(self):
+ self._logger.info('Received GetAllJobGroups() request.')
+ return pickle.dumps(self.job_group_manager.GetAllJobGroups())
+
+ def KillJobGroup(self, job_group_id):
+ self._logger.info('Received KillJobGroup(%d) request.', job_group_id)
+ self.job_group_manager.KillJobGroup(pickle.loads(job_group_id))
+
+ def GetJobGroup(self, job_group_id):
+ self._logger.info('Received GetJobGroup(%d) request.', job_group_id)
+
+ return pickle.dumps(self.job_group_manager.GetJobGroup(job_group_id))
+
+ def GetJob(self, job_id):
+ self._logger.info('Received GetJob(%d) request.', job_id)
+
+ return pickle.dumps(self.job_manager.GetJob(job_id))
+
+ def GetMachineList(self):
+ self._logger.info('Received GetMachineList() request.')
+
+ return pickle.dumps(self.job_manager.machine_manager.GetMachineList())
+
+ def StartServer(self):
+ self.job_manager.StartJobManager()
+
+ def StopServer(self):
+ self.job_manager.StopJobManager()
+ self.job_manager.join()
+
+
+def GetServerOptions():
+ """Get server's settings from command line options."""
+ parser = optparse.OptionParser()
+ parser.add_option('-m',
+ '--machines-file',
+ dest='machines_file',
+ help='The location of the file '
+ 'containing the machines database',
+ default=machine_manager.DEFAULT_MACHINES_FILE)
+ parser.add_option('-n',
+ '--dry-run',
+ dest='dry_run',
+ help='Start the server in dry-run mode, where jobs will '
+ 'not actually be executed.',
+ action='store_true',
+ default=False)
+ return parser.parse_args()[0]
+
+
+def Main():
+ logger.SetUpRootLogger(filename='server.log', level=logging.DEBUG)
+
+ options = GetServerOptions()
+ server = Server(options.machines_file, options.dry_run)
+ server.StartServer()
+
+ def _HandleKeyboardInterrupt(*_):
+ server.StopServer()
+ sys.exit(1)
+
+ signal.signal(signal.SIGINT, _HandleKeyboardInterrupt)
+
+ try:
+ xmlserver = SimpleXMLRPCServer(
+ ('localhost', 8000),
+ allow_none=True,
+ logRequests=False)
+ xmlserver.register_instance(server)
+ xmlserver.serve_forever()
+ except Exception as ex:
+ logging.error(ex)
+ server.StopServer()
+ sys.exit(1)
+
+
+if __name__ == '__main__':
+ Main()
diff --git a/deprecated/automation/server/server_test.py b/deprecated/automation/server/server_test.py
new file mode 100755
index 00000000..131ebb3b
--- /dev/null
+++ b/deprecated/automation/server/server_test.py
@@ -0,0 +1,26 @@
+#!/usr/bin/python2
+#
+# Copyright 2010 Google Inc. All Rights Reserved.
+"""Machine manager unittest.
+
+MachineManagerTest tests MachineManager.
+"""
+
+__author__ = 'asharif@google.com (Ahmad Sharif)'
+
+import server
+import unittest
+
+
+class ServerTest(unittest.TestCase):
+
+ def setUp(self):
+ pass
+
+ def testGetAllJobs(self):
+ s = server.Server()
+ print s.GetAllJobs()
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/deprecated/automation/server/test_pool.csv b/deprecated/automation/server/test_pool.csv
new file mode 100644
index 00000000..b0700c9b
--- /dev/null
+++ b/deprecated/automation/server/test_pool.csv
@@ -0,0 +1,4 @@
+hostname,label,cpu,cores,os,username
+"chrotomation.mtv.corp.google.com","pc-workstation","core2duo",8,"linux","mobiletc-prebuild"
+"chromeos-test1.mtv.corp.google.com","cr48","atom",1,"chromeos","chromeos"
+"chromeos-test2.mtv.corp.google.com","cr48","atom",1,"chromeos","chromeos"