#!/usr/bin/env python # Copyright 2011 OpenStack Foundation # Copyright 2011 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # NOTE: XenServer still only supports Python 2.4 in it's dom0 userspace # which means the Nova xenapi plugins must use only Python 2.4 features # # XenAPI plugin for host operations # Changes in this file will be merged post liberty # OVS interim bridge: https://review.openstack.org/#/c/242846/29 # Neutron security group: https://review.openstack.org/#/c/251271/10 # try: import json except ImportError: import simplejson as json import logging import re import sys import time import utils import pluginlib_nova as pluginlib import XenAPI import XenAPIPlugin try: import xmlrpclib except ImportError: import six.moves.xmlrpc_client as xmlrpclib pluginlib.configure_logging("xenhost") _ = pluginlib._ host_data_pattern = re.compile(r"\s*(\S+) \([^\)]+\) *: ?(.*)") config_file_path = "/usr/etc/xenhost.conf" DEFAULT_TRIES = 23 DEFAULT_SLEEP = 10 def jsonify(fnc): def wrapper(*args, **kwargs): return json.dumps(fnc(*args, **kwargs)) return wrapper class TimeoutError(StandardError): pass def _run_command(cmd, cmd_input=None): """Wrap utils.run_command to raise PluginError on failure """ try: return utils.run_command(cmd, cmd_input=cmd_input) except utils.SubprocessException, e: raise pluginlib.PluginError(e.err) def _resume_compute(session, compute_ref, compute_uuid): """Resume compute node on slave host after pool join. This has to happen regardless of the success or failure of the join operation.""" try: # session is valid if the join operation has failed session.xenapi.VM.start(compute_ref, False, True) except XenAPI.Failure, e: # if session is invalid, e.g. xapi has restarted, then the pool # join has been successful, wait for xapi to become alive again for c in xrange(0, DEFAULT_TRIES): try: _run_command(["xe", "vm-start", "uuid=%s" % compute_uuid]) return except pluginlib.PluginError, e: logging.exception('Waited %d seconds for the slave to ' 'become available.' % (c * DEFAULT_SLEEP)) time.sleep(DEFAULT_SLEEP) raise pluginlib.PluginError('Unrecoverable error: the host has ' 'not come back for more than %d seconds' % (DEFAULT_SLEEP * (DEFAULT_TRIES + 1))) @jsonify def set_host_enabled(self, arg_dict): """Sets this host's ability to accept new instances. It will otherwise continue to operate normally. """ enabled = arg_dict.get("enabled") if enabled is None: raise pluginlib.PluginError( _("Missing 'enabled' argument to set_host_enabled")) host_uuid = arg_dict['host_uuid'] if enabled == "true": result = _run_command(["xe", "host-enable", "uuid=%s" % host_uuid]) elif enabled == "false": result = _run_command(["xe", "host-disable", "uuid=%s" % host_uuid]) else: raise pluginlib.PluginError(_("Illegal enabled status: %s") % enabled) # Should be empty string if result: raise pluginlib.PluginError(result) # Return the current enabled status cmd = ["xe", "host-param-get", "uuid=%s" % host_uuid, "param-name=enabled"] host_enabled = _run_command(cmd) if host_enabled == "true": status = "enabled" else: status = "disabled" return {"status": status} def _write_config_dict(dct): conf_file = file(config_file_path, "w") json.dump(dct, conf_file) conf_file.close() def _get_config_dict(): """Returns a dict containing the key/values in the config file. If the file doesn't exist, it is created, and an empty dict is returned. """ try: conf_file = file(config_file_path) config_dct = json.load(conf_file) conf_file.close() except IOError: # File doesn't exist config_dct = {} # Create the file _write_config_dict(config_dct) return config_dct @jsonify def get_config(self, arg_dict): """Return the value stored for the specified key, or None if no match.""" conf = _get_config_dict() params = arg_dict["params"] try: dct = json.loads(params) except Exception, e: dct = params key = dct["key"] ret = conf.get(key) if ret is None: # Can't jsonify None return "None" return ret @jsonify def set_config(self, arg_dict): """Write the specified key/value pair, overwriting any existing value.""" conf = _get_config_dict() params = arg_dict["params"] try: dct = json.loads(params) except Exception, e: dct = params key = dct["key"] val = dct["value"] if val is None: # Delete the key, if present conf.pop(key, None) else: conf.update({key: val}) _write_config_dict(conf) def iptables_config(session, args): # command should be either save or restore logging.debug("iptables_config:enter") logging.debug("iptables_config: args=%s", args) cmd_args = pluginlib.exists(args, 'cmd_args') logging.debug("iptables_config: cmd_args=%s", cmd_args) process_input = pluginlib.optional(args, 'process_input') logging.debug("iptables_config: process_input=%s", process_input) cmd = json.loads(cmd_args) cmd = map(str, cmd) # either execute iptable-save or iptables-restore # command must be only one of these two # process_input must be used only with iptables-restore if len(cmd) > 0 and cmd[0] in ('iptables-save', 'iptables-restore', 'ip6tables-save', 'ip6tables-restore'): result = _run_command(cmd, process_input) ret_str = json.dumps(dict(out=result, err='')) logging.debug("iptables_config:exit") return ret_str else: # else don't do anything and return an error raise pluginlib.PluginError(_("Invalid iptables command")) def network_config(session, args): # function to config OVS bridge and Linux bridge ALLOWED_CMDS = [ 'ovs-vsctl', 'brctl', 'ip' ] cmd = json.loads(args.get('cmd')) if cmd is None or cmd == []: msg = _("empty command is supplied") raise pluginlib.PluginError(msg) if cmd[0] not in ALLOWED_CMDS: msg = _("Dom0 execution of '%s' is not permitted") % cmd[0] raise pluginlib.PluginError(msg) result = _run_command(cmd, json.loads(args.get('cmd_input', 'null'))) return json.dumps(result) def _power_action(action, arg_dict): # Host must be disabled first host_uuid = arg_dict['host_uuid'] result = _run_command(["xe", "host-disable", "uuid=%s" % host_uuid]) if result: raise pluginlib.PluginError(result) # All running VMs must be shutdown result = _run_command(["xe", "vm-shutdown", "--multiple", "resident-on=%s" % host_uuid]) if result: raise pluginlib.PluginError(result) cmds = {"reboot": "host-reboot", "startup": "host-power-on", "shutdown": "host-shutdown",} result = _run_command(["xe", cmds[action], "uuid=%s" % host_uuid]) # Should be empty string if result: raise pluginlib.PluginError(result) return {"power_action": action} @jsonify def host_reboot(self, arg_dict): """Reboots the host.""" return _power_action("reboot", arg_dict) @jsonify def host_shutdown(self, arg_dict): """Reboots the host.""" return _power_action("shutdown", arg_dict) @jsonify def host_start(self, arg_dict): """Starts the host. Currently not feasible, since the host runs on the same machine as Xen. """ return _power_action("startup", arg_dict) @jsonify def host_join(self, arg_dict): """Join a remote host into a pool whose master is the host where the plugin is called from. The following constraints apply: - The host must have no VMs running, except nova-compute, which will be shut down (and restarted upon pool-join) automatically, - The host must have no shared storage currently set up, - The host must have the same license of the master, - The host must have the same supplemental packs as the master.""" session = XenAPI.Session(arg_dict.get("url")) session.login_with_password(arg_dict.get("user"), arg_dict.get("password")) compute_ref = session.xenapi.VM.get_by_uuid(arg_dict.get('compute_uuid')) session.xenapi.VM.clean_shutdown(compute_ref) try: if arg_dict.get("force"): session.xenapi.pool.join(arg_dict.get("master_addr"), arg_dict.get("master_user"), arg_dict.get("master_pass")) else: session.xenapi.pool.join_force(arg_dict.get("master_addr"), arg_dict.get("master_user"), arg_dict.get("master_pass")) finally: _resume_compute(session, compute_ref, arg_dict.get("compute_uuid")) @jsonify def host_data(self, arg_dict): """Runs the commands on the xenstore host to return the current status information. """ host_uuid = arg_dict['host_uuid'] resp = _run_command(["xe", "host-param-list", "uuid=%s" % host_uuid]) parsed_data = parse_response(resp) # We have the raw dict of values. Extract those that we need, # and convert the data types as needed. ret_dict = cleanup(parsed_data) # Add any config settings config = _get_config_dict() ret_dict.update(config) return ret_dict def parse_response(resp): data = {} for ln in resp.splitlines(): if not ln: continue mtch = host_data_pattern.match(ln.strip()) try: k, v = mtch.groups() data[k] = v except AttributeError: # Not a valid line; skip it continue return data @jsonify def host_uptime(self, arg_dict): """Returns the result of the uptime command on the xenhost.""" return {"uptime": _run_command(['uptime'])} def cleanup(dct): """Take the raw KV pairs returned and translate them into the appropriate types, discarding any we don't need. """ def safe_int(val): """Integer values will either be string versions of numbers, or empty strings. Convert the latter to nulls. """ try: return int(val) except ValueError: return None def strip_kv(ln): return [val.strip() for val in ln.split(":", 1)] out = {} # sbs = dct.get("supported-bootloaders", "") # out["host_supported-bootloaders"] = sbs.split("; ") # out["host_suspend-image-sr-uuid"] = dct.get("suspend-image-sr-uuid", "") # out["host_crash-dump-sr-uuid"] = dct.get("crash-dump-sr-uuid", "") # out["host_local-cache-sr"] = dct.get("local-cache-sr", "") out["enabled"] = dct.get("enabled", "true") == "true" out["host_memory"] = omm = {} omm["total"] = safe_int(dct.get("memory-total", "")) omm["overhead"] = safe_int(dct.get("memory-overhead", "")) omm["free"] = safe_int(dct.get("memory-free", "")) omm["free-computed"] = safe_int( dct.get("memory-free-computed", "")) # out["host_API-version"] = avv = {} # avv["vendor"] = dct.get("API-version-vendor", "") # avv["major"] = safe_int(dct.get("API-version-major", "")) # avv["minor"] = safe_int(dct.get("API-version-minor", "")) out["enabled"] = dct.get("enabled", True) out["host_uuid"] = dct.get("uuid", None) out["host_name-label"] = dct.get("name-label", "") out["host_name-description"] = dct.get("name-description", "") # out["host_host-metrics-live"] = dct.get( # "host-metrics-live", "false") == "true" out["host_hostname"] = dct.get("hostname", "") out["host_ip_address"] = dct.get("address", "") oc = dct.get("other-config", "") out["host_other-config"] = ocd = {} if oc: for oc_fld in oc.split("; "): ock, ocv = strip_kv(oc_fld) ocd[ock] = ocv capabilities = dct.get("capabilities", "") out["host_capabilities"] = capabilities.replace(";", "").split() # out["host_allowed-operations"] = dct.get( # "allowed-operations", "").split("; ") # lsrv = dct.get("license-server", "") # out["host_license-server"] = ols = {} # if lsrv: # for lspart in lsrv.split("; "): # lsk, lsv = lspart.split(": ") # if lsk == "port": # ols[lsk] = safe_int(lsv) # else: # ols[lsk] = lsv # sv = dct.get("software-version", "") # out["host_software-version"] = osv = {} # if sv: # for svln in sv.split("; "): # svk, svv = strip_kv(svln) # osv[svk] = svv cpuinf = dct.get("cpu_info", "") out["host_cpu_info"] = ocp = {} if cpuinf: for cpln in cpuinf.split("; "): cpk, cpv = strip_kv(cpln) if cpk in ("cpu_count", "family", "model", "stepping"): ocp[cpk] = safe_int(cpv) else: ocp[cpk] = cpv # out["host_edition"] = dct.get("edition", "") # out["host_external-auth-service-name"] = dct.get( # "external-auth-service-name", "") return out def query_gc(session, sr_uuid, vdi_uuid): result = _run_command(["/opt/xensource/sm/cleanup.py", "-q", "-u", sr_uuid]) # Example output: "Currently running: True" return result[19:].strip() == "True" def get_pci_device_details(session): """Returns a string that is a list of pci devices with details. This string is obtained by running the command lspci. With -vmm option, it dumps PCI device data in machine readable form. This verbose format display a sequence of records separated by a blank line. We will also use option "-n" to get vendor_id and device_id as numeric values and the "-k" option to get the kernel driver used if any. """ return _run_command(["lspci", "-vmmnk"]) def get_pci_type(session, pci_device): """Returns the type of the PCI device (type-PCI, type-VF or type-PF). pci-device -- The address of the pci device """ # We need to add the domain if it is missing if pci_device.count(':') == 1: pci_device = "0000:" + pci_device output = _run_command(["ls", "/sys/bus/pci/devices/" + pci_device + "/"]) if "physfn" in output: return "type-VF" if "virtfn" in output: return "type-PF" return "type-PCI" if __name__ == "__main__": # Support both serialized and non-serialized plugin approaches _, methodname = xmlrpclib.loads(sys.argv[1]) if methodname in ['query_gc', 'get_pci_device_details', 'get_pci_type']: utils.register_plugin_calls(query_gc, get_pci_device_details, get_pci_type) XenAPIPlugin.dispatch( {"host_data": host_data, "set_host_enabled": set_host_enabled, "host_shutdown": host_shutdown, "host_reboot": host_reboot, "host_start": host_start, "host_join": host_join, "get_config": get_config, "set_config": set_config, "iptables_config": iptables_config, "network_config": network_config, "host_uptime": host_uptime})