1
# Copyright (C) 2015 Canonical
3
# This program is free software: you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation, either version 3 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17
"""CI Automation: mojo project pool.
19
Instead of maintaining a 1:1 mapping between a mojo project and a service it
20
deploys, let's use a pool of mojo projects.
22
The code in this file helps us do that while avoiding race conditions.
24
The MojoProjectPool class handles allocating and deallocating mojo projects.
25
It does this with a directory on disk. This directory is called the 'pool
26
directory' in documentation.
28
The pool directory must contain files, the names of which are each mojo
29
project name. These files must be created manually. If a mojo project is
30
represented in the pool directory, then it is used exclusively by the
31
deployer, and MUST NOT be used manually (since manual invocation of mojo
32
will not update lock files).
34
MojoProjectPool allocates projects by creating symlinks on disk to project
35
files. We use symlinks because:
36
- symlink creation is atomic
37
- symlink counts can be read from the lockfile inode.
39
Symlinks are created in the 'project lock directory'.
42
from contextlib import contextmanager
48
class NoProjectError(Exception):
51
return "No mojo projects available"
54
class MojoProjectPool(object):
56
def __init__(self, project_dir, lock_dir):
57
if not os.path.exists(project_dir):
58
os.makedirs(project_dir)
59
self.project_dir = project_dir
60
if not os.path.exists(lock_dir):
62
self.lock_dir = lock_dir
65
def acquire_mojo_project(self):
66
"""Get a mojo project to use.
68
This contextmanager yields the mojo project name, so it can be used
71
with project_pool.acquire_mojo_project() as mojo_project_name:
72
print("Using mojo project: {}".format(project_name))
74
If no project can be found, a NoProjectError exception is raised.
77
candidates = os.listdir(self.project_dir)
78
random.shuffle(candidates)
79
for candidate_project in candidates:
83
os.path.join(self.lock_dir, candidate_project)
85
except FileExistsError:
87
# If we get here, the lock is acquired:
89
yield candidate_project
91
os.unlink(os.path.join(self.lock_dir, candidate_project))
94
raise NoProjectError()