catalog.py revision 296
23N/A#!/usr/bin/python
23N/A#
23N/A# CDDL HEADER START
23N/A#
23N/A# The contents of this file are subject to the terms of the
23N/A# Common Development and Distribution License (the "License").
23N/A# You may not use this file except in compliance with the License.
23N/A#
23N/A# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
23N/A# or http://www.opensolaris.org/os/licensing.
23N/A# See the License for the specific language governing permissions
23N/A# and limitations under the License.
23N/A#
23N/A# When distributing Covered Code, include this CDDL HEADER in each
23N/A# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
23N/A# If applicable, add the following below this CDDL HEADER, with the
23N/A# fields enclosed by brackets "[]" replaced with your own identifying
23N/A# information: Portions Copyright [yyyy] [name of copyright owner]
23N/A#
23N/A# CDDL HEADER END
23N/A#
221N/A# Copyright 2008 Sun Microsystems, Inc. All rights reserved.
23N/A# Use is subject to license terms.
23N/A
221N/A"""Interfaces and implementation for the Catalog object, as well as functions
221N/Athat operate on lists of package FMRIs."""
221N/A
23N/Aimport os
23N/Aimport re
23N/Aimport urllib
174N/Aimport errno
204N/Aimport anydbm as dbm
204N/Aimport signal
204N/Aimport threading
215N/Aimport datetime
264N/Aimport sys
270N/Aimport cPickle
23N/A
23N/Aimport pkg.fmri as fmri
157N/Aimport pkg.version as version
204N/Aimport pkg.manifest as manifest
204N/A
157N/Aclass CatalogException(Exception):
157N/A def __init__(self, args=None):
157N/A self.args = args
157N/A
23N/Aclass RenameException(Exception):
258N/A def __init__(self, args=None):
258N/A self.args = args
258N/A
258N/Aclass Catalog(object):
34N/A """A Catalog is the representation of the package FMRIs available to
34N/A this client or repository. Both purposes utilize the same storage
34N/A format.
34N/A
24N/A The serialized structure of the repository is an unordered list of
26N/A available package versions, followed by an unordered list of
26N/A incorporation relationships between packages. This latter section
26N/A allows the graph to be topologically sorted by the client.
26N/A
26N/A S Last-Modified: [timespec]
50N/A
50N/A XXX A authority mirror-uri ...
37N/A XXX ...
37N/A
37N/A V fmri
26N/A V fmri
26N/A ...
26N/A C fmri
50N/A C fmri
50N/A ...
50N/A I fmri fmri
26N/A I fmri fmri
26N/A ...
26N/A
270N/A In order to improve the time to search the catalog, a cached list
270N/A of package names is kept in the catalog instance. In an effort
270N/A to prevent the catalog from having to generate this list every time
270N/A it is constructed, the array that contains the names is pickled and
270N/A saved and pkg_names.pkl.
270N/A """
59N/A
37N/A # XXX Mirroring records also need to be allowed from client
59N/A # configuration, and not just catalogs.
59N/A #
59N/A # XXX It would be nice to include available tags and package sizes,
59N/A # although this could also be calculated from the set of manifests.
59N/A #
59N/A # XXX Current code is O(N_packages) O(M_versions), should be
157N/A # O(1) O(M_versions), and possibly O(1) O(1).
157N/A #
59N/A # XXX Initial estimates suggest that the Catalog could be composed of
59N/A # 1e5 - 1e7 lines. Catalogs across these magnitudes will need to be
59N/A # spread out into chunks, and may require a delta-oriented update
59N/A # interface.
59N/A
23N/A def __init__(self, cat_root, authority = None, pkg_root = None,
270N/A read_only = False):
270N/A """Create a catalog. If the path supplied does not exist,
157N/A this will create the required directory structure.
157N/A Otherwise, if the directories are already in place, the
157N/A existing catalog is opened. If pkg_root is specified
195N/A and no catalog is found at cat_root, the catalog will be
195N/A rebuilt. authority names the authority that
195N/A is represented by this catalog."""
195N/A
157N/A self.catalog_root = cat_root
157N/A self.attrs = {}
157N/A self.auth = authority
195N/A self.renamed = None
258N/A self.pkg_names = set()
270N/A self.searchdb_update_handle = None
204N/A self.searchdb = None
207N/A self._search_available = False
204N/A self.deferred_searchdb_updates = []
204N/A # We need to lock the search database against multiple
204N/A # simultaneous updates from separate threads closing
204N/A # publication transactions.
204N/A self.searchdb_lock = threading.Lock()
204N/A self.pkg_root = pkg_root
204N/A self.read_only = read_only
270N/A if self.pkg_root:
204N/A self.searchdb_file = os.path.dirname(self.pkg_root) + \
204N/A "/search"
204N/A
34N/A self.attrs["npkgs"] = 0
157N/A
157N/A if not os.path.exists(cat_root):
157N/A os.makedirs(cat_root)
221N/A
157N/A # Rebuild catalog, if we're the depot and it's necessary
270N/A if pkg_root is not None:
204N/A self.build_catalog()
204N/A
157N/A self.load_attrs()
195N/A self.check_prefix()
220N/A
50N/A if self.pkg_names:
270N/A return
270N/A
270N/A # Load the list of pkg names. If it doesn't exist, build a list
270N/A # of pkg names. If the catalog gets rebuilt in build_catalog,
270N/A # add_fmri() will generate the list of package names instead.
270N/A try:
270N/A pkg_names = Catalog.load_pkg_names(self.catalog_root)
270N/A except IOError, e:
270N/A if e.errno == errno.ENOENT:
270N/A pkg_names = Catalog.build_pkg_names(
270N/A self.catalog_root)
270N/A if pkg_names and not self.read_only:
270N/A Catalog.save_pkg_names(
270N/A self.catalog_root,
270N/A pkg_names)
270N/A else:
270N/A raise
270N/A
270N/A self.pkg_names = pkg_names
270N/A
270N/A
270N/A
270N/A def add_fmri(self, fmri, critical = False):
157N/A """Add a package, named by the fmri, to the catalog.
157N/A Throws an exception if an identical package is already
157N/A present. Throws an exception if package has no version."""
157N/A if fmri.version == None:
157N/A raise CatalogException, \
157N/A "Unversioned FMRI not supported: %s" % fmri
157N/A
26N/A # Callers should verify that the FMRI they're going to add is
258N/A # valid; however, this check is here in case they're
258N/A # lackadaisical
258N/A if not self.valid_new_fmri(fmri):
258N/A raise CatalogException, \
258N/A "Existing renames make adding FMRI %s invalid." \
258N/A % fmri
258N/A
258N/A if critical:
50N/A pkgstr = "C %s\n" % fmri.get_fmri(anarchy = True)
157N/A else:
157N/A pkgstr = "V %s\n" % fmri.get_fmri(anarchy = True)
157N/A
34N/A pathstr = os.path.normpath(os.path.join(self.catalog_root,
157N/A "catalog"))
157N/A
157N/A pfile = file(pathstr, "a+")
157N/A pfile.seek(0)
157N/A
36N/A for entry in pfile:
157N/A if entry == pkgstr:
157N/A pfile.close()
157N/A raise CatalogException, \
157N/A "Package %s is already in the catalog" % \
207N/A fmri
207N/A
157N/A pfile.write(pkgstr)
157N/A pfile.close()
157N/A
30N/A self.attrs["npkgs"] += 1
157N/A
50N/A ts = datetime.datetime.now()
215N/A self.set_time(ts)
215N/A
215N/A # Add this pkg name to the list of package names
270N/A self.pkg_names.add(fmri.pkg_name)
270N/A Catalog.save_pkg_names(self.catalog_root, self.pkg_names)
270N/A
270N/A return ts
215N/A
157N/A def added_prefix(self, p):
220N/A """Perform any catalog transformations necessary if
220N/A prefix p is found in the catalog. Previously, we didn't
220N/A know how to handle this prefix and now we do. If we
220N/A need to transform the entry from server to client form,
220N/A make sure that happens here."""
220N/A
220N/A # Nothing to do now.
220N/A pass
220N/A
220N/A def attrs_as_lines(self):
157N/A """Takes the list of in-memory attributes and returns
157N/A a list of strings, each string naming an attribute."""
157N/A
50N/A ret = []
50N/A
50N/A for k,v in self.attrs.items():
157N/A s = "S %s: %s\n" % (k, v)
157N/A ret.append(s)
157N/A
157N/A return ret
157N/A
157N/A def _fmri_from_path(self, pkg, vers):
204N/A """Helper method that takes the full path to the package
204N/A directory and the name of the manifest file, and returns an FMRI
204N/A constructed from the information in those components."""
204N/A
204N/A v = version.Version(urllib.unquote(vers), None)
204N/A f = fmri.PkgFmri(urllib.unquote(os.path.basename(pkg)), None)
204N/A f.version = v
204N/A return f
204N/A
204N/A def check_prefix(self):
220N/A """If this version of the catalog knows about new prefixes,
220N/A check the on disk catalog to see if we can perform any
220N/A transformations based upon previously unknown catalog formats.
220N/A
220N/A This routine will add a catalog attribute if it doesn't exist,
220N/A otherwise it checks this attribute against a hard-coded
220N/A version-specific tuple to see if new methods were added.
220N/A
220N/A If new methods were added, it will call an additional routine
220N/A that updates the on-disk catalog, if necessary."""
220N/A
220N/A
220N/A # If a prefixes attribute doesn't exist, write one and get on
220N/A # with it.
220N/A if not "prefix" in self.attrs:
220N/A self.attrs["prefix"] = "".join(known_prefixes)
220N/A self.save_attrs()
220N/A return
220N/A
220N/A # Prefixes attribute does exist. Check if it has changed.
220N/A pfx_set = set(self.attrs["prefix"])
220N/A
220N/A # Nothing to do if prefixes haven't changed
220N/A if pfx_set == known_prefixes:
220N/A return
220N/A
220N/A # If known_prefixes contains a prefix not in pfx_set,
220N/A # add the prefix and perform a catalog transform.
220N/A new = known_prefixes.difference(pfx_set)
220N/A if new:
220N/A for p in new:
220N/A self.added_prefix(p)
220N/A
220N/A pfx_set.update(new)
220N/A
220N/A # Write out updated prefixes list
220N/A self.attrs["prefix"] = "".join(pfx_set)
220N/A self.save_attrs()
220N/A
220N/A def build_catalog(self):
204N/A """Walk the on-disk package data and build (or rebuild) the
204N/A package catalog and search database."""
204N/A try:
204N/A idx_mtime = \
204N/A os.stat(self.searchdb_file + ".pag").st_mtime
204N/A except OSError, e:
204N/A if e.errno != errno.ENOENT:
204N/A raise
204N/A idx_mtime = 0
204N/A
204N/A try:
204N/A cat_mtime = os.stat(os.path.join(
204N/A self.catalog_root, "catalog")).st_mtime
204N/A except OSError, e:
204N/A if e.errno != errno.ENOENT:
204N/A raise
204N/A cat_mtime = 0
204N/A
204N/A fmri_list = []
204N/A
157N/A # XXX eschew os.walk in favor of another os.listdir here?
157N/A tree = os.walk(self.pkg_root)
204N/A for pkg in tree:
157N/A if pkg[0] == self.pkg_root:
204N/A continue
157N/A
157N/A for e in os.listdir(pkg[0]):
157N/A ver_mtime = os.stat(os.path.join(
204N/A self.pkg_root, pkg[0], e)).st_mtime
204N/A
204N/A # XXX force a rebuild despite mtimes?
204N/A # XXX queue this and fork later?
204N/A if ver_mtime > cat_mtime:
204N/A f = self._fmri_from_path(pkg[0], e)
204N/A
204N/A self.add_fmri(f)
204N/A print f
204N/A
204N/A # XXX force a rebuild despite mtimes?
204N/A # If the database doesn't exist, don't bother
204N/A # building the list; we'll just build it all.
204N/A if ver_mtime > idx_mtime > 0:
204N/A fmri_list.append((pkg[0], e))
204N/A
204N/A # If we have no updates to make to the search database but it
204N/A # already exists, just make it available. If we do have updates
204N/A # to make (including possibly building it from scratch), fork it
204N/A # off into another process; when that's done, we'll mark it
204N/A # available.
204N/A if not fmri_list and idx_mtime > 0:
204N/A try:
264N/A self.searchdb = \
264N/A dbm.open(self.searchdb_file, "w")
264N/A
264N/A self._search_available = True
264N/A except dbm.error, e:
264N/A print >> sys.stderr, \
264N/A "Failed to open search database", \
264N/A "for writing: %s (errno=%s)" % \
264N/A (e.args[1], e.args[0])
264N/A try:
264N/A self.searchdb = \
264N/A dbm.open(self.searchdb_file, "r")
264N/A
264N/A self._search_available = True
264N/A except dbm.error, e:
264N/A print >> sys.stderr, \
264N/A "Failed to open search " + \
264N/A "database: %s (errno=%s)" % \
264N/A (e.args[1], e.args[0])
264N/A else:
204N/A if os.name == 'posix':
204N/A from pkg.subprocess_method import Mopen, PIPE
204N/A try:
204N/A signal.signal(signal.SIGCHLD, self.child_handler)
204N/A self.searchdb_update_handle = \
204N/A Mopen(self.update_searchdb, [fmri_list], {},
204N/A stderr = PIPE)
204N/A except ValueError:
204N/A # if we are in a subthread already, the signal method
204N/A # will not work
204N/A self.update_searchdb(fmri_list)
204N/A else:
204N/A # on non-unix, where there is no convenient
204N/A # way to fork subprocesses, just update the
204N/A # searchdb inline.
264N/A self.update_searchdb(fmri_list)
264N/A
264N/A def child_handler(self, sig, frame):
264N/A """Handler method for the SIGCLD signal. Checks to see if the
264N/A search database update child has finished, and enables searching
274N/A if it finished successfully, or logs an error if it didn't."""
264N/A if not self.searchdb_update_handle:
264N/A return
264N/A
264N/A rc = self.searchdb_update_handle.poll()
264N/A if rc == 0:
264N/A try:
264N/A self.searchdb = \
264N/A dbm.open(self.searchdb_file, "w")
264N/A
264N/A self._search_available = True
264N/A self.searchdb_update_handle = None
264N/A except dbm.error, e:
264N/A print >> sys.stderr, \
264N/A "Failed to open search database", \
264N/A "for writing: %s (errno=%s)" % \
264N/A (e.args[1], e.args[0])
264N/A try:
264N/A self.searchdb = \
264N/A dbm.open(self.searchdb_file, "r")
204N/A
204N/A self._search_available = True
204N/A self.searchdb_update_handle = None
204N/A return
204N/A except dbm.error, e:
204N/A print >> sys.stderr, \
204N/A "Failed to open search " + \
204N/A "database: %s (errno=%s)" % \
265N/A (e.args[1], e.args[0])
204N/A return
204N/A
261N/A if self.deferred_searchdb_updates:
264N/A self.update_searchdb(
264N/A self.deferred_searchdb_updates)
264N/A elif rc > 0:
264N/A # XXX This should be logged instead
264N/A print "ERROR building search database:"
264N/A print self.searchdb_update_handle.stderr.read()
264N/A
264N/A def __update_searchdb_unlocked(self, fmri_list):
264N/A new = False
264N/A if fmri_list:
264N/A if self.searchdb is None:
264N/A try:
204N/A self.searchdb = \
204N/A dbm.open(self.searchdb_file, "c")
204N/A except dbm.error, e:
204N/A # Since we're here explicitly to update
204N/A # the database, if we fail, there's
264N/A # nothing more to do.
264N/A print >> sys.stderr, \
264N/A "Failed to open search database", \
264N/A "for writing: %s (errno=%s)" % \
264N/A (e.args[1], e.args[0])
264N/A return 1
264N/A
264N/A if not self.searchdb.has_key("indir_num"):
264N/A self.searchdb["indir_num"] = "0"
264N/A else:
204N/A # new = True
204N/A try:
204N/A self.searchdb = \
204N/A dbm.open(self.searchdb_file, "n")
204N/A except dbm.error, e:
204N/A print >> sys.stderr, \
204N/A "Failed to open search database", \
204N/A "for writing: %s (errno=%s)" % \
204N/A (e.args[1], e.args[0])
204N/A return 1
204N/A
204N/A self.searchdb["indir_num"] = "0"
204N/A # XXX We should probably iterate over the catalog, for
204N/A # cases where manifests have stuck around, but have been
204N/A # moved to historical and removed from the catalog.
204N/A fmri_list = (
204N/A (os.path.join(self.pkg_root, pkg), ver)
204N/A for pkg in os.listdir(self.pkg_root)
204N/A for ver in os.listdir(
204N/A os.path.join(self.pkg_root, pkg))
204N/A )
204N/A
265N/A for pkg, vers in fmri_list:
265N/A mfst_path = os.path.join(pkg, vers)
265N/A mfst = manifest.Manifest()
265N/A mfst_file = file(mfst_path)
265N/A mfst.set_content(mfst_file.read())
265N/A mfst_file.close()
265N/A
265N/A f = self._fmri_from_path(pkg, vers)
265N/A
265N/A self.update_index(f, mfst.search_dict())
265N/A
265N/A def update_searchdb(self, fmri_list):
265N/A """Update the search database with the FMRIs passed in via
265N/A 'fmri_list'. If 'fmri_list' is empty or None, then rebuild the
265N/A database from scratch. 'fmri_list' should be a list of tuples
265N/A where the first element is the full path to the package name in
265N/A pkg_root and the second element is the version string."""
265N/A
265N/A # If we're in the process of updating the database in our
265N/A # separate process, and this particular update until that's
204N/A # done.
204N/A if self.searchdb_update_handle:
204N/A self.deferred_searchdb_updates += fmri_list
204N/A return
204N/A
204N/A self.searchdb_lock.acquire()
204N/A
157N/A try:
204N/A self.__update_searchdb_unlocked(fmri_list)
204N/A finally:
204N/A self.searchdb_lock.release()
204N/A
204N/A # If we rebuilt the database from scratch ... XXX why would we
204N/A # want to do this?
204N/A # if new:
204N/A # self.searchdb.close()
204N/A # self.searchdb = None
204N/A self._search_available = True
204N/A
204N/A # Five digits of a base-62 number represents a little over 900 million.
204N/A # Assuming 1 million tokens used in a WOS build (current imports use
204N/A # just short of 500k, but we don't have all the l10n packages, and may
204N/A # not have all the search tokens we want) and keeping every nightly
204N/A # build gives us 2.5 years before we run out of token space. We're
204N/A # likely to garbage collect manifests and rebuild the db before then.
204N/A #
204N/A # XXX We're eventually going to run into conflicts with real tokens
204N/A # here. This is unlikely until we hit, say "alias", which is a ways
204N/A # off, but we should still look at solving this.
204N/A idx_tok_len = 5
204N/A
204N/A def next_token(self):
204N/A alphabet = "abcdefghijklmnopqrstuvwxyz"
204N/A k = "0123456789" + alphabet + alphabet.upper()
204N/A
204N/A num = int(self.searchdb["indir_num"])
204N/A
204N/A s = ""
204N/A for i in range(1, self.idx_tok_len + 1):
204N/A junk, tail = divmod(num, 62 ** i)
204N/A idx, junk = divmod(tail, 62 ** (i - 1))
204N/A s = k[idx] + s
204N/A
204N/A # XXX Do we want to log warnings as we approach index capacity?
204N/A self.searchdb["indir_num"] = \
204N/A str(int(self.searchdb["indir_num"]) + 1)
204N/A
204N/A return s
204N/A
204N/A def update_index(self, fmri, search_dict):
204N/A """Update the search database with the data from the manifest
204N/A for 'fmri', which has been collected into 'search_dict'"""
204N/A # self.searchdb: token -> (type, fmri, action)
204N/A # XXX search_dict doesn't have action info, but should
204N/A
204N/A # Don't update the database if it already has this FMRI's
204N/A # indices.
204N/A if self.searchdb.has_key(str(fmri)):
204N/A return
204N/A
204N/A self.searchdb[str(fmri)] = "True"
157N/A for tok_type in search_dict.keys():
204N/A for tok in search_dict[tok_type]:
204N/A # XXX The database files are so damned huge (if
204N/A # holey) because we have zillions of copies of
204N/A # the full fmri strings. We might want to
204N/A # indirect these as well.
204N/A s = "%s %s" % (tok_type, fmri)
204N/A s_ptr = self.next_token()
204N/A self.searchdb[s_ptr] = s
204N/A
204N/A self.update_chain(tok, s_ptr)
204N/A
204N/A def update_chain(self, token, data_token):
204N/A """Because of the size limitations of the underlying database
204N/A records, not only do we have to store pointers to the actual
204N/A search data, but once the pointer records fill up, we have to
204N/A chain those records up to spillover records. This method adds
204N/A the pointer to the data to the end of the last link in the
204N/A chain, overflowing as necessary. The search token is passed in
204N/A as 'token', and the pointer to the actual data which should be
204N/A returned is passed in as 'data_token'."""
204N/A
204N/A while True:
204N/A try:
204N/A cur = self.searchdb[token]
204N/A except KeyError:
204N/A cur = ""
204N/A l = len(cur)
204N/A
204N/A # According to the ndbm man page, the total length of
204N/A # key and value must be less than 1024. Seems like the
204N/A # actual value is 1018, probably due to some padding or
204N/A # accounting bytes or something. The 2 is for the space
204N/A # separator and the plus-sign for the extension token.
204N/A # XXX The comparison should be against 1017, but that
204N/A # crahes in the if clause below trying to append the
204N/A # extension token. Dunno why.
204N/A if len(token) + l + self.idx_tok_len + 2 > 1000:
204N/A # If we're adding the first element in the next
204N/A # link of the chain, add the extension token to
204N/A # the end of this link, and put the token
204N/A # pointing to the data at the beginning of the
204N/A # next link.
204N/A if cur[-(self.idx_tok_len + 1)] != "+":
204N/A nindir_tok = "+" + self.next_token()
204N/A self.searchdb[token] += " " + nindir_tok
204N/A self.searchdb[nindir_tok] = data_token
204N/A break # from while True; we're done
204N/A # If we find an extension token, start looking
204N/A # at the next chain link.
204N/A else:
204N/A token = cur[-(self.idx_tok_len + 1):]
204N/A continue
204N/A
204N/A # If we get here, it's safe to append the data token to
204N/A # the current link, and get out.
204N/A if cur:
204N/A self.searchdb[token] += " " + data_token
204N/A else:
204N/A self.searchdb[token] = data_token
204N/A break
204N/A
204N/A def search(self, token):
204N/A """Search through the search database for 'token'. Return a
204N/A list of token type / fmri pairs."""
204N/A ret = []
204N/A
204N/A while True:
204N/A # For each indirect token in the search token's value,
204N/A # add its value to the return list. If we see a chain
204N/A # token, switch to its value and continue. If we fall
157N/A # out of the loop without seeing a chain token, we can
161N/A # return.
161N/A for tok in self.searchdb[token].split():
161N/A if tok[0] == "+":
161N/A token = tok
161N/A break
161N/A else:
161N/A ret.append(
161N/A self.searchdb[tok].split(" ", 1))
161N/A else:
161N/A return ret
195N/A
221N/A def get_matching_fmris(self, patterns, matcher = None,
270N/A constraint = None, counthash = None):
270N/A """Iterate through the catalog, looking for packages matching
270N/A 'pattern', based on the function in 'matcher' and the versioning
270N/A constraint described by 'constraint'. If 'matcher' is None,
270N/A uses fmri subset matching as the default. Returns a sorted list
270N/A of PkgFmri objects, newest versions first. If 'counthash' is a
270N/A dictionary, instead store the number of matched fmris for each
161N/A package name which was matched."""
161N/A
161N/A tuples = {}
161N/A names_matched = set()
221N/A
221N/A if self.attrs["npkgs"] == 0:
221N/A return []
221N/A
161N/A if matcher is None:
161N/A matcher = fmri.fmri_match
161N/A
161N/A if not isinstance(patterns, list):
161N/A patterns = [ patterns ]
161N/A
161N/A # 'patterns' may be partially or fully decorated fmris; we want
157N/A # to extract their names and versions to match separately
270N/A # against the catalog.
270N/A #
270N/A # XXX "5.11" here needs to be saner
270N/A for pattern in patterns:
270N/A if isinstance(pattern, fmri.PkgFmri):
270N/A tuples[pattern] = pattern.tuple()
220N/A else:
270N/A tuples[pattern] = \
157N/A fmri.PkgFmri(pattern, "5.11").tuple()
221N/A
221N/A # Walk list of pkg names and patterns. See if any of the
221N/A # patterns match known package names
147N/A for p in self.pkg_names:
30N/A for t in tuples.values():
157N/A if matcher(p, t[1]):
157N/A names_matched.add(p)
157N/A
157N/A pkgs = self._list_fmris_matched(names_matched)
195N/A
195N/A ret = extract_matching_fmris(pkgs, patterns, matcher,
195N/A constraint, counthash)
195N/A
195N/A return sorted(ret, reverse = True)
195N/A
195N/A def fmris(self):
195N/A """A generator function that produces FMRIs as it
24N/A iterates over the contents of the catalog."""
157N/A
220N/A try:
220N/A pfile = file(os.path.normpath(
220N/A os.path.join(self.catalog_root, "catalog")), "r")
220N/A except IOError, e:
161N/A if e.errno == errno.ENOENT:
258N/A return
258N/A else:
258N/A raise
161N/A
258N/A for entry in pfile:
161N/A if not entry[1].isspace() or \
195N/A not entry[0] in known_prefixes:
195N/A continue
161N/A
161N/A try:
161N/A if entry[0] not in tuple("CV"):
161N/A continue
258N/A
258N/A cv, pkg, cat_name, cat_version = entry.split()
59N/A if pkg == "pkg":
157N/A yield fmri.PkgFmri("%s@%s" %
59N/A (cat_name, cat_version),
258N/A authority = self.auth)
258N/A except ValueError:
258N/A # Handle old two-column catalog file, mostly in
258N/A # use on server.
258N/A cv, cat_fmri = entry.split()
258N/A yield fmri.PkgFmri(cat_fmri,
258N/A authority = self.auth)
258N/A
258N/A pfile.close()
258N/A
258N/A def fmri_renamed_dest(self, fmri):
258N/A """Returns a list of RenameRecords where fmri is listed as the
258N/A destination package."""
258N/A
258N/A # Don't bother doing this if no FMRI is present
258N/A if not fmri:
258N/A return
258N/A
258N/A # Load renamed packages, if needed
258N/A if self.renamed is None:
258N/A self._load_renamed()
258N/A
258N/A for rr in self.renamed:
258N/A if rr.destname == fmri.pkg_name and \
258N/A fmri.version >= rr.destversion:
258N/A yield rr
258N/A
258N/A def fmri_renamed_src(self, fmri):
258N/A """Returns a list of RenameRecords where fmri is listed as
258N/A the source package."""
258N/A
258N/A # Don't bother doing this if no FMRI is present
258N/A if not fmri:
258N/A return
270N/A
270N/A # Load renamed packages, if needed
270N/A if self.renamed is None:
270N/A self._load_renamed()
270N/A
270N/A for rr in self.renamed:
270N/A if rr.srcname == fmri.pkg_name and \
270N/A fmri.version < rr.srcversion:
270N/A yield rr
270N/A
270N/A def _list_fmris_matched(self, pkg_names):
270N/A """Given a list of pkg_names, return all of the FMRIs
270N/A that contain an pkg_name entry as a substring."""
270N/A fmris = []
270N/A
270N/A try:
270N/A pfile = file(os.path.normpath(
270N/A os.path.join(self.catalog_root, "catalog")), "r")
270N/A except IOError, e:
270N/A if e.errno == errno.ENOENT:
270N/A return fmris
270N/A else:
270N/A raise
270N/A
270N/A for entry in pfile:
270N/A if not entry[1].isspace() or \
270N/A not entry[0] in known_prefixes:
270N/A continue
270N/A
270N/A try:
270N/A if entry[0] not in tuple("CV"):
270N/A continue
270N/A
270N/A cv, pkg, cat_name, cat_version = entry.split()
270N/A if pkg != "pkg":
270N/A continue
270N/A if cat_name not in pkg_names:
270N/A continue
270N/A except ValueError:
270N/A # Handle old two-column catalog file, mostly in
270N/A # use on server.
270N/A cv, cat_fmri = entry.split()
270N/A cat_name = fmri.extract_pkg_name(cat_fmri)
270N/A if cat_name not in pkg_names:
270N/A continue
270N/A fmris.append(fmri.PkgFmri(cat_fmri, "5.11",
270N/A authority = self.auth))
215N/A continue
215N/A
215N/A fmris.append(fmri.PkgFmri("%s@%s" %
215N/A (cat_name, cat_version), "5.11",
215N/A authority = self.auth))
157N/A
157N/A pfile.close()
157N/A
157N/A return fmris
157N/A
157N/A def last_modified(self):
157N/A """Return the time at which the catalog was last modified."""
59N/A
59N/A return self.attrs.get("Last-Modified", None)
157N/A
157N/A def load_attrs(self, filenm = "attrs"):
157N/A """Load attributes from the catalog file into the in-memory
157N/A attributes dictionary"""
157N/A
157N/A apath = os.path.normpath(
157N/A os.path.join(self.catalog_root, filenm))
157N/A if not os.path.exists(apath):
157N/A return
157N/A
215N/A afile = file(apath, "r")
195N/A attrre = re.compile('^S ([^:]*): (.*)')
195N/A
195N/A for entry in afile:
270N/A m = attrre.match(entry)
270N/A if m != None:
270N/A self.attrs[m.group(1)] = m.group(2)
270N/A
270N/A afile.close()
270N/A
270N/A # convert npkgs to integer value
270N/A if "npkgs" in self.attrs:
270N/A self.attrs["npkgs"] = int(self.attrs["npkgs"])
270N/A
270N/A @staticmethod
270N/A def build_pkg_names(cat_root):
270N/A """Read the catalog and build the array of fmri pkg names
270N/A that is contained within the catalog. Returns a list
270N/A of strings of package names."""
270N/A
270N/A pkg_names = set()
270N/A ppath = os.path.normpath(os.path.join(cat_root,
270N/A "catalog"))
270N/A
270N/A try:
270N/A pfile = file(ppath, "r")
270N/A except IOError, e:
270N/A if e.errno == errno.ENOENT:
270N/A return pkg_names
270N/A else:
270N/A raise
270N/A
270N/A for entry in pfile:
270N/A try:
270N/A if entry[0] not in tuple("CV"):
270N/A continue
270N/A
270N/A cv, pkg, cat_name, cat_version = entry.split()
270N/A if pkg != "pkg":
270N/A continue
270N/A except ValueError:
270N/A # Handle old two-column catalog file, mostly in
270N/A # use on server.
270N/A cv, cat_fmri = entry.split()
270N/A cat_name = fmri.extract_pkg_name(cat_fmri)
270N/A
270N/A pkg_names.add(cat_name)
270N/A
270N/A pfile.close()
270N/A
270N/A return pkg_names
270N/A
270N/A @staticmethod
270N/A def save_pkg_names(cat_root, pkg_names):
270N/A """Pickle the list of package names in the catalog for faster
270N/A re-loading."""
270N/A
270N/A if not pkg_names:
270N/A return
270N/A
270N/A ppath = os.path.normpath(os.path.join(cat_root,
270N/A "pkg_names.pkl"))
270N/A
270N/A try:
270N/A pfile = file(ppath, "wb")
270N/A except IOError, e:
270N/A if e.errno == errno.EACCES:
270N/A # Don't bother saving, if we don't have
270N/A # permission.
270N/A return
270N/A else:
270N/A raise
270N/A
270N/A cPickle.dump(pkg_names, pfile, -1)
270N/A pfile.close()
270N/A
270N/A @staticmethod
270N/A def load_pkg_names(cat_root):
270N/A """Load pickled list of package names. This function
270N/A may raise an IOError if the file doesn't exist. Callers
270N/A should be sure to catch this exception and rebuild
270N/A the package names, if required."""
258N/A
258N/A ppath = os.path.normpath(os.path.join(cat_root,
258N/A "pkg_names.pkl"))
258N/A
258N/A pfile = file(ppath, "rb")
258N/A pkg_names = cPickle.load(pfile)
258N/A pfile.close()
258N/A
258N/A return pkg_names
258N/A
258N/A def _load_renamed(self):
258N/A """Load the catalog's rename records into self.renamed"""
258N/A
258N/A self.renamed = []
258N/A
258N/A try:
258N/A pfile = file(os.path.normpath(
258N/A os.path.join(self.catalog_root, "catalog")), "r")
258N/A except IOError, e:
258N/A if e.errno == errno.ENOENT:
258N/A return
157N/A else:
157N/A raise
157N/A
157N/A self.renamed = [
157N/A RenamedPackage(*entry.split()[1:]) for entry in pfile
157N/A if entry[0] == "R"
157N/A ]
215N/A
157N/A pfile.close()
157N/A
157N/A def npkgs(self):
157N/A """Returns the number of packages in the catalog."""
157N/A
157N/A return self.attrs["npkgs"]
157N/A
157N/A @staticmethod
157N/A def recv(filep, path):
157N/A """A static method that takes a file-like object and
157N/A a path. This is the other half of catalog.send(). It
157N/A reads a stream as an incoming catalog and lays it down
157N/A on disk."""
220N/A
220N/A if not os.path.exists(path):
220N/A os.makedirs(path)
220N/A
220N/A attrf = file(os.path.normpath(
157N/A os.path.join(path, "attrs")), "w+")
258N/A catf = file(os.path.normpath(
258N/A os.path.join(path, "catalog")), "w+")
157N/A
161N/A for s in filep:
161N/A if not s[1].isspace():
161N/A continue
161N/A elif not s[0] in known_prefixes:
161N/A catf.write(s)
157N/A elif s.startswith("S "):
157N/A attrf.write(s)
157N/A elif s.startswith("R "):
157N/A catf.write(s)
270N/A else:
270N/A # XXX Need to be able to handle old and new
270N/A # format catalogs.
270N/A f = fmri.PkgFmri(s[2:])
258N/A catf.write("%s %s %s %s\n" %
258N/A (s[0], "pkg", f.pkg_name, f.version))
258N/A
258N/A attrf.close()
258N/A catf.close()
258N/A
258N/A # Save a list of package names for easier searching
258N/A pkg_names = Catalog.build_pkg_names(path)
258N/A Catalog.save_pkg_names(path, pkg_names)
258N/A
258N/A def rename_package(self, srcname, srcvers, destname, destvers):
258N/A """Record that the name of package oldname has been changed
258N/A to newname as of version vers. Returns a timestamp
258N/A of when the catalog was modified and a RenamedPackage
258N/A object that describes the rename."""
258N/A
258N/A rr = RenamedPackage(srcname, srcvers, destname, destvers)
258N/A
258N/A # Check that the destination (new) package is already in the
258N/A # catalog. Also check that the old package does not exist at
258N/A # the version that is being renamed.
258N/A if rr.new_fmri():
258N/A newfm = self.get_matching_fmris(rr.new_fmri())
258N/A if len(newfm) < 1:
258N/A raise CatalogException, \
258N/A "Destination FMRI %s must be in catalog" % \
258N/A rr.new_fmri()
258N/A
258N/A oldfm = self.get_matching_fmris(rr.old_fmri())
258N/A if len(oldfm) > 0:
258N/A raise CatalogException, \
258N/A "Src FMRI %s must not be in catalog" % \
258N/A rr.old_fmri()
258N/A
258N/A # Load renamed packages, if needed
258N/A if self.renamed is None:
258N/A self._load_renamed()
258N/A
258N/A # Check that rename record isn't already in catalog
258N/A if rr in self.renamed:
258N/A raise CatalogException, \
258N/A "Rename %s is already in the catalog" % rr
258N/A
258N/A # Keep renames acyclic. Check that the destination of this
258N/A # rename isn't the source of another rename.
258N/A if rr.new_fmri() and \
258N/A self.rename_is_predecessor(rr.new_fmri(), rr.old_fmri()):
258N/A raise RenameException, \
258N/A "Can't rename %s. Causes cycle in rename graph." \
258N/A % rr.srcname
258N/A
258N/A pathstr = os.path.normpath(os.path.join(self.catalog_root,
258N/A "catalog"))
258N/A pfile = file(pathstr, "a+")
258N/A pfile.write("%s\n" % rr)
258N/A pfile.close()
258N/A
258N/A self.renamed.append(rr)
258N/A
258N/A ts = datetime.datetime.now()
258N/A self.set_time(ts)
258N/A
258N/A return (ts, rr)
258N/A
258N/A def rename_is_same_pkg(self, fmri, pfmri):
258N/A """Returns true if fmri and pfmri are the same package because
258N/A of a rename operation."""
258N/A
258N/A for s in self.fmri_renamed_src(fmri):
258N/A if s.destname == pfmri.pkg_name:
258N/A return True
258N/A elif s.new_fmri() and \
258N/A self.rename_is_same_pkg(s.new_fmri(), pfmri):
258N/A return True
258N/A
258N/A for d in self.fmri_renamed_dest(fmri):
258N/A if d.srcname == pfmri.pkg_name:
258N/A return True
258N/A elif self.rename_is_same_pkg(d.old_fmri(), pfmri):
258N/A return True
258N/A
258N/A return False
258N/A
258N/A def rename_is_successor(self, fmri, pfmri):
258N/A """Returns true if fmri is a successor to pfmri by way
258N/A of a rename operation."""
258N/A
258N/A for d in self.fmri_renamed_dest(fmri):
258N/A if d.srcname == pfmri.pkg_name and \
258N/A pfmri.version <= d.srcversion:
258N/A return True
258N/A else:
258N/A return self.rename_is_successor(d.old_fmri(),
258N/A pfmri)
258N/A
258N/A return False
258N/A
258N/A def rename_is_predecessor(self, fmri, pfmri):
258N/A """Returns true if fmri is a predecessor to pfmri by
258N/A a rename operation."""
258N/A
258N/A for s in self.fmri_renamed_src(fmri):
258N/A if s.destname == pfmri.pkg_name and \
258N/A s.destversion < pfmri.version:
258N/A return True
258N/A elif s.new_fmri():
258N/A return self.rename_is_predecessor(s.new_fmri(),
258N/A pfmri)
258N/A
258N/A return False
258N/A
258N/A def rename_newer_pkgs(self, fmri):
258N/A """Returns a list of packages that are newer than fmri."""
258N/A
258N/A pkgs = []
258N/A
258N/A for s in self.fmri_renamed_src(fmri):
258N/A if s.new_fmri():
258N/A pkgs.append(s.new_fmri())
258N/A nl = self.rename_newer_pkgs(s.new_fmri())
258N/A pkgs.extend(nl)
258N/A
258N/A return pkgs
258N/A
258N/A def rename_older_pkgs(self, fmri):
258N/A """Returns a list of packages that are older than fmri."""
157N/A
157N/A pkgs = []
157N/A
157N/A for d in self.fmri_renamed_dest(fmri):
157N/A pkgs.append(d.old_fmri())
157N/A ol = self.rename_older_pkgs(d.old_fmri())
157N/A pkgs.extend(ol)
157N/A
157N/A return pkgs
157N/A
157N/A def save_attrs(self, filenm = "attrs"):
157N/A """Save attributes from the in-memory catalog to a file
157N/A specified by filenm."""
157N/A
157N/A try:
157N/A afile = file(os.path.normpath(
157N/A os.path.join(self.catalog_root, filenm)), "w+")
157N/A except IOError, e:
157N/A # This may get called in a situation where
174N/A # the user does not have write access to the attrs
174N/A # file.
174N/A if e.errno == errno.EACCES:
174N/A return
174N/A else:
174N/A raise
174N/A
174N/A for a in self.attrs.keys():
174N/A s = "S %s: %s\n" % (a, self.attrs[a])
174N/A afile.write(s)
157N/A
157N/A afile.close()
157N/A
157N/A def send(self, filep):
157N/A """Send the contents of this catalog out to the filep
157N/A specified as an argument."""
215N/A
215N/A # Send attributes first.
215N/A filep.writelines(self.attrs_as_lines())
215N/A
215N/A try:
215N/A cfile = file(os.path.normpath(
215N/A os.path.join(self.catalog_root, "catalog")), "r")
215N/A except IOError, e:
215N/A # Missing catalog is fine; other errors need to be
215N/A # reported.
215N/A if e.errno == errno.ENOENT:
215N/A return
45N/A else:
204N/A raise
204N/A
204N/A for e in cfile:
258N/A filep.write(e)
258N/A
258N/A cfile.close()
258N/A
258N/A def set_time(self, ts = None):
258N/A """Set time to timestamp if supplied by caller. Otherwise
258N/A use the system time."""
258N/A
258N/A if ts and isinstance(ts, str):
258N/A self.attrs["Last-Modified"] = ts
258N/A elif ts and isinstance(ts, datetime.datetime):
258N/A self.attrs["Last-Modified"] = ts.isoformat()
258N/A else:
258N/A self.attrs["Last-Modified"] = timestamp()
258N/A
258N/A self.save_attrs()
106N/A
157N/A def search_available(self):
157N/A return self._search_available
157N/A
215N/A def valid_new_fmri(self, fmri):
220N/A """Check that the fmri supplied as an argument would be
258N/A valid to add to the catalog. This checks to make sure that
220N/A rename/freeze operations would not prohibit the caller
215N/A from adding this FMRI."""
215N/A
215N/A if self.renamed is None:
215N/A self._load_renamed()
215N/A
215N/A for rr in self.renamed:
215N/A if rr.srcname == fmri.pkg_name and \
215N/A fmri.version >= rr.srcversion:
215N/A return False
215N/A
215N/A return True
215N/A
215N/A
215N/A# In order to avoid a fine from the Department of Redundancy Department,
215N/A# allow these methods to be invoked without explictly naming the Catalog class.
215N/Arecv = Catalog.recv
215N/A
215N/A# Prefixes that this catalog knows how to handle
215N/Aknown_prefixes = frozenset("CSVR")
215N/A
215N/A# Method used by Catalog and UpdateLog. Since UpdateLog needs to know
215N/A# about Catalog, keep it in Catalog to avoid circular dependency problems.
215N/Adef timestamp():
215N/A """Return an integer timestamp that can be used for comparisons."""
215N/A
215N/A tobj = datetime.datetime.now()
221N/A tstr = tobj.isoformat()
221N/A
221N/A return tstr
221N/A
221N/Adef ts_to_datetime(ts):
221N/A """Take timestamp ts in string isoformat, and convert it to a datetime
221N/A object."""
221N/A
221N/A year = int(ts[0:4])
221N/A month = int(ts[5:7])
221N/A day = int(ts[8:10])
221N/A hour = int(ts[11:13])
221N/A min = int(ts[14:16])
221N/A sec = int(ts[17:19])
221N/A # usec is not in the string if 0
221N/A try:
221N/A usec = int(ts[20:26])
221N/A except ValueError:
221N/A usec = 0
221N/A
221N/A dt = datetime.datetime(year, month, day, hour, min, sec, usec)
221N/A
221N/A return dt
221N/A
221N/A
221N/Adef extract_matching_fmris(pkgs, patterns, matcher = None,
221N/A constraint = None, counthash = None):
221N/A """Iterate through the given list of PkgFmri objects,
221N/A looking for packages matching 'pattern', based on the function
221N/A in 'matcher' and the versioning constraint described by
221N/A 'constraint'. If 'matcher' is None, uses fmri subset matching
221N/A as the default. Returns a sorted list of PkgFmri objects,
221N/A newest versions first. If 'counthash' is a dictionary, instead
221N/A store the number of matched fmris for each package name which
221N/A was matched."""
221N/A
221N/A if not matcher:
221N/A matcher = fmri.fmri_match
221N/A
221N/A if not isinstance(patterns, list):
221N/A patterns = [ patterns ]
221N/A
221N/A # 'pattern' may be a partially or fully decorated fmri; we want
221N/A # to extract its name and version to match separately against
221N/A # the catalog.
221N/A # XXX "5.11" here needs to be saner
221N/A tuples = {}
221N/A
221N/A for pattern in patterns:
221N/A if isinstance(pattern, fmri.PkgFmri):
221N/A tuples[pattern] = pattern.tuple()
221N/A else:
221N/A assert pattern != None
221N/A tuples[pattern] = \
258N/A fmri.PkgFmri(pattern, "5.11").tuple()
258N/A
258N/A ret = []
258N/A
258N/A for p in pkgs:
258N/A cat_auth, cat_name, cat_version = p.tuple()
258N/A
258N/A for pattern in patterns:
258N/A pat_auth, pat_name, pat_version = tuples[pattern]
258N/A if (fmri.is_same_authority(pat_auth, cat_auth) or not \
258N/A pat_auth) and matcher(cat_name, pat_name):
258N/A if not pat_version or \
258N/A p.version.is_successor(
258N/A pat_version, constraint) or \
258N/A p.version == pat_version:
258N/A if counthash is not None:
258N/A if pattern in counthash:
258N/A counthash[pattern] += 1
258N/A else:
258N/A counthash[pattern] = 1
258N/A
258N/A if pat_auth:
258N/A p.set_authority(pat_auth)
258N/A ret.append(p)
258N/A
258N/A return sorted(ret, reverse = True)
258N/A
258N/Aclass RenamedPackage(object):
258N/A """An in-memory representation of a rename object. This object records
258N/A information about a package that has had its name changed.
258N/A
258N/A Renaming a package presents a number of challenges. The packaging
258N/A system must still be able to recognize and decode dependencies on
258N/A packages with the old name. In order for this to work correctly, the
258N/A rename record must contain both the old and new name of the package. It
258N/A is also undesireable to have a renamed package receive subsequent
258N/A versions. However, it still should be possible to publish bugfixes to
258N/A the old package lineage. This means that we must also record
258N/A versioning information at the time a package is renamed.
258N/A
258N/A This versioning information allows us to determine which portions
258N/A of the version and namespace are allowed to add new versions.
258N/A
258N/A If a package is re-named to the NULL package at a specific version,
258N/A this is equivalent to freezing the package. No further updates to
258N/A the version history may be made under that name. (NULL is never open)
258N/A
258N/A The rename catalog format is as follows:
258N/A
258N/A R <srcname> <srcversion> <destname> <destversion>
258N/A """
258N/A
258N/A def __init__(self, srcname, srcversion, destname, destversion):
258N/A """Create a RenamedPackage object. Srcname is the original
258N/A name of the package, destname is the name this package
258N/A will take after the operation is successful.
258N/A
258N/A
258N/A Versionstr is the version at which this change takes place. No
258N/A versions >= version of srcname will be permitted."""
258N/A
258N/A if destname == "NULL":
258N/A self.destname = None
258N/A destversion = None
258N/A else:
258N/A self.destname = destname
258N/A
258N/A self.srcname = srcname
258N/A
258N/A if not srcversion and not destversion:
258N/A raise RenameException, \
258N/A "Must supply a source or destination version"
258N/A elif not srcversion:
258N/A self.srcversion = version.Version(destversion, None)
258N/A self.destversion = self.srcversion
258N/A elif not destversion:
258N/A self.srcversion = version.Version(srcversion, None)
258N/A self.destversion = self.srcversion
258N/A else:
258N/A self.destversion = version.Version(destversion, None)
258N/A self.srcversion = version.Version(srcversion, None)
258N/A
258N/A def __str__(self):
258N/A if not self.destname:
258N/A return "R %s %s NULL NULL" % (self.srcname,
258N/A self.srcversion)
258N/A
258N/A return "R %s %s %s %s" % (self.srcname, self.srcversion,
258N/A self.destname, self.destversion)
258N/A
258N/A def __eq__(self, other):
258N/A """Implementing our own == function allows us to properly
258N/A check whether a rename object is in a list of renamed
258N/A objects."""
258N/A
258N/A if not isinstance(other, RenamedPackage):
258N/A return False
258N/A
258N/A if self.srcname != other.srcname:
258N/A return False
258N/A
258N/A if self.destname != other.destname:
258N/A return False
258N/A
258N/A if self.srcversion != other.srcversion:
258N/A return False
258N/A
258N/A if self.destversion != other.destversion:
return False
return True
def new_fmri(self):
"""Return a FMRI that represents the destination name and
version of the renamed package."""
if not self.destname:
return None
fmstr = "pkg:/%s@%s" % (self.destname, self.destversion)
fm = fmri.PkgFmri(fmstr, None)
return fm
def old_fmri(self):
"""Return a FMRI that represents the most recent version
of the package had it not been renamed."""
fmstr = "pkg:/%s@%s" % (self.srcname, self.srcversion)
fm = fmri.PkgFmri(fmstr, None)
return fm