Ryan Barry has uploaded a new change for review. Change subject: [DRAFT] First pass at rhn_model ......................................................................
[DRAFT] First pass at rhn_model Reworking the RHN classic/satellite stuff. Onto SAM next, which will be faster, then reworking the transaction flow Change-Id: I52cc84a07ff0c45f3345fd2ec09fc97aa5b75a3a Signed-off-by: Ryan Barry <[email protected]> --- M src/ovirt/node/setup/rhn/rhn_model.py 1 file changed, 238 insertions(+), 141 deletions(-) git pull ssh://gerrit.ovirt.org:29418/ovirt-node refs/changes/76/40376/1 diff --git a/src/ovirt/node/setup/rhn/rhn_model.py b/src/ovirt/node/setup/rhn/rhn_model.py old mode 100644 new mode 100755 index a808200..6823ce2 --- a/src/ovirt/node/setup/rhn/rhn_model.py +++ b/src/ovirt/node/setup/rhn/rhn_model.py @@ -18,41 +18,16 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. A copy of the GNU General Public License is # also available at http://www.gnu.org/copyleft/gpl.html. -from ovirt.node import utils +from ovirt.node import base, utils from ovirt.node.config.defaults import NodeConfigFileSection from ovirt.node.utils import process, system from ovirt.node.utils.fs import Config -from urlparse import urlparse import sys +import re import os.path import glob -import subprocess -import urllib - - -RHN_XMLRPC_ADDR = "https://xmlrpc.rhn.redhat.com/XMLRPC" -RHN_SSL_CERT = "/usr/share/rhn/RHNS-CA-CERT" - - -def parse_host_port(url): - if url.count('://') == 1: - (proto, url) = url.split('://') - else: - proto = '' - if url.count(':') == 1: - (url, port) = url.split(':') - try: - port = int(port) - except: - port = 0 - elif proto == 'http': - port = 80 - elif proto == 'https': - port = 443 - else: - port = 0 - host = url.split('/')[0] - return (host, port) +import requests +import urlparse class RHN(NodeConfigFileSection): @@ -80,135 +55,231 @@ cfg = dict(NodeConfigFileSection.retrieve(self)) return cfg - def retrieveCert(self, url, dest): - for x in range(0, 3): - try: - # urllib doesn't check ssl certs, so we're ok here - urllib.urlretrieve(url, dest) - return - except IOError: - self.logger.debug( - "Failed to download {url} on try {x}".format( - url=url, x=x)) + def retrieve_cert(self, url, dest): + """ + Grab the SSL certificate by trying 3 times (use another method to + actually do the retrieval). If it doesn't get pulled, raise an + exception + """ + success = False - # If we're here, we failed to get it - raise RuntimeError("Error downloading SSL certificate!") + for x in range(0, 3): + msg = self._retrieve_url(url, dest) + if msg: + self.logger.info("Failed to retrieve download {url} on " + "try {x}".format(url=url, x=x)) + self.logger.info(msg) + else: + # We could return and rely on the falling through to + # raise the exception, but it's clearer to be explicit + success = True + break + if not success: + raise RuntimeError("Error downloading SSL certificate") + + def _retrieve_url(self, url, dest): + """ + Retrieve a file from a URL. Use python-requests so we can be as + explicit as possible about problems, and it's the most pythonic + library in stdlib (don't use urllib or urllib2 for this) + """ + msg = None + + with open(dest, 'w') as f: + try: + s = requests.Session() + r = s.get(url, stream=True) + if r.status_code != 200: + msg = "Cannot download the file: HTTP error code %s" \ + % str(r.status_code) + os.unlink(dest) + f.write(r.raw.read()) + except requests.exceptions.ConnectionError as e: + msg = "Connection Error: %s" % str(e[0]) + os.unlink(dest) + except: + import traceback + msg = "Unexpected error: %s" % str(traceback.format_exc()) + finally: + return msg + + def parse_host_uri_re(self, uri): + # Regexes (and complex regexes) aren't always the clearest solution, + # but they're a lot clearer than trying to parse out a URI without them + # through weird substring matching and partial matches which don't + # paint a complete picture of what's happening in one shot. Do the + # legwork just in case, but urlparse (below) is better + + matcher = re.compile(r""" + ^ + (?: #Start a nonmatching capture group + (?P<proto>\w+) # But capture the protocol + (?:://))? # Not the URI separator + (?P<host>.*?) # Get the host, not optional + (?:(?::)(?P<port>\d+))? # Maybe a port, maybe not + (?:/.*?)? # Path may continue, but scrap it + $""", re.VERBOSE) + + matches = matcher.match(uri).groupdict() + + ports = {"http": 80, + "https": 443} + + matches["port"] = matches["port"] or ports[matches["proto"]] if \ + matches["proto"] in ports else 0 + + return matches["host"], matches["port"] + + def parse_host_uri(self, uri): + ports = {"http": 80, + "https": 443} + + # urlparse expects the URI to start with proto://, or else it's + # a relative path. But only the // separator is required to parse + # it out correctly. If it's not there, add it. Still safer than + # re-implementing uri parsing ourselves + urischeme = re.compile(r'^\w+://') + uri = uri if urischeme.match(uri) else "//" + uri + + parsed = urlparse.urlparse(uri) + port = parsed.port or ports[parsed.scheme] if parsed.scheme in ports \ + else 0 + return parsed.hostname, port def transaction(self, password, proxypass=None): + cfg = dict(self.retrieve()) + + class Vars: + argbuilder = None + class ConfigureRHNClassic(utils.Transaction.Element): + title = "Setting up rhnreg config" + + def commit(self): + # Append the path so we can import rhnreg. Why isn't it + # also packaged in site-packages? Should ask the maintainer + sys.path.append("/usr/share/rhn/up2date_client") + import rhnreg + + rhnreg.cfg.set("serverURL", + "https://xmlrpc.rhn.redhat.com/XMLRPC") + rhnreg.cfg.set("sslCACert", + "/usr/share/rhn/RHNS-CA-CERT") + rhnreg.cfg.save() + self.logger.debug("Updated rhnreg config using their API") + + class DownloadCertificate(utils.Transaction.Element): + title = "Checking SSL Certificate" + + def commit(self): + # If there's no CA cert path specified, assume the defaut + cfg["ca_cert"] = cfg["ca_cert"] if cfg["ca_cert"] else \ + cfg["url"] + "/pub/RHN-ORG-TRUSTED-SSL-CERT" + + location = "/etc/sysconfig/rhn/%s" % os.path.basename( + cfg["ca_cert"]) + + if not os.path.exists(location): + self.logger.info("Downloading CA cert from: %s as %s", + (cfg["ca_cert"], location)) + RHN().retrieve_cert(cfg["ca_cert"], location) + + # Why would this ever happen without an exception from the + # method retrieving the cert? + if os.stat(location).st_size == 0: + raise RuntimeError("SSL certificate %s has has zero size, " + "can't use it. Please check.") + else: + Config().persist(location) + + class PrepareClassicRegistration(utils.Transaction.Element): + title = "Preparing for registration" + + def commit(self): + # FIXME: we should do this validation in the page in + # on_merge instead of trying to catch in the model. + # In theory, we could return this if only some values for + # autoinstaller registering were passed, but not registering + # then is expected behavior + if not cfg["activationkey"] and not cfg["username"]: + self.logger.debug("No activationkey or username+password " + "given. Exiting") + raise RuntimeError("No activation key or username+" + "password was given. Can't register!") + + # Why these flags? + # --novirtinfo: rhn-virtualization daemon refreshes virtinfo + # --nopackages because it's a ro image and an appliance + # can't update them anyway + # --norhnsd because we don't want to try (and fail) to run + # actions on groups from Spacewalk/Satellite + initial_args = ["/usr/sbin/rhnreg_ks"] + initial_args.extend(["--novirtinfo", "--norhnsd", + "--nopackages", "--force"]) + + # If the URL doesn't end with the default path, add it + cfg["url"] = cfg["url"] if cfg["url"].endswith("/XMLRPC") \ + else cfg["url"] + "/XMLRPC" + + mapping = {"--serverUrl": cfg["url"], + "--sslCACert": cfg["ca_cert"], + "--activationkey": cfg["activationkey"], + "--username": cfg["username"], + "--password": password, + "--profilename": cfg["profile"], + "--proxy": cfg["proxy"], + "--proxyUser": cfg["proxyuser"], + "--proxyPassword": proxypass + } + + Vars.argbuilder = ArgBuilder(initial_args, mapping) + + class RegisterRHNClassic(utils.Transaction.Element): state = ("RHN" if RHN().retrieve()["rhntype"] == "rhn" else "Satellite") title = "Configuring %s" % state def commit(self): - cfg = RHN().retrieve() - self.logger.debug(cfg) - rhntype = cfg["rhntype"] - serverurl = cfg["url"] - cacert = cfg["ca_cert"] - activationkey = cfg["activationkey"] - username = cfg["username"] - profilename = cfg["profile"] - proxy = cfg["proxy"] - proxyuser = cfg["proxyuser"] - - # novirtinfo: rhn-virtualization daemon refreshes virtinfo - extra_args = ['--novirtinfo', '--norhnsd', '--nopackages', - '--force'] - args = ['/usr/sbin/rhnreg_ks'] - if rhntype == "rhn": - sys.path.append("/usr/share/rhn/up2date_client") - import rhnreg - rhnreg.cfg.set("serverURL", RHN_XMLRPC_ADDR) - rhnreg.cfg.set("sslCACert", RHN_SSL_CERT) - rhnreg.cfg.save() - self.logger.info("ran update") - if serverurl: - cacert = cacert if cacert is not None else serverurl + \ - "/pub/RHN-ORG-TRUSTED-SSL-CERT" - if not serverurl.endswith("/XMLRPC"): - serverurl = serverurl + "/XMLRPC" - args.append('--serverUrl') - args.append(serverurl) - location = "/etc/sysconfig/rhn/%s" % \ - os.path.basename(cacert) - if cacert: - if not os.path.exists(cacert): - self.logger.info("Downloading CA cert.....") - self.logger.debug("From: %s To: %s" % - (cacert, location)) - RHN().retrieveCert(cacert, location) - if os.path.isfile(location): - if os.stat(location).st_size > 0: - args.append('--sslCACert') - args.append(location) - Config().persist(location) - else: - raise RuntimeError("Error Downloading \ - CA cert!") - if activationkey: - args.append('--activationkey') - args.append(activationkey) - elif username: - args.append('--username') - args.append(username) - if password: - args.append('--password') - args.append(password) - else: - # skip RHN registration when neither activationkey - # nor username/password is supplied - self.logger.debug("No activationkey or " - "username+password given") - return - - if profilename: - args.append('--profilename') - args.append(profilename) - - if proxy: - args.append('--proxy') - args.append(proxy) - if proxyuser: - args.append('--proxyUser') - args.append(proxyuser) - if proxypass: - args.append('--proxyPassword') - args.append(proxypass) - args.extend(extra_args) - self.logger.info("Registering to RHN account.....") - conf = Config() - conf.unpersist("/etc/sysconfig/rhn/systemid") - conf.unpersist("/etc/sysconfig/rhn/up2date") - logged_args = list(args) - remove_values_from_args = ["--password", "--proxyPassword"] - for idx, arg in enumerate(logged_args): - if arg in remove_values_from_args: - logged_args[idx+1] = "XXXXXXX" - logged_args = str(logged_args) + Config().unpersist("/etc/sysconfig/rhn/systemid") + Config().unpersist("/etc/sysconfig/rhn/up2date") + + # Filter out passwords from the log + logged_args = Vars.argbuilder.get_commandlist(filtered=True) self.logger.debug(logged_args) try: - subprocess.check_call(args) - conf.persist("/etc/sysconfig/rhn/up2date") - conf.persist("/etc/sysconfig/rhn/systemid") + process.check_call(Vars.argbuilder.get_commandlist()) + Config().persist("/etc/sysconfig/rhn/up2date") + Config().persist("/etc/sysconfig/rhn/systemid") self.logger.info("System %s sucessfully registered to %s" % - (profilename, serverurl)) - # sync profile if reregistering, fixes problem with - # virt guests not showing - sys.path.append("/usr/share/rhn") - from virtualization import support - support.refresh(True) - # find old SAM/Sat 6 registrations - if Config().exists("/etc/rhsm/rhsm.conf"): + (cfg["profile"], cfg["url"])) + + except process.CalledProcessError: + self.logger.exception("Failed to call: %s" % logged_args) + raise RuntimeError("Error registering to RHN account") + + # Syncing the profile resolves a problem with guests not + # showing + sys.path.append("/usr/share/rhn") + from virtualization import support + support.refresh(True) + + class RemoveSAMConfigs(utils.Transaction.Element): + title = "Removing old configuration..." + + def commit(self): + # find old SAM/Sat 6 registrations + if Config().exists("/etc/rhsm/rhsm.conf"): + try: process.call(["subscription-manager", "remove", "--all"]) process.call(["subscription-manager", "clean"]) Config().unpersist("/etc/rhsm/rhsm.conf") - except: - self.logger.exception("Failed to call: %s" % logged_args) - raise RuntimeError("Error registering to RHN account") + except process.CalledProcessError: + raise RuntimeError("Couldn't remove old configuration!" + " Check the output of " + "subscription-manager remove --all") class ConfigureSAM(utils.Transaction.Element): # sam path is used for sat6 as well, making generic @@ -263,7 +334,7 @@ if serverurl: (host, port) = parse_host_port(serverurl) - parsed_url = urlparse(serverurl) + parsed_url = urlparse.urlparse(serverurl) prefix = parsed_url.path if cacert.endswith(".pem") and rhntype == "satellite": prefix = "/rhsm" @@ -409,3 +480,29 @@ else: tx.append(ConfigureRHNClassic()) return tx + + +class ArgBuilder(base.Base): + args = None + filtered_args = ["--password", "--proxyPassword"] + + def __init__(self, mapping, initial_args): + self.args = initial_args + self._build_map(mapping) + + def _build_map(self, mapping): + # Can't filter() on a dict, so use a dict comprehension to do it + # and append in one line + _ = map(lambda (x,y): self.args.extend([x,y]), + dict((k,v) for k,v in mapping.items() if v).iteritems()) + + def get_commandlist(self, filtered=False): + command = " ".join(self.args) + + if not filtered: + return command + else: + for arg in self.filtered_args: + r = re.compile(r'(%s) \w+' % arg) + command = re.sub(r, r'\1 XXXXXXXX', command) + return command -- To view, visit https://gerrit.ovirt.org/40376 To unsubscribe, visit https://gerrit.ovirt.org/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I52cc84a07ff0c45f3345fd2ec09fc97aa5b75a3a Gerrit-PatchSet: 1 Gerrit-Project: ovirt-node Gerrit-Branch: master Gerrit-Owner: Ryan Barry <[email protected]> _______________________________________________ node-patches mailing list [email protected] http://lists.ovirt.org/mailman/listinfo/node-patches
