import os.path import fileinput import dateutil.parser import distutils.version import wizard from wizard import log ## -- Global Functions -- def getInstallLines(vs): """Retrieves a list of lines from the version directory that can be passed to Deployment.parse()""" if os.path.isfile(vs): return fileinput.input([vs]) return fileinput.input([vs + "/" + f for f in os.listdir(vs)]) def parse_install_lines(show, options, yield_errors = False): if not show: show = applications() show = frozenset(show) for line in getInstallLines(options.versions_path): # construction try: d = Deployment.parse(line) name = d.application.name except NoSuchApplication as e: if yield_errors: yield e continue except Error: # we consider this a worse error logging.warning("Error with '%s'" % line.rstrip()) continue # filter if name + "-" + str(d.version) in show or name in show: pass else: continue # yield yield d ## -- Model Objects -- class Deployment(object): """Represents a deployment of an autoinstall; i.e. a concrete directory in web_scripts that has .scripts-version in it.""" def __init__(self, location, version=None): """ `location` Location of the deployment `version` ApplicationVersion of the app. ONLY supply this if you don't mind having stale data; generally 'wizard list' and commands that operate of of the versions store will set this.""" self.location = location self._app_version = version # some cache variables self._read_cache = {} self._log = None def read(self, file, force = False): """Reads a file's contents, possibly from cache unless force is True.""" if force or file not in self._read_cache: f = open(os.path.join(self.location, file)) self._read_cache[file] = f.read() f.close() return self._read_cache[file] def extract(self): """Extracts all the values of all variables from deployment.""" return self.application.extract(self) def updateVersion(self, version): """Bump the version of this deployment. This method will update the version of this deployment in memory and on disk. It doesn't actually do an upgrade. The version string you pass here should probably have '-scripts' as a suffix.""" self._app_version = self.application.makeVersion(version) f = open(os.path.join(self.scripts_dir, 'version'), 'w') f.write(self.application.name + '-' + version + "\n") f.close() def scriptsifyVersion(self): """Converts from v1.0 to v1.0-scripts; use at end of migration.""" self.updateVersion(str(self.version) + '-scripts') @property def scripts_dir(self): return os.path.join(self.location, '.scripts') @property def old_version_file(self): """Use of this is discouraged for migrated installs.""" if os.path.isdir(self.scripts_dir): return os.path.join(self.scripts_dir, 'old-version') else: return os.path.join(self.location, '.scripts-version') @property def version_file(self): return os.path.join(self.scripts_dir, 'version') @property def application(self): return self.app_version.application @property def log(self): if not self._log: self._log = log.DeployLog.load(self) return self._log @property def version(self): """Returns the distutils Version of the deployment""" return self.app_version.version @property def app_version(self): """Returns the ApplicationVersion of the deployment""" if not self._app_version: if os.path.isfile(self.version_file): fh = open(self.version_file) appname, _, version = fh.read().rstrip().partition('-') fh.close() self._app_version = ApplicationVersion.make(appname, version) else: self._app_version = self.log[-1].version return self._app_version @staticmethod def parse(line): """Parses a line from the versions directory. Note: Use this method only when speed is of the utmost importance. You should prefer to directly create a deployment using Deployment(location) when accuracy is desired.""" line = line.rstrip() try: location, deploydir = line.split(":") except ValueError: return Deployment(line) # lazy loaded version try: return Deployment(location, version=ApplicationVersion.parse(deploydir)) except Error as e: e.location = location raise e class Application(object): """Represents the generic notion of an application, i.e. mediawiki or phpbb.""" def __init__(self, name): self.name = name self.versions = {} # cache variables self._extractors = {} @property def repository(self): """Returns the Git repository that would contain this application.""" repo = os.path.join("/afs/athena.mit.edu/contrib/scripts/git/autoinstalls", self.name + ".git") if not os.path.isdir(repo): raise NoRepositoryError(app) return repo def makeVersion(self, version): if version not in self.versions: self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self) return self.versions[version] def extract(self, deployment): """Extracts wizard variables from a deployment.""" result = {} for k,extractor in self.extractors.items(): result[k] = extractor(deployment) return result @property def extractors(self): return {} @staticmethod def make(name): """Makes an application, but uses the correct subtype if available.""" try: __import__("wizard.app." + name) return getattr(wizard.app, name).Application(name) except ImportError: return Application(name) class ApplicationVersion(object): """Represents an abstract notion of a version for an application""" def __init__(self, version, application): """ `version` Instance of distutils.LooseVersion `application` Instance of Application WARNING: Please don't call this directly; instead, use getVersion() on the application you want, so that this version gets registered.""" self.version = version self.application = application @property def scripts_tag(self): """Returns the name of the Git tag for this version""" # XXX: This assumes that there's only a -scripts version # which will not be true in the future. Unfortunately, finding # the "true" latest version is computationally expensive return "v%s-scripts" % self.version def __cmp__(x, y): return cmp(x.version, y.version) @staticmethod def parse(value): """Parses a line from the versions directory and return ApplicationVersion. Use this only for cases when speed is of primary importance; the data in version is unreliable and when possible, you should prefer directly instantiating a Deployment and having it query the autoinstall itself for information. value : The value to parse, will look like: /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z for old style installs APP-x.y.z-scripts for wizard style installs """ name = value.split("/")[-1] try: if name.find("-") != -1: app, _, version = name.partition("-") else: # kind of poor, maybe should error. Generally this # will actually result in a not found error app = name version = "trunk" except ValueError: raise DeploymentParseError(deploydir) return ApplicationVersion.make(app, version) @staticmethod def make(app, version): try: # defer to the application for version creation to enforce # singletons return applications()[app].makeVersion(version) except KeyError: raise NoSuchApplication(app) ## -- Exceptions -- class Error(Exception): """Base error class for this module""" pass class NoSuchApplication(Error): def __init__(self, app): """app : Application that doesn't exist""" self.app = app self.location = None # filled in when available class DeploymentParseError(Error): def __init__(self, value): """value : Value from 'versions' that could not be parsed""" self.value = value self.location = None # filled in when available class NoRepositoryError(Error): def __init__(self, app): """app : The application that doesn't have a Git repository""" self.app = app # If you want, you can wrap this up into a registry and access things # through that, but it's not really necessary application_list = [ "mediawiki", "wordpress", "joomla", "e107", "gallery2", "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django", # these are technically deprecated "advancedpoll", "gallery", ] _applications = None def applications(): """Hash table for looking up string application name to instance""" global _applications if not _applications: _applications = dict([(n,Application.make(n)) for n in application_list ]) return _applications