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