Your IP : 18.220.134.161
#
# Copyright (C) 2015 Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties 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, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
from __future__ import absolute_import
from __future__ import unicode_literals
from dnfpluginscore import _, logger
import dnf
import dnf.exceptions
import errno
import json
import librepo
import os
from copy import copy
from dnf.conf.config import PRIO_PLUGINCONFIG
import up2date_client.up2dateAuth
import up2date_client.config
import up2date_client.rhnChannel
import up2date_client.rhnPackageInfo
from rhn.i18n import ustr
from up2date_client import up2dateErrors
STORED_CHANNELS_NAME = '_spacewalk.json'
RHN_DISABLED = _("CloudLinux Network based repositories will be disabled.")
CHANNELS_DISABLED = _("CloudLinux Network channel support will be disabled.")
COMMUNICATION_ERROR = _("There was an error communicating with CloudLinux Network server.")
NOT_REGISTERED_ERROR = _("This system is not registered with CloudLinux Network server.")
NOT_SUBSCRIBED_ERROR = _("This system is not subscribed to any channels.")
NO_SYSTEM_ID_ERROR = _("SystemId could not be acquired.")
USE_RHNREGISTER = _("You can use rhn_register to register.")
UPDATES_FROM_SPACEWALK = _("This system is receiving updates from CloudLinux Network server.")
GPG_KEY_REJECTED = _("For security reasons packages from CloudLinux Network based repositories can be verified only with locally installed gpg keys. GPG key '%s' has been rejected.")
PROFILE_NOT_SENT = _("Package profile information could not be sent.")
MISSING_HEADER = _("Missing required login information for CloudLinux Network: %s")
LEAPP_IN_PROGRESS = _("Leapp upgrade is running - using cache.")
MUST_BE_ROOT = _('Spacewalk plugin has to be run under with the root privileges.')
class Spacewalk(dnf.Plugin):
name = 'spacewalk'
def __init__(self, base, cli):
super(Spacewalk, self).__init__(base, cli)
self.base = base
self.cli = cli
self.stored_channels_path = os.path.join(self.base.conf.persistdir,
STORED_CHANNELS_NAME)
self.connected_to_spacewalk = False
self.up2date_cfg = {}
self.conf = copy(self.base.conf)
self.parser = self.read_config(self.conf)
if "main" in self.parser.sections():
options = self.parser.items("main")
for (key, value) in options:
self.conf._set_value(key, value, PRIO_PLUGINCONFIG)
if not dnf.util.am_i_root():
logger.warning(MUST_BE_ROOT)
self.conf.enabled = False
if not self.conf.enabled:
return
logger.debug('initialized Spacewalk plugin')
self.activate_channels()
def config(self):
if not self.conf.enabled:
return
self.cli.demands.root_user = True
def clnreg(self):
os.system('/usr/sbin/clnreg_ks --strict-edition')
def activate_channels(self, networking=True):
enabled_channels = {}
sslcacert = None
force_http = 0
proxy_url = None
login_info = None
cached_channels = self._read_channels_file()
if not networking:
# no network communication, use list of channels from persistdir
enabled_channels = cached_channels
elif os.path.isfile("/etc/cln_leapp_in_progress"):
# networking is true, but CLN urls won't be accessible, use cache
logger.warning(LEAPP_IN_PROGRESS)
enabled_channels = cached_channels
else:
# setup proxy according to up2date
self.up2date_cfg = up2date_client.config.initUp2dateConfig()
sslcacert = get_ssl_ca_cert(self.up2date_cfg)
force_http = self.up2date_cfg['useNoSSLForPackages'],
# trying to register system once in case of error while getLoginInfo
clnreg_tried = False
while not clnreg_tried:
try:
login_info = up2date_client.up2dateAuth.getLoginInfo(timeout=self.conf.timeout)
clnreg_tried = True
except up2dateErrors.RhnServerException as e:
if clnreg_tried == False:
self.clnreg()
clnreg_tried = True
continue
logger.error("%s\n%s\n%s", COMMUNICATION_ERROR, RHN_DISABLED, e)
return
if not login_info:
logger.error("%s\n%s", NOT_REGISTERED_ERROR, RHN_DISABLED)
self._write_channels_file({})
return
try:
svrChannels = up2date_client.rhnChannel.getChannelDetails(
timeout=self.conf.timeout)
except up2dateErrors.CommunicationError as e:
logger.error("%s\n%s\n%s", COMMUNICATION_ERROR, RHN_DISABLED, e)
return
except up2dateErrors.NoChannelsError:
logger.error("%s\n%s", NOT_SUBSCRIBED_ERROR, CHANNELS_DISABLED)
self._write_channels_file({})
return
except up2dateErrors.NoSystemIdError:
logger.error("%s %s\n%s\n%s", NOT_SUBSCRIBED_ERROR,
NO_SYSTEM_ID_ERROR, USE_RHNREGISTER, RHN_DISABLED)
return
self.connected_to_spacewalk = True
logger.info(UPDATES_FROM_SPACEWALK)
for channel in svrChannels:
if channel['version']:
enabled_channels[channel['label']] = dict(channel.items())
self._write_channels_file(enabled_channels)
repos = self.base.repos
for (channel_id, channel_dict) in enabled_channels.items():
cached_channel = cached_channels.get(channel_id)
cached_version = None
if cached_channel:
cached_version = cached_channel.get('version')
conf = copy(self.conf)
if channel_id in self.parser.sections():
options = self.parser.items(channel_id)
for (key, value) in options:
conf._set_value(key, value, PRIO_PLUGINCONFIG)
repo = SpacewalkRepo(channel_dict, {
'conf' : self.base.conf,
'proxy' : proxy_url,
'timeout' : conf.timeout,
'sslcacert' : sslcacert,
'force_http': force_http,
'cached_version' : cached_version,
'login_info': login_info,
'gpgcheck': conf.gpgcheck,
'enabled': conf.enabled,
})
repos.add(repo)
# DEBUG
logger.debug(enabled_channels)
def transaction(self):
""" Update system's profile after transaction. """
if not self.conf.enabled:
return
if not self.connected_to_spacewalk:
# not connected so nothing to do here
return
if self.up2date_cfg['writeChangesToLog'] == 1:
delta = self._make_package_delta()
up2date_client.rhnPackageInfo.logDeltaPackages(delta)
try:
up2date_client.rhnPackageInfo.updatePackageProfile(
timeout=self.conf.timeout)
except up2dateErrors.RhnServerException as e:
logger.error("%s\n%s\n%s", COMMUNICATION_ERROR, PROFILE_NOT_SENT, e)
def _read_channels_file(self):
try:
with open(self.stored_channels_path, "r") as channels_file:
content = channels_file.read()
channels = json.loads(content)
return channels
except (FileNotFoundError, IOError) as e:
if e.errno != errno.ENOENT:
raise
except json.decoder.JSONDecodeError as e:
pass # ignore broken json and recreate it later
return {}
def _write_channels_file(self, var):
try:
with open(self.stored_channels_path, "w") as channels_file:
json.dump(var, channels_file, indent=4)
except (FileNotFoundError, IOError) as e:
if e.errno != errno.ENOENT:
raise
def _make_package_delta(self):
delta = {'added' : [(p.name, p.version, p. release, p.epoch, p.arch)
for p in self.base.transaction.install_set],
'removed': [(p.name, p.version, p. release, p.epoch, p.arch)
for p in self.base.transaction.remove_set],
}
return delta
class SpacewalkRepo(dnf.repo.Repo):
"""
Repository object for Spacewalk. Uses up2date libraries.
"""
needed_headers = ['X-RHN-Server-Id',
'X-RHN-Auth-User-Id',
'X-RHN-Auth',
'X-RHN-Auth-Server-Time',
'X-RHN-Auth-Expire-Offset']
def __init__(self, channel, opts):
super(SpacewalkRepo, self).__init__(ustr(channel['label']),
opts.get('conf'))
# dnf stuff
self.name = ustr(channel['name'])
self.baseurl = [ url + '/GET-REQ/' + self.id for url in channel['url']]
self.sslcacert = opts.get('sslcacert')
self.proxy = opts.get('proxy')
try:
self.gpgkey = get_gpg_key_urls(channel['gpg_key_url'])
except InvalidGpgKeyLocation as e:
logger.warning(GPG_KEY_REJECTED, dnf.i18n.ucd(e))
self.gpgkey = []
if channel['version'] != opts.get('cached_version'):
self.metadata_expire = 1
# spacewalk stuff
self.login_info = opts.get('login_info')
self.keepalive = 0
self.bandwidth = 0
self.retries = 1
self.throttle = 0
self.timeout = opts.get('timeout')
self.gpgcheck = opts.get('gpgcheck')
self.force_http = opts.get('force_http')
if opts.get('enabled'):
self.enable()
else:
self.disable()
if hasattr(self, 'set_http_headers'):
# dnf > 4.0.9 on RHEL 8, Fedora 29/30
http_headers = self.create_http_headers()
if http_headers:
self.set_http_headers(http_headers)
def create_http_headers(self):
http_headers = []
if not self.login_info:
return http_headers
for header in self.needed_headers:
if not header in self.login_info:
error = MISSING_HEADER % header
raise dnf.Error.RepoError(error)
if self.login_info[header] in (None, ''):
# This doesn't work due to bug in librepo (or even deeper in libcurl)
# the workaround bellow can be removed once BZ#1211662 is fixed
#http_headers.append("%s;" % header)
http_headers.append("%s: \r\nX-libcurl-Empty-Header-Workaround: *" % header)
else:
http_headers.append("%s: %s" % (header, self.login_info[header]))
if not self.force_http:
http_headers.append("X-RHN-Transport-Capability: follow-redirects=3")
return http_headers
def _handle_new_remote(self, destdir, mirror_setup=True):
# this function is called only on dnf < 3.6.0 (up to Fedora 29)
handle = super(SpacewalkRepo, self)._handle_new_remote(destdir, mirror_setup)
http_headers = self.create_http_headers()
if http_headers:
handle.setopt(librepo.LRO_HTTPHEADER, http_headers)
return handle
# FIXME
# all rutines bellow should go to rhn-client-tools because they are share
# between yum-rhn-plugin and dnf-plugin-spacewalk
def get_gpg_key_urls(key_url_string):
"""
Parse the key urls and validate them.
key_url_string is a space seperated list of gpg key urls that must be
located in /etc/pkg/rpm-gpg/.
Return a list of strings containing the key urls.
Raises InvalidGpgKeyLocation if any of the key urls are invalid.
"""
key_urls = key_url_string.split()
for key_url in key_urls:
if not is_valid_gpg_key_url(key_url):
raise InvalidGpgKeyLocation(key_url)
return key_urls
class InvalidGpgKeyLocation(Exception):
pass
def is_valid_gpg_key_url(key_url):
proto_split = key_url.split('://')
if len(proto_split) != 2:
return False
proto, path = proto_split
if proto.lower() != 'file':
return False
path = os.path.normpath(path)
if not path.startswith('/etc/pki/rpm-gpg/'):
return False
return True
def get_ssl_ca_cert(up2date_cfg):
if not ('sslCACert' in up2date_cfg and up2date_cfg['sslCACert']):
raise BadSslCaCertConfig
ca_certs = up2date_cfg['sslCACert']
if type(ca_certs) == list:
return ca_certs[0]
return ca_certs