|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import absolute_import, print_function |
|
|
|
import glob |
|
import logging |
|
import os.path |
|
import re |
|
import shutil |
|
import time |
|
|
|
import apt_pkg |
|
from .distinfo import DistInfo |
|
|
|
|
|
|
|
|
|
|
|
__all__ = ['is_mirror', 'SourceEntry', 'NullMatcher', 'SourcesList', |
|
'SourceEntryMatcher'] |
|
|
|
|
|
def is_mirror(master_uri, compare_uri): |
|
""" check if the given add_url is idential or a mirror of orig_uri e.g.: |
|
master_uri = archive.ubuntu.com |
|
compare_uri = de.archive.ubuntu.com |
|
-> True |
|
""" |
|
|
|
compare_uri = compare_uri.rstrip("/ ") |
|
master_uri = master_uri.rstrip("/ ") |
|
|
|
if compare_uri == master_uri: |
|
|
|
return True |
|
|
|
|
|
try: |
|
compare_srv = compare_uri.split("//")[1] |
|
master_srv = master_uri.split("//")[1] |
|
|
|
except IndexError: |
|
|
|
return False |
|
|
|
if "." in compare_srv and \ |
|
compare_srv[compare_srv.index(".") + 1:] == master_srv: |
|
|
|
return True |
|
return False |
|
|
|
|
|
def uniq(s): |
|
""" simple and efficient way to return uniq collection |
|
|
|
This is not intended for use with a SourceList. It is provided |
|
for internal use only. It does not have a leading underscore to |
|
not break any old code that uses it; but it should not be used |
|
in new code (and is not listed in __all__).""" |
|
return list(set(s)) |
|
|
|
|
|
class SourceEntry(object): |
|
""" single sources.list entry """ |
|
|
|
def __init__(self, line, file=None): |
|
self.invalid = False |
|
self.disabled = False |
|
self.type = "" |
|
self.architectures = [] |
|
self.trusted = None |
|
self.uri = "" |
|
self.dist = "" |
|
self.comps = [] |
|
self.comment = "" |
|
self.line = line |
|
if file is None: |
|
file = apt_pkg.config.find_dir( |
|
"Dir::Etc") + apt_pkg.config.find("Dir::Etc::sourcelist") |
|
self.file = file |
|
self.parse(line) |
|
self.template = None |
|
self.children = [] |
|
|
|
def __eq__(self, other): |
|
""" equal operator for two sources.list entries """ |
|
return (self.disabled == other.disabled and |
|
self.type == other.type and |
|
self.uri.rstrip('/') == other.uri.rstrip('/') and |
|
self.dist == other.dist and |
|
self.comps == other.comps) |
|
|
|
def mysplit(self, line): |
|
""" a split() implementation that understands the sources.list |
|
format better and takes [] into account (for e.g. cdroms) """ |
|
line = line.strip() |
|
pieces = [] |
|
tmp = "" |
|
|
|
p_found = False |
|
space_found = False |
|
for i in range(len(line)): |
|
if line[i] == "[": |
|
if space_found: |
|
space_found = False |
|
p_found = True |
|
pieces.append(tmp) |
|
tmp = line[i] |
|
else: |
|
p_found = True |
|
tmp += line[i] |
|
elif line[i] == "]": |
|
p_found = False |
|
tmp += line[i] |
|
elif space_found and not line[i].isspace(): |
|
|
|
space_found = False |
|
pieces.append(tmp) |
|
tmp = line[i] |
|
elif line[i].isspace() and not p_found: |
|
|
|
space_found = True |
|
else: |
|
tmp += line[i] |
|
|
|
if len(tmp) > 0: |
|
pieces.append(tmp) |
|
return pieces |
|
|
|
def parse(self, line): |
|
""" parse a given sources.list (textual) line and break it up |
|
into the field we have """ |
|
self.line = line |
|
line = line.strip() |
|
|
|
if line == "" or line == "#": |
|
self.invalid = True |
|
return |
|
if line[0] == "#": |
|
self.disabled = True |
|
pieces = line[1:].strip().split() |
|
|
|
if not pieces[0] in ("rpm", "rpm-src", "deb", "deb-src"): |
|
self.invalid = True |
|
return |
|
else: |
|
line = line[1:] |
|
|
|
i = line.find("#") |
|
if i > 0: |
|
self.comment = line[i + 1:] |
|
line = line[:i] |
|
|
|
pieces = self.mysplit(line) |
|
|
|
if len(pieces) < 3: |
|
self.invalid = True |
|
return |
|
|
|
self.type = pieces[0].strip() |
|
|
|
if self.type not in ("deb", "deb-src", "rpm", "rpm-src"): |
|
self.invalid = True |
|
return |
|
|
|
if pieces[1].strip()[0] == "[": |
|
options = pieces.pop(1).strip("[]").split() |
|
for option in options: |
|
try: |
|
key, value = option.split("=", 1) |
|
except Exception: |
|
self.invalid = True |
|
else: |
|
if key == "arch": |
|
self.architectures = value.split(",") |
|
elif key == "trusted": |
|
self.trusted = apt_pkg.string_to_bool(value) |
|
else: |
|
self.invalid = True |
|
|
|
|
|
self.uri = pieces[1].strip() |
|
if len(self.uri) < 1: |
|
self.invalid = True |
|
|
|
|
|
self.dist = pieces[2].strip() |
|
if len(pieces) > 3: |
|
|
|
self.comps = pieces[3:] |
|
else: |
|
self.comps = [] |
|
|
|
def set_enabled(self, new_value): |
|
""" set a line to enabled or disabled """ |
|
self.disabled = not new_value |
|
|
|
if new_value: |
|
self.line = self.line.lstrip().lstrip('#') |
|
else: |
|
|
|
if self.line.strip()[0] != "#": |
|
self.line = "#" + self.line |
|
|
|
def __str__(self): |
|
""" debug helper """ |
|
return self.str().strip() |
|
|
|
def str(self): |
|
""" return the current line as string """ |
|
if self.invalid: |
|
return self.line |
|
line = "" |
|
if self.disabled: |
|
line = "# " |
|
|
|
line += self.type |
|
|
|
if self.architectures and self.trusted is not None: |
|
line += " [arch=%s trusted=%s]" % ( |
|
",".join(self.architectures), "yes" if self.trusted else "no") |
|
elif self.trusted is not None: |
|
line += " [trusted=%s]" % ("yes" if self.trusted else "no") |
|
elif self.architectures: |
|
line += " [arch=%s]" % ",".join(self.architectures) |
|
line += " %s %s" % (self.uri, self.dist) |
|
if len(self.comps) > 0: |
|
line += " " + " ".join(self.comps) |
|
if self.comment != "": |
|
line += " #" + self.comment |
|
line += "\n" |
|
return line |
|
|
|
|
|
class NullMatcher(object): |
|
""" a Matcher that does nothing """ |
|
|
|
def match(self, s): |
|
return True |
|
|
|
|
|
class SourcesList(object): |
|
""" represents the full sources.list + sources.list.d file """ |
|
|
|
def __init__(self, |
|
withMatcher=True, |
|
matcherPath="/usr/share/python-apt/templates/"): |
|
self.list = [] |
|
if withMatcher: |
|
self.matcher = SourceEntryMatcher(matcherPath) |
|
else: |
|
self.matcher = NullMatcher() |
|
self.refresh() |
|
|
|
def refresh(self): |
|
""" update the list of known entries """ |
|
self.list = [] |
|
|
|
file = apt_pkg.config.find_file("Dir::Etc::sourcelist") |
|
if os.path.exists(file): |
|
self.load(file) |
|
|
|
partsdir = apt_pkg.config.find_dir("Dir::Etc::sourceparts") |
|
for file in glob.glob("%s/*.list" % partsdir): |
|
self.load(file) |
|
|
|
for source in self.list: |
|
if not source.invalid: |
|
self.matcher.match(source) |
|
|
|
def __iter__(self): |
|
""" simple iterator to go over self.list, returns SourceEntry |
|
types """ |
|
for entry in self.list: |
|
yield entry |
|
|
|
def __find(self, *predicates, **attrs): |
|
uri = attrs.pop('uri', None) |
|
for source in self.list: |
|
if uri and uri.rstrip('/') != source.uri.rstrip('/'): |
|
continue |
|
if (all(getattr(source, key) == attrs[key] for key in attrs) and |
|
all(predicate(source) for predicate in predicates)): |
|
yield source |
|
|
|
def add(self, type, uri, dist, orig_comps, comment="", pos=-1, file=None, |
|
architectures=[]): |
|
""" |
|
Add a new source to the sources.list. |
|
The method will search for existing matching repos and will try to |
|
reuse them as far as possible |
|
""" |
|
|
|
type = type.strip() |
|
disabled = type.startswith("#") |
|
if disabled: |
|
type = type[1:].lstrip() |
|
architectures = set(architectures) |
|
|
|
|
|
comps = orig_comps[:] |
|
sources = self.__find(lambda s: set(s.architectures) == architectures, |
|
disabled=disabled, invalid=False, type=type, |
|
uri=uri, dist=dist) |
|
|
|
for source in sources: |
|
for new_comp in comps: |
|
if new_comp in source.comps: |
|
|
|
|
|
del comps[comps.index(new_comp)] |
|
if len(comps) == 0: |
|
return source |
|
|
|
sources = self.__find(lambda s: set(s.architectures) == architectures, |
|
invalid=False, type=type, uri=uri, dist=dist) |
|
for source in sources: |
|
if source.disabled == disabled: |
|
|
|
|
|
if set(source.comps) != set(comps): |
|
source.comps = uniq(source.comps + comps) |
|
return source |
|
elif source.disabled and not disabled: |
|
|
|
if set(source.comps) == set(comps): |
|
source.disabled = False |
|
return source |
|
|
|
parts = [ |
|
"#" if disabled else "", |
|
type, |
|
("[arch=%s]" % ",".join(architectures)) if architectures else "", |
|
uri, |
|
dist, |
|
] |
|
parts.extend(comps) |
|
if comment: |
|
parts.append("#" + comment) |
|
line = " ".join(part for part in parts if part) + "\n" |
|
|
|
new_entry = SourceEntry(line) |
|
if file is not None: |
|
new_entry.file = file |
|
self.matcher.match(new_entry) |
|
if pos < 0: |
|
self.list.append(new_entry) |
|
else: |
|
self.list.insert(pos, new_entry) |
|
return new_entry |
|
|
|
def remove(self, source_entry): |
|
""" remove the specified entry from the sources.list """ |
|
self.list.remove(source_entry) |
|
|
|
def restore_backup(self, backup_ext): |
|
" restore sources.list files based on the backup extension " |
|
file = apt_pkg.config.find_file("Dir::Etc::sourcelist") |
|
if os.path.exists(file + backup_ext) and os.path.exists(file): |
|
shutil.copy(file + backup_ext, file) |
|
|
|
partsdir = apt_pkg.config.find_dir("Dir::Etc::sourceparts") |
|
for file in glob.glob("%s/*.list" % partsdir): |
|
if os.path.exists(file + backup_ext): |
|
shutil.copy(file + backup_ext, file) |
|
|
|
def backup(self, backup_ext=None): |
|
""" make a backup of the current source files, if no backup extension |
|
is given, the current date/time is used (and returned) """ |
|
already_backuped = set() |
|
if backup_ext is None: |
|
backup_ext = time.strftime("%y%m%d.%H%M") |
|
for source in self.list: |
|
if (source.file not in already_backuped and |
|
os.path.exists(source.file)): |
|
shutil.copy(source.file, "%s%s" % (source.file, backup_ext)) |
|
return backup_ext |
|
|
|
def load(self, file): |
|
""" (re)load the current sources """ |
|
try: |
|
with open(file, "r") as f: |
|
for line in f: |
|
source = SourceEntry(line, file) |
|
self.list.append(source) |
|
except Exception: |
|
logging.warning("could not open file '%s'\n" % file) |
|
|
|
def save(self): |
|
""" save the current sources """ |
|
files = {} |
|
|
|
if len(self.list) == 0: |
|
path = apt_pkg.config.find_file("Dir::Etc::sourcelist") |
|
header = ( |
|
"## See sources.list(5) for more information, especialy\n" |
|
"# Remember that you can only use http, ftp or file URIs\n" |
|
"# CDROMs are managed through the apt-cdrom tool.\n") |
|
|
|
with open(path, "w") as f: |
|
f.write(header) |
|
return |
|
|
|
try: |
|
for source in self.list: |
|
if source.file not in files: |
|
files[source.file] = open(source.file, "w") |
|
files[source.file].write(source.str()) |
|
finally: |
|
for f in files: |
|
files[f].close() |
|
|
|
def check_for_relations(self, sources_list): |
|
"""get all parent and child channels in the sources list""" |
|
parents = [] |
|
used_child_templates = {} |
|
for source in sources_list: |
|
|
|
if source.template is None: |
|
continue |
|
|
|
|
|
if source.template.child: |
|
key = source.template |
|
if key not in used_child_templates: |
|
used_child_templates[key] = [] |
|
temp = used_child_templates[key] |
|
temp.append(source) |
|
else: |
|
|
|
if len(source.template.children) > 0: |
|
parents.append(source) |
|
|
|
|
|
return (parents, used_child_templates) |
|
|
|
|
|
class SourceEntryMatcher(object): |
|
""" matcher class to make a source entry look nice |
|
lots of predefined matchers to make it i18n/gettext friendly |
|
""" |
|
|
|
def __init__(self, matcherPath): |
|
self.templates = [] |
|
|
|
spec_files = glob.glob("%s/*.info" % matcherPath) |
|
for f in spec_files: |
|
f = os.path.basename(f) |
|
i = f.find(".info") |
|
f = f[0:i] |
|
dist = DistInfo(f, base_dir=matcherPath) |
|
for template in dist.templates: |
|
if template.match_uri is not None: |
|
self.templates.append(template) |
|
return |
|
|
|
def match(self, source): |
|
"""Add a matching template to the source""" |
|
found = False |
|
for template in self.templates: |
|
if (re.search(template.match_uri, source.uri) and |
|
re.match(template.match_name, source.dist) and |
|
|
|
|
|
(source.type == template.type or template.type == "deb")): |
|
found = True |
|
source.template = template |
|
break |
|
elif (template.is_mirror(source.uri) and |
|
re.match(template.match_name, source.dist)): |
|
found = True |
|
source.template = template |
|
break |
|
return found |
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
apt_pkg.init_config() |
|
sources = SourcesList() |
|
|
|
for entry in sources: |
|
logging.info("entry %s" % entry.str()) |
|
|
|
|
|
mirror = is_mirror("http://archive.ubuntu.com/ubuntu/", |
|
"http://de.archive.ubuntu.com/ubuntu/") |
|
logging.info("is_mirror(): %s" % mirror) |
|
|
|
logging.info(is_mirror("http://archive.ubuntu.com/ubuntu", |
|
"http://de.archive.ubuntu.com/ubuntu/")) |
|
logging.info(is_mirror("http://archive.ubuntu.com/ubuntu/", |
|
"http://de.archive.ubuntu.com/ubuntu")) |
|
|