]> scripts.mit.edu Git - wizard.git/blob - wizard/deploy.py
Implement parametrization.
[wizard.git] / wizard / deploy.py
1 import os.path
2 import fileinput
3 import dateutil.parser
4 import distutils.version
5 import tempfile
6
7 import wizard
8 from wizard import log
9
10 ## -- Global Functions --
11
12 def getInstallLines(vs):
13     """Retrieves a list of lines from the version directory that
14     can be passed to Deployment.parse()"""
15     if os.path.isfile(vs):
16         return fileinput.input([vs])
17     return fileinput.input([vs + "/" + f for f in os.listdir(vs)])
18
19 def parse_install_lines(show, options, yield_errors = False):
20     if not show: show = applications()
21     show = frozenset(show)
22     for line in getInstallLines(options.versions_path):
23         # construction
24         try:
25             d = Deployment.parse(line)
26             name = d.application.name
27         except NoSuchApplication as e:
28             if yield_errors:
29                 yield e
30             continue
31         except Error:
32             # we consider this a worse error
33             logging.warning("Error with '%s'" % line.rstrip())
34             continue
35         # filter
36         if name + "-" + str(d.version) in show or name in show:
37             pass
38         else:
39             continue
40         # yield
41         yield d
42
43 ## -- Model Objects --
44
45 class Deployment(object):
46     """Represents a deployment of an autoinstall; i.e. a concrete
47     directory in web_scripts that has .scripts-version in it."""
48     def __init__(self, location, version=None):
49         """ `location`  Location of the deployment
50             `version`   ApplicationVersion of the app.  ONLY supply this
51                         if you don't mind having stale data; generally
52                         'wizard list' and commands that operate of of the
53                         versions store will set this."""
54         self.location = os.path.realpath(location)
55         self._app_version = version
56         # some cache variables
57         self._read_cache = {}
58         self._log = None
59     def read(self, file, force = False):
60         """Reads a file's contents, possibly from cache unless force is True."""
61         if force or file not in self._read_cache:
62             f = open(os.path.join(self.location, file))
63             self._read_cache[file] = f.read()
64             f.close()
65         return self._read_cache[file]
66     def extract(self):
67         """Extracts all the values of all variables from deployment."""
68         return self.application.extract(self)
69     def parametrize(self, dir):
70         """Edits files in dir to replace WIZARD_* variables with literal
71         instances.  This is used for constructing virtual merge bases, and
72         as such dir will generally not equal self.location."""
73         return self.application.parametrize(self, dir)
74     def updateVersion(self, version):
75         """Bump the version of this deployment.
76
77         This method will update the version of this deployment in memory
78         and on disk.  It doesn't actually do an upgrade.  The version
79         string you pass here should probably have '-scripts' as a suffix."""
80         self._app_version = self.application.makeVersion(version)
81         f = open(os.path.join(self.scripts_dir, 'version'), 'w')
82         f.write(self.application.name + '-' + version + "\n")
83         f.close()
84     def scriptsifyVersion(self):
85         """Converts from v1.0 to v1.0-scripts; use at end of migration."""
86         self.updateVersion(str(self.version) + '-scripts')
87     @property
88     def scripts_dir(self):
89         return os.path.join(self.location, '.scripts')
90     @property
91     def old_version_file(self):
92         """Use of this is discouraged for migrated installs."""
93         if os.path.isdir(self.scripts_dir):
94             return os.path.join(self.scripts_dir, 'old-version')
95         else:
96             return os.path.join(self.location, '.scripts-version')
97     @property
98     def version_file(self):
99         return os.path.join(self.scripts_dir, 'version')
100     @property
101     def application(self):
102         return self.app_version.application
103     @property
104     def log(self):
105         if not self._log:
106             self._log = log.DeployLog.load(self)
107         return self._log
108     @property
109     def version(self):
110         """Returns the distutils Version of the deployment"""
111         return self.app_version.version
112     @property
113     def app_version(self):
114         """Returns the ApplicationVersion of the deployment"""
115         if not self._app_version:
116             if os.path.isfile(self.version_file):
117                 fh = open(self.version_file)
118                 appname, _, version = fh.read().rstrip().partition('-')
119                 fh.close()
120                 self._app_version = ApplicationVersion.make(appname, version)
121             else:
122                 self._app_version = self.log[-1].version
123         return self._app_version
124     @staticmethod
125     def parse(line):
126         """Parses a line from the versions directory.
127
128         Note: Use this method only when speed is of the utmost
129         importance.  You should prefer to directly create a deployment
130         using Deployment(location) when accuracy is desired."""
131         line = line.rstrip()
132         try:
133             location, deploydir = line.split(":")
134         except ValueError:
135             return Deployment(line) # lazy loaded version
136         try:
137             return Deployment(location, version=ApplicationVersion.parse(deploydir))
138         except Error as e:
139             e.location = location
140             raise e
141
142 class Application(object):
143     """Represents the generic notion of an application, i.e.
144     mediawiki or phpbb."""
145     parametrized_files = []
146     def __init__(self, name):
147         self.name = name
148         self.versions = {}
149         # cache variables
150         self._extractors = {}
151         self._parametrizers = {}
152     @property
153     def repository(self):
154         """Returns the Git repository that would contain this application."""
155         repo = os.path.join("/afs/athena.mit.edu/contrib/scripts/git/autoinstalls", self.name + ".git")
156         if not os.path.isdir(repo):
157             raise NoRepositoryError(app)
158         return repo
159     def makeVersion(self, version):
160         if version not in self.versions:
161             self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self)
162         return self.versions[version]
163     def extract(self, deployment):
164         """Extracts wizard variables from a deployment."""
165         result = {}
166         for k,extractor in self.extractors.items():
167             result[k] = extractor(deployment)
168         return result
169     def parametrize(self, deployment, dir):
170         """Takes a generic source checkout at dir and parametrizes
171         it according to the values of deployment."""
172         variables = deployment.extract()
173         for file in self.parametrized_files:
174             fullpath = os.path.join(dir, file)
175             f = open(fullpath, "r")
176             contents = f.read()
177             f.close()
178             for key, value in variables.items():
179                 if value is None: continue
180                 contents = contents.replace(key, value)
181             tmp = tempfile.NamedTemporaryFile(delete=False)
182             tmp.write(contents)
183             tmp.close()
184             os.rename(tmp.name, fullpath)
185     @property
186     def extractors(self):
187         return {}
188     @staticmethod
189     def make(name):
190         """Makes an application, but uses the correct subtype if available."""
191         try:
192             __import__("wizard.app." + name)
193             return getattr(wizard.app, name).Application(name)
194         except ImportError:
195             return Application(name)
196
197 class ApplicationVersion(object):
198     """Represents an abstract notion of a version for an application"""
199     def __init__(self, version, application):
200         """ `version`       Instance of distutils.LooseVersion
201             `application`   Instance of Application
202         WARNING: Please don't call this directly; instead, use getVersion()
203         on the application you want, so that this version gets registered."""
204         self.version = version
205         self.application = application
206     @property
207     def scripts_tag(self):
208         """Returns the name of the Git tag for this version"""
209         # XXX: This assumes that there's only a -scripts version
210         # which will not be true in the future.  Unfortunately, finding
211         # the "true" latest version is computationally expensive
212         return "v%s-scripts" % self.version
213     def __cmp__(x, y):
214         return cmp(x.version, y.version)
215     @staticmethod
216     def parse(value):
217         """Parses a line from the versions directory and return ApplicationVersion.
218
219         Use this only for cases when speed is of primary importance;
220         the data in version is unreliable and when possible, you should
221         prefer directly instantiating a Deployment and having it query
222         the autoinstall itself for information.
223
224         value : The value to parse, will look like:
225            /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z for old style installs
226            APP-x.y.z-scripts for wizard style installs
227         """
228         name = value.split("/")[-1]
229         try:
230             if name.find("-") != -1:
231                 app, _, version = name.partition("-")
232             else:
233                 # kind of poor, maybe should error.  Generally this
234                 # will actually result in a not found error
235                 app = name
236                 version = "trunk"
237         except ValueError:
238             raise DeploymentParseError(deploydir)
239         return ApplicationVersion.make(app, version)
240     @staticmethod
241     def make(app, version):
242         try:
243             # defer to the application for version creation to enforce
244             # singletons
245             return applications()[app].makeVersion(version)
246         except KeyError:
247             raise NoSuchApplication(app)
248
249 ## -- Exceptions --
250
251 class Error(Exception):
252     """Base error class for this module"""
253     pass
254
255 class NoSuchApplication(Error):
256     def __init__(self, app):
257         """app : Application that doesn't exist"""
258         self.app = app
259         self.location = None # filled in when available
260
261 class DeploymentParseError(Error):
262     def __init__(self, value):
263         """value : Value from 'versions' that could not be parsed"""
264         self.value = value
265         self.location = None # filled in when available
266
267 class NoRepositoryError(Error):
268     def __init__(self, app):
269         """app : The application that doesn't have a Git repository"""
270         self.app = app
271
272 # If you want, you can wrap this up into a registry and access things
273 # through that, but it's not really necessary
274
275 application_list = [
276     "mediawiki", "wordpress", "joomla", "e107", "gallery2",
277     "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django",
278     # these are technically deprecated
279     "advancedpoll", "gallery",
280 ]
281 _applications = None
282
283 def applications():
284     """Hash table for looking up string application name to instance"""
285     global _applications
286     if not _applications:
287         _applications = dict([(n,Application.make(n)) for n in application_list ])
288     return _applications
289