55
_DNS_REDIRECT_IP = None
57
# matcher used in template rendering functions
58
BASIC_MATCHER = re.compile(r'\$\{([A-Za-z0-9_.]+)\}|\$([A-Za-z0-9_.]+)')
39
61
def _subp(args, data=None, rcs=None, env=None, capture=False, shell=False,
40
logstring=False, decode="replace"):
62
logstring=False, decode="replace", target=None):
68
if target_path(target) != "/":
69
args = ['chroot', target] + list(args)
47
72
LOG.debug(("Running command %s with allowed return codes %s"
48
73
" (shell=%s, capture=%s)"), args, rcs, shell, capture)
345
370
def __enter__(self):
346
371
for p in self.mounts:
347
tpath = os.path.join(self.target, p[1:])
372
tpath = target_path(self.target, p)
348
373
if do_mount(p, tpath, opts='--bind'):
349
374
self.umounts.append(tpath)
351
376
if not self.allow_daemons:
352
377
self.disabled_daemons = disable_daemons_in_root(self.target)
354
target_etc = os.path.join(self.target, "etc")
379
rconf = target_path(self.target, "/etc/resolv.conf")
380
target_etc = os.path.dirname(rconf)
355
381
if self.target != "/" and os.path.isdir(target_etc):
356
382
# never muck with resolv.conf on /
357
383
rconf = os.path.join(target_etc, "resolv.conf")
360
rtd = tempfile.mkdtemp(dir=os.path.dirname(rconf))
386
rtd = tempfile.mkdtemp(dir=target_etc)
361
387
tmp = os.path.join(rtd, "resolv.conf")
362
388
os.rename(rconf, tmp)
363
389
self.rconf_d = rtd
375
401
undisable_daemons_in_root(self.target)
377
403
# if /dev is to be unmounted, udevadm settle (LP: #1462139)
378
if os.path.join(self.target, "dev") in self.umounts:
404
if target_path(self.target, "/dev") in self.umounts:
379
405
subp(['udevadm', 'settle'])
381
407
for p in reversed(self.umounts):
384
rconf = os.path.join(self.target, "etc", "resolv.conf")
410
rconf = target_path(self.target, "/etc/resolv.conf")
385
411
if self.sys_resolvconf and self.rconf_d:
386
412
os.rename(os.path.join(self.rconf_d, "resolv.conf"), rconf)
387
413
shutil.rmtree(self.rconf_d)
415
def subp(self, *args, **kwargs):
416
kwargs['target'] = self.target
417
return subp(*args, **kwargs)
390
class RunInChroot(ChrootableTarget):
391
def __call__(self, args, **kwargs):
392
if self.target != "/":
393
chroot = ["chroot", self.target]
396
return subp(chroot + args, **kwargs)
419
def path(self, path):
420
return target_path(self.target, path)
399
423
def is_exe(fpath):
404
428
def which(program, search=None, target=None):
405
if target is None or os.path.realpath(target) == "/":
429
target = target_path(target)
408
431
if os.path.sep in program:
409
432
# if program had a '/' in it, then do not search PATH
410
433
# 'which' does consider cwd here. (cd / && which bin/ls) = bin/ls
411
434
# so effectively we set cwd to / (or target)
412
if is_exe(os.path.sep.join((target, program,))):
435
if is_exe(target_path(target, program)):
415
438
if search is None:
469
493
def get_architecture(target=None):
471
if target is not None:
472
chroot = ['chroot', target]
473
out, _ = subp(chroot + ['dpkg', '--print-architecture'],
494
out, _ = subp(['dpkg', '--print-architecture'], capture=True,
475
496
return out.strip()
478
499
def has_pkg_available(pkg, target=None):
480
if target is not None:
481
chroot = ['chroot', target]
482
out, _ = subp(chroot + ['apt-cache', 'pkgnames'], capture=True)
500
out, _ = subp(['apt-cache', 'pkgnames'], capture=True, target=target)
483
501
for item in out.splitlines():
484
502
if pkg == item.strip():
507
def get_installed_packages(target=None):
508
(out, _) = subp(['dpkg-query', '--list'], target=target, capture=True)
511
for line in out.splitlines():
513
(state, pkg, other) = line.split(None, 2)
516
if state.startswith("hi") or state.startswith("ii"):
517
pkgs_inst.add(re.sub(":.*", "", pkg))
489
522
def has_pkg_installed(pkg, target=None):
491
if target is not None:
492
chroot = ['chroot', target]
494
out, _ = subp(chroot + ['dpkg-query', '--show', '--showformat',
495
'${db:Status-Abbrev}', pkg],
524
out, _ = subp(['dpkg-query', '--show', '--showformat',
525
'${db:Status-Abbrev}', pkg],
526
capture=True, target=target)
497
527
return out.rstrip() == "ii"
498
528
except ProcessExecutionError:
542
572
"""Use dpkg-query to extract package pkg's version string
543
573
and parse the version string into a dictionary
546
if target is not None:
547
chroot = ['chroot', target]
549
out, _ = subp(chroot + ['dpkg-query', '--show', '--showformat',
576
out, _ = subp(['dpkg-query', '--show', '--showformat',
577
'${Version}', pkg], capture=True, target=target)
552
578
raw = out.rstrip()
553
579
return parse_dpkg_version(raw, name=pkg, semx=semx)
554
580
except ProcessExecutionError:
600
626
if comment.endswith("\n"):
601
627
comment = comment[:-1]
603
marker = os.path.join(target, marker)
629
marker = target_path(target, marker)
604
630
# if marker exists, check if there are files that would make it obsolete
605
listfiles = [os.path.join(target, "etc/apt/sources.list")]
631
listfiles = [target_path(target, "/etc/apt/sources.list")]
606
632
listfiles += glob.glob(
607
os.path.join(target, "etc/apt/sources.list.d/*.list"))
633
target_path(target, "etc/apt/sources.list.d/*.list"))
609
635
if os.path.exists(marker) and not force:
610
636
if len(find_newer(marker, listfiles)) == 0:
846
871
return (isinstance(exc, IOError) and exc.errno == errno.ENOENT)
874
def _lsb_release(target=None):
850
875
fmap = {'Codename': 'codename', 'Description': 'description',
851
876
'Distributor ID': 'id', 'Release': 'release'}
880
out, _ = subp(['lsb_release', '--all'], capture=True, target=target)
881
for line in out.splitlines():
882
fname, _, val = line.partition(":")
884
data[fmap[fname]] = val.strip()
885
missing = [k for k in fmap.values() if k not in data]
887
LOG.warn("Missing fields in lsb_release --all output: %s",
890
except ProcessExecutionError as err:
891
LOG.warn("Unable to get lsb_release --all: %s", err)
892
data = {v: "UNAVAILABLE" for v in fmap.values()}
897
def lsb_release(target=None):
898
if target_path(target) != "/":
899
# do not use or update cache if target is provided
900
return _lsb_release(target)
852
902
global _LSB_RELEASE
853
903
if not _LSB_RELEASE:
856
out, err = subp(['lsb_release', '--all'], capture=True)
857
for line in out.splitlines():
858
fname, tok, val = line.partition(":")
860
data[fmap[fname]] = val.strip()
861
missing = [k for k in fmap.values() if k not in data]
863
LOG.warn("Missing fields in lsb_release --all output: %s",
866
except ProcessExecutionError as e:
867
LOG.warn("Unable to get lsb_release --all: %s", e)
868
data = {v: "UNAVAILABLE" for v in fmap.values()}
904
data = _lsb_release()
870
905
_LSB_RELEASE.update(data)
871
906
return _LSB_RELEASE
896
931
return platform2arch.get(platform.machine(), platform.machine())
934
def basic_template_render(content, params):
935
"""This does simple replacement of bash variable like templates.
937
It identifies patterns like ${a} or $a and can also identify patterns like
938
${a.b} or $a.b which will look for a key 'b' in the dictionary rooted
944
replacer used in regex match to replace content
946
# Only 1 of the 2 groups will actually have a valid entry.
947
name = match.group(1)
949
name = match.group(2)
951
raise RuntimeError("Match encountered but no valid group present")
952
path = collections.deque(name.split("."))
953
selected_params = params
956
if not isinstance(selected_params, dict):
957
raise TypeError("Can not traverse into"
958
" non-dictionary '%s' of type %s while"
959
" looking for subkey '%s'"
961
selected_params.__class__.__name__,
963
selected_params = selected_params[key]
965
if not isinstance(selected_params, dict):
966
raise TypeError("Can not extract key '%s' from non-dictionary"
968
% (key, selected_params,
969
selected_params.__class__.__name__))
970
return str(selected_params[key])
972
return BASIC_MATCHER.sub(replacer, content)
975
def render_string(content, params):
977
render a string following replacement rules as defined in
978
basic_template_render returning the string
982
return basic_template_render(content, params)
985
def is_resolvable(name):
986
"""determine if a url is resolvable, return a boolean
987
This also attempts to be resilent against dns redirection.
989
Note, that normal nsswitch resolution is used here. So in order
990
to avoid any utilization of 'search' entries in /etc/resolv.conf
991
we have to append '.'.
993
The top level 'invalid' domain is invalid per RFC. And example.com
994
should also not exist. The random entry will be resolved inside
997
global _DNS_REDIRECT_IP
998
if _DNS_REDIRECT_IP is None:
1000
badnames = ("does-not-exist.example.com.", "example.invalid.")
1002
for iname in badnames:
1004
result = socket.getaddrinfo(iname, None, 0, 0,
1006
socket.AI_CANONNAME)
1007
badresults[iname] = []
1008
for (_, _, _, cname, sockaddr) in result:
1009
badresults[iname].append("%s: %s" % (cname, sockaddr[0]))
1010
badips.add(sockaddr[0])
1011
except (socket.gaierror, socket.error):
1013
_DNS_REDIRECT_IP = badips
1015
LOG.debug("detected dns redirection: %s", badresults)
1018
result = socket.getaddrinfo(name, None)
1019
# check first result's sockaddr field
1020
addr = result[0][4][0]
1021
if addr in _DNS_REDIRECT_IP:
1022
LOG.debug("dns %s in _DNS_REDIRECT_IP", name)
1024
LOG.debug("dns %s resolved to '%s'", name, result)
1026
except (socket.gaierror, socket.error):
1027
LOG.debug("dns %s failed to resolve", name)
1031
def is_resolvable_url(url):
1032
"""determine if this url is resolvable (existing or ip)."""
1033
return is_resolvable(urlparse(url).hostname)
1036
def target_path(target, path=None):
1037
# return 'path' inside target, accepting target as None
1038
if target in (None, ""):
1040
elif not isinstance(target, string_types):
1041
raise ValueError("Unexpected input for target: %s" % target)
1043
target = os.path.abspath(target)
1044
# abspath("//") returns "//" specifically for 2 slashes.
1045
if target.startswith("//"):
1051
# os.path.join("/etc", "/foo") returns "/foo". Chomp all leading /.
1052
while len(path) and path[0] == "/":
1055
return os.path.join(target, path)
898
1058
# vi: ts=4 expandtab syntax=python