]> scripts.mit.edu Git - wizard.git/blob - wizard/deploy.py
Improve upgrade dry run, improve error messages.
[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, log=None, version=None):
48         """ `location`  Location of the deployment
49             `version`   ApplicationVersion of the app (this is cached info)
50             `log`       DeployLog of the app"""
51         self.location = location
52         self._app_version = version
53         self._log = log
54         self._read_cache = {}
55     def read(self, file, force = False):
56         """Reads a file's contents and stuffs it in a cache"""
57         if force or file not in self._read_cache:
58             f = open(os.path.join(self.location, file))
59             self._read_cache[file] = f.read()
60             f.close()
61         return self._read_cache[file]
62     def extract(self):
63         return self.application.extract(self)
64     def updateVersion(self, version=None):
65         """`version` Version string to update to, or leave out to simply
66             force the creation of .scripts/version file"""
67         if not version:
68             version = str(self.version)
69         else:
70             self._app_version = self.application.makeVersion(version)
71         f = open(os.path.join(self.scripts_dir, 'version'), 'w')
72         f.write(self.application.name + '-' + version + "\n")
73         f.close()
74     def scriptsifyVersion(self):
75         """At the end of a migration, writes out the current version
76         with -scripts appended to .scripts/version"""
77         self.updateVersion(str(self.version) + '-scripts')
78     @property
79     def scripts_dir(self):
80         return os.path.join(self.location, '.scripts')
81     @property
82     def version_file(self):
83         return os.path.join(self.location, '.scripts-version')
84     @property
85     def application(self):
86         return self.app_version.application
87     @property
88     def log(self):
89         if not self._log:
90             self._log = log.DeployLog.load(self.version_file)
91         return self._log
92     @property
93     def version(self):
94         """Returns the distutils Version of the deployment"""
95         return self.app_version.version
96     @property
97     def app_version(self, force = False):
98         """Returns the ApplicationVersion of the deployment"""
99         if self._app_version and not force: return self._app_version
100         else: return self.log[-1].version
101     @staticmethod
102     def parse(line):
103         """Parses a line from the results of parallel-find.pl.
104         This will work out of the box with fileinput, see
105         getInstallLines()"""
106         line = line.rstrip()
107         try:
108             location, deploydir = line.split(":")
109         except ValueError:
110             return Deployment(line) # lazy loaded version
111         return Deployment(location, version=ApplicationVersion.parse(deploydir, location))
112
113 class Application(object):
114     """Represents the generic notion of an application, i.e.
115     mediawiki or phpbb."""
116     def __init__(self, name):
117         self.name = name
118         self.versions = {}
119         self._extractors = {}
120     @property
121     def repository(self):
122         """Returns the Git repository that would contain this application."""
123         repo = os.path.join("/afs/athena.mit.edu/contrib/scripts/git/autoinstalls", self.name + ".git")
124         if not os.path.isdir(repo):
125             raise NoRepositoryError(app)
126         return repo
127     def makeVersion(self, version):
128         if version not in self.versions:
129             self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self)
130         return self.versions[version]
131     def extract(self, deployment):
132         """Extracts wizard variables from a deployment."""
133         result = {}
134         for k,extractor in self.extractors.items():
135             result[k] = extractor(deployment)
136         return result
137     @property
138     def extractors(self):
139         return {}
140     @staticmethod
141     def make(name):
142         """Makes an application, but uses the correct subtype if available."""
143         try:
144             __import__("wizard.app." + name)
145             return getattr(wizard.app, name).Application(name)
146         except ImportError:
147             return Application(name)
148
149 class ApplicationVersion(object):
150     """Represents an abstract notion of a version for an application"""
151     def __init__(self, version, application):
152         """ `version`       Instance of distutils.LooseVersion
153             `application`   Instance of Application
154         WARNING: Please don't call this directly; instead, use getVersion()
155         on the application you want, so that this version gets registered."""
156         self.version = version
157         self.application = application
158     @property
159     def scripts_tag(self):
160         """Returns the name of the Git tag for this version"""
161         # XXX: This assumes that there's only a -scripts version
162         # which will not be true in the future.  Unfortunately, finding
163         # the "true" latest version is computationally expensive
164         return "v%s-scripts" % self.version
165     def __cmp__(x, y):
166         return cmp(x.version, y.version)
167     @staticmethod
168     def parse(deploydir,location,applookup=None):
169         # The version of the deployment, will be:
170         #   /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z for old style installs
171         name = deploydir.split("/")[-1]
172         try:
173             if name.find(" ") != -1:
174                 raw_app, raw_version = name.split(" ")
175                 version = raw_version[1:] # remove leading v
176                 app, _ = raw_app.split(".") # remove trailing .git
177             elif name.find("-") != -1:
178                 app, _, version = name.partition("-")
179             else:
180                 app = name
181                 version = "trunk"
182         except ValueError: # mostly from the a, b = foo.split(' ')
183             raise DeploymentParseError(deploydir, location)
184         if not applookup: applookup = applications()
185         try:
186             # defer to the application for version creation
187             return applookup[app].makeVersion(version)
188         except KeyError:
189             raise NoSuchApplication(app, location)
190
191 ## -- Exceptions --
192
193 class Error(Exception):
194     """Base error class for this module"""
195     pass
196
197 class NoSuchApplication(Error):
198     def __init__(self, name, location):
199         self.name = name
200         self.location = location
201     def __str__(self):
202         return "ERROR: Unrecognized app '%s' at %s" % (self.name, self.location)
203
204 class DeploymentParseError(Error):
205     def __init__(self, malformed, location):
206         self.malformed = malformed
207         self.location = location
208     def __str__(self):
209         return """ERROR: Unparseable '%s' at %s""" % (self.malformed, self.location)
210
211 class NoRepositoryError(Error):
212     def __init__(self, app):
213         self.app = app
214         self.location = "unknown"
215     def __str__(self):
216         return """
217
218 ERROR: Could not find repository for this application. Have
219 you converted the repository over? Is the name %s
220 the same as the name of the .git folder?
221 """ % self.app
222
223 # If you want, you can wrap this up into a registry and access things
224 # through that, but it's not really necessary
225
226 application_list = [
227     "mediawiki", "wordpress", "joomla", "e107", "gallery2",
228     "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django",
229     # these are technically deprecated
230     "advancedpoll", "gallery",
231 ]
232 _applications = None
233
234 def applications():
235     """Hash table for looking up string application name to instance"""
236     global _applications
237     if not _applications:
238         _applications = dict([(n,Application.make(n)) for n in application_list ])
239     return _applications
240