/usr/share/cfengine3/modules/packages/zypper is in cfengine3 3.10.2-4build1.
This file is owned by root:root, with mode 0o755.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 | #!/usr/bin/python
#####################################################################################
# Copyright 2016 Normation SAS
#####################################################################################
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#####################################################################################
# This script is based on the CFEngine's masterfiles yum script:
# https://github.com/cfengine/masterfiles/blob/master/modules/packages/yum
# Licensed under:
# MIT Public License
# Copyright 2017 Northern.tech AS
import sys
import os
import subprocess
import re
rpm_cmd = os.environ.get('CFENGINE_TEST_RPM_CMD', "/bin/rpm")
rpm_quiet_option = ["--quiet"]
rpm_output_format = "Name=%{name}\nVersion=%{version}-%{release}\nArchitecture=%{arch}\n"
zypper_cmd = os.environ.get('CFENGINE_TEST_ZYPPER_CMD', "/usr/bin/zypper")
zypper_options = ["--quiet", "-n"]
NULLFILE = open(os.devnull, 'w')
# Suse zypper "--oldpackage" option is only supported greater that 1.6.169
import rpm
from distutils.version import StrictVersion
ZYPPER_SUPPORTS_OLDPACKAGE=False
package_zypper = rpm.TransactionSet().dbMatch('name', "zypper").next()
if StrictVersion(package_zypper['version']) >= StrictVersion("1.6.169"):
ZYPPER_SUPPORTS_OLDPACKAGE=True
redirection_is_broken_cached = -1
def redirection_is_broken():
# Older versions of Python have a bug where it is impossible to redirect
# stderr using subprocess, and any attempt at redirecting *anything*, not
# necessarily stderr, will result in it being closed instead. This is very
# bad, because RPM may then open its RPM database on file descriptor 2
# (stderr), and will cause it to output error messages directly into the
# database file. Fortunately "stdout=subprocess.PIPE" doesn't have the bug,
# and that's good, because it would have been much more tricky to solve.
global redirection_is_broken_cached
if redirection_is_broken_cached == -1:
cmd_line = [sys.argv[0], "internal-test-stderr"]
if subprocess.call(cmd_line, stdout=sys.stderr) == 0:
redirection_is_broken_cached = 0
else:
redirection_is_broken_cached = 1
return redirection_is_broken_cached
def subprocess_Popen(cmd, stdout=None, stderr=None):
if not redirection_is_broken() or (stdout is None and stderr is None) or stdout == subprocess.PIPE or stderr == subprocess.PIPE:
return subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
old_stdout_fd = -1
old_stderr_fd = -1
if stdout is not None:
old_stdout_fd = os.dup(1)
os.dup2(stdout.fileno(), 1)
if stderr is not None:
old_stderr_fd = os.dup(2)
os.dup2(stderr.fileno(), 2)
result = subprocess.Popen(cmd)
if old_stdout_fd >= 0:
os.dup2(old_stdout_fd, 1)
os.close(old_stdout_fd)
if old_stderr_fd >= 0:
os.dup2(old_stderr_fd, 2)
os.close(old_stderr_fd)
return result
def subprocess_call(cmd, stdout=None, stderr=None):
process = subprocess_Popen(cmd, stdout, stderr)
return process.wait()
def get_package_data():
pkg_string = ""
for line in sys.stdin:
if line.startswith("File="):
pkg_string = line.split("=", 1)[1].rstrip()
# Don't break, we need to exhaust stdin.
if not pkg_string:
return 1
if pkg_string.startswith("/"):
# Absolute file.
sys.stdout.write("PackageType=file\n")
sys.stdout.flush()
return subprocess_call([rpm_cmd, "--qf", rpm_output_format, "-qp", pkg_string])
elif re.search("[:,]", pkg_string):
# Contains an illegal symbol.
sys.stdout.write(line + "ErrorMessage: Package string with illegal format\n")
return 1
else:
sys.stdout.write("PackageType=repo\n")
sys.stdout.write("Name=" + pkg_string + "\n")
return 0
def list_installed():
# Ignore everything.
sys.stdin.readlines()
return subprocess_call([rpm_cmd, "-qa", "--qf", rpm_output_format])
def list_updates(online):
# Ignore everything.
sys.stdin.readlines()
online_flag = []
if not online:
online_flag = ["--no-refresh"]
process = subprocess_Popen([zypper_cmd] + zypper_options + online_flag + ["list-updates"], stdout=subprocess.PIPE)
lastline = ""
for line in process.stdout:
# Zypper's output looks like:
#
# S | Repository | Name | Current Version | Available Version | Arch
# --+-------------------+--------------+------------------------------------+------------------------------------------------+-------
# v | Rudder repository | rudder-agent | 1398866025:3.2.6.release-1.SLES.11 | 1398866025:3.2.7.rc1.git201609190419-1.SLES.11 | x86_64
#
# Which gives:
#
# v | Rudder repository | rudder-agent | 1398866025:3.2.6.release-1.SLES.11 | 1398866025:3.2.7.rc1.git201609190419-1.SLES.11 | x86_64
# may contain package name old version, ignore it version available architecture
# special chars
# v\s+\|[^\|]+\ |\s+(?P<name>\S+)\s+\|\s+\S+\s+\ |\s+(?P<version>\S+)\s+\ |\s+(?P<arch>\S+)\s*$
# The first char will always be "v" which means there is a new version avaialble on search outputs.
match = re.match("v\s+\|[^\|]+\|\s+(?P<name>\S+)\s+\|\s+\S+\s+\|\s+(?P<version>\S+)\s+\|\s+(?P<arch>\S+)\s*$", line)
if match is not None:
sys.stdout.write("Name=" + match.group("name") + "\n")
sys.stdout.write("Version=" + match.group("version") + "\n")
sys.stdout.write("Architecture=" + match.group("arch") + "\n")
return 0
# Returns a pair:
# List 1: Contains arguments for a single command line.
# List 2: Contains arguments for multiple command lines (see comments in
# repo_install()).
def one_package_argument(name, arch, version, is_zypper_install):
args = []
archs = []
exists = False
if arch:
archs.append(arch)
if is_zypper_install:
process = subprocess_Popen([rpm_cmd, "--qf", "%{arch}\n",
"-q", name], stdout=subprocess.PIPE)
existing_archs = [line.rstrip() for line in process.stdout]
process.wait()
if process.returncode == 0 and existing_archs:
exists = True
if not arch:
# Here we have no specified architecture and we are
# installing. If we have existing versions, operate
# on those, instead of the platform default.
archs += existing_archs
version_suffix = ""
if version:
version_suffix = "=" + version
if archs:
args += [name + "." + arch + version_suffix for arch in archs]
else:
args.append(name + version_suffix)
if exists and version:
return [], args
else:
return args, []
# Returns a pair:
# List 1: Contains arguments for a single command line.
# List 2: Contains arguments for multiple command lines (see comments in
# repo_install()). This is a list of lists, where the logic is:
# list
# | +---- package1:amd64 -+
# +- sublist ---+ +--- Do these together
# | +---- package1:i386 -+
# |
# |
# | +---- package2:amd64 -+
# +- sublist ---+ +--- And these together
# +---- package2:i386 -+
def package_arguments_builder(is_zypper_install):
name = ""
version = ""
arch = ""
single_cmd_args = [] # List of arguments
multi_cmd_args = [] # List of lists of arguments
old_name = ""
for line in sys.stdin:
if line.startswith("Name="):
if name:
# Each new "Name=" triggers a new entry.
single_list, multi_list = one_package_argument(name, arch, version, is_zypper_install)
single_cmd_args += single_list
if name == old_name:
# Packages that differ only by architecture should be
# processed together
multi_cmd_args[-1] += multi_list
elif multi_list:
# Otherwise we process them individually.
multi_cmd_args += [multi_list]
version = ""
arch = ""
old_name = name
name = line.split("=", 1)[1].rstrip()
elif line.startswith("Version="):
version = line.split("=", 1)[1].rstrip()
elif line.startswith("Architecture="):
arch = line.split("=", 1)[1].rstrip()
if name:
single_list, multi_list = one_package_argument(name, arch, version, is_zypper_install)
single_cmd_args += single_list
if name == old_name:
# Packages that differ only by architecture should be
# processed together
multi_cmd_args[-1] += multi_list
elif multi_list:
# Otherwise we process them individually.
multi_cmd_args += [multi_list]
return single_cmd_args, multi_cmd_args
def repo_install():
# Due to how zypper works we need to split repo installs into several
# components.
#
# 1. Installation of fresh packages is easy, we add all of them on one
# command line.
# 2. Upgrade of existing packages where no version has been specified is
# also easy, we add that to the same command line.
# 3. Up/downgrade of existing packages where version is specified is
# tricky, for several reasons:
# a) There is no one zypper command that will do both, "install" or
# "upgrade" will only upgrade, and "downgrade" will only downgrade.
# b) There is no way rpm or zypper will tell you which version is higher
# than the other, and we know from experience with the old package
# promise implementation that we don't want to try to do such a
# comparison ourselves.
# c) zypper has no dry-run mode, so we cannot tell in advance which
# operation will succeed.
# d) zypper will not even tell you whether operation succeeded when you
# run it for real
#
# So here's what we need to do. We start by querying each package to find
# out whether that exact version is installed. If it fulfills 1. or 2. we
# add it to that single command line.
#
# If we end up at 3. we need to split the work and do each package
# separately. We do:
#
# 1. Try to upgrade using "zypper upgrade".
# 2. Query the package again, see if it is the right version now.
# 3. If not, try to downgrade using "zypper downgrade".
# 4. Query the package again, see if it is the right version now.
# 5. Final safeguard, try installing using "zypper install". This may happen
# in case we have one architecture already, but we are installing a
# second one. In this case only install will work.
# 6. (No need to check again, CFEngine will do the final check)
#
# This is considerably more expensive than what we do for apt, but it's the
# only way to cover all bases. In apt it will be one apt call for any number
# of packages, with zypper it will in the worst case be:
# 1 + 5 * number_of_packages
# although a more common case will probably be:
# 1 + 2 * number_of_packages
# since it's unlikely that people will do a whole lot of downgrades
# simultaneously.
ret = 0
single_cmd_args, multi_cmd_args = package_arguments_builder(True)
if single_cmd_args:
cmd_line = [zypper_cmd] + zypper_options + ["install"]
if ZYPPER_SUPPORTS_OLDPACKAGE:
cmd_line += ["--oldpackage"]
cmd_line.extend(single_cmd_args)
ret = subprocess_call(cmd_line, stdout=NULLFILE)
if multi_cmd_args:
for block in multi_cmd_args:
# Try to upgrade.
cmd_line = [zypper_cmd] + zypper_options + ["update"] + block
subprocess_call(cmd_line, stdout=NULLFILE)
# See if it succeeded.
success = True
for item in block:
cmd_line = [rpm_cmd] + rpm_quiet_option + ["-q", item]
if subprocess_call(cmd_line, stdout=NULLFILE) != 0:
success = False
break
if success:
continue
# Try to plain install.
cmd_line = [zypper_cmd] + zypper_options + ["install"]
if ZYPPER_SUPPORTS_OLDPACKAGE:
cmd_line += ["--oldpackage"]
cmd_line += block
subprocess_call(cmd_line, stdout=NULLFILE)
# No final check. CFEngine will figure out that it's missing
# if it failed.
# ret == 0 doesn't mean we succeeded with everything, but it's expensive to
# check, so let CFEngine do that.
return ret
def remove():
cmd_line = [zypper_cmd] + zypper_options + ["remove"]
# package_arguments_builder will always return empty second element in case
# of removals, so just drop it. |
# V
args = package_arguments_builder(False)[0]
if args:
return subprocess_call(cmd_line + args, stdout=NULLFILE)
return 0
def file_install():
cmd_line = [rpm_cmd] + rpm_quiet_option + ["--force", "-U"]
found = False
for line in sys.stdin:
if line.startswith("File="):
found = True
cmd_line.append(line.split("=", 1)[1].rstrip())
if not found:
return 0
return subprocess_call(cmd_line, stdout=NULLFILE)
def main():
if len(sys.argv) < 2:
sys.stderr.write("Need to provide argument\n")
return 2
if sys.argv[1] == "internal-test-stderr":
# This will cause an exception if stderr is closed.
try:
os.fstat(2)
except OSError:
return 1
return 0
elif sys.argv[1] == "supports-api-version":
sys.stdout.write("1\n")
return 0
elif sys.argv[1] == "get-package-data":
return get_package_data()
elif sys.argv[1] == "list-installed":
return list_installed()
elif sys.argv[1] == "list-updates":
return list_updates(True)
elif sys.argv[1] == "list-updates-local":
return list_updates(False)
elif sys.argv[1] == "repo-install":
return repo_install()
elif sys.argv[1] == "remove":
return remove()
elif sys.argv[1] == "file-install":
return file_install()
else:
sys.stderr.write("Invalid operation\n")
return 2
sys.exit(main())
|