#!/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 # TODO(sfinucan): Resolve all 'noqa' items once the above is no longer true # # XenAPI plugin for host operations # try: import json except ImportError: import simplejson as json import logging import re import sys import time import six import utils import dom0_pluginlib as pluginlib import XenAPI import XenAPIPlugin try: import xmlrpclib except ImportError: import six.moves.xmlrpc_client as xmlrpclib pluginlib.configure_logging("xenhost") 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(Exception): 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 as e: # noqa 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: # 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 range(0, DEFAULT_TRIES): try: _run_command(["xe", "vm-start", "uuid=%s" % compute_uuid]) return except pluginlib.PluginError: 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: 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: 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(list(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 don't do anything and return an error else: raise pluginlib.PluginError("Invalid iptables command") def _ovs_add_patch_port(args): bridge_name = pluginlib.exists(args, 'bridge_name') port_name = pluginlib.exists(args, 'port_name') peer_port_name = pluginlib.exists(args, 'peer_port_name') cmd_args = ['ovs-vsctl', '--', '--if-exists', 'del-port', port_name, '--', 'add-port', bridge_name, port_name, '--', 'set', 'interface', port_name, 'type=patch', 'options:peer=%s' % peer_port_name] return _run_command(cmd_args) def _ovs_del_port(args): bridge_name = pluginlib.exists(args, 'bridge_name') port_name = pluginlib.exists(args, 'port_name') cmd_args = ['ovs-vsctl', '--', '--if-exists', 'del-port', bridge_name, port_name] return _run_command(cmd_args) def _ovs_del_br(args): bridge_name = pluginlib.exists(args, 'bridge_name') cmd_args = ['ovs-vsctl', '--', '--if-exists', 'del-br', bridge_name] return _run_command(cmd_args) def _ovs_set_if_external_id(args): interface = pluginlib.exists(args, 'interface') extneral_id = pluginlib.exists(args, 'extneral_id') value = pluginlib.exists(args, 'value') cmd_args = ['ovs-vsctl', 'set', 'Interface', interface, 'external-ids:%s=%s' % (extneral_id, value)] return _run_command(cmd_args) def _ovs_add_port(args): bridge_name = pluginlib.exists(args, 'bridge_name') port_name = pluginlib.exists(args, 'port_name') cmd_args = ['ovs-vsctl', '--', '--if-exists', 'del-port', port_name, '--', 'add-port', bridge_name, port_name] return _run_command(cmd_args) def _ovs_create_port(args): bridge = pluginlib.exists(args, 'bridge') port = pluginlib.exists(args, 'port') iface_id = pluginlib.exists(args, 'iface-id') mac = pluginlib.exists(args, 'mac') status = pluginlib.exists(args, 'status') cmd_args = ['ovs-vsctl', '--', '--if-exists', 'del-port', port, '--', 'add-port', bridge, port, '--', 'set', 'Interface', port, 'external_ids:iface-id=%s' % iface_id, 'external_ids:iface-status=%s' % status, 'external_ids:attached-mac=%s' % mac, 'external_ids:xs-vif-uuid=%s' % iface_id] return _run_command(cmd_args) def _ip_link_get_dev(args): device_name = pluginlib.exists(args, 'device_name') cmd_args = ['ip', 'link', 'show', device_name] return _run_command(cmd_args) def _ip_link_del_dev(args): device_name = pluginlib.exists(args, 'device_name') cmd_args = ['ip', 'link', 'delete', device_name] return _run_command(cmd_args) def _ip_link_add_veth_pair(args): dev1_name = pluginlib.exists(args, 'dev1_name') dev2_name = pluginlib.exists(args, 'dev2_name') cmd_args = ['ip', 'link', 'add', dev1_name, 'type', 'veth', 'peer', 'name', dev2_name] return _run_command(cmd_args) def _ip_link_set_dev(args): device_name = pluginlib.exists(args, 'device_name') option = pluginlib.exists(args, 'option') cmd_args = ['ip', 'link', 'set', device_name, option] return _run_command(cmd_args) def _ip_link_set_promisc(args): device_name = pluginlib.exists(args, 'device_name') option = pluginlib.exists(args, 'option') cmd_args = ['ip', 'link', 'set', device_name, 'promisc', option] return _run_command(cmd_args) def _brctl_add_br(args): bridge_name = pluginlib.exists(args, 'bridge_name') cmd_args = ['brctl', 'addbr', bridge_name] return _run_command(cmd_args) def _brctl_del_br(args): bridge_name = pluginlib.exists(args, 'bridge_name') cmd_args = ['brctl', 'delbr', bridge_name] return _run_command(cmd_args) def _brctl_set_fd(args): bridge_name = pluginlib.exists(args, 'bridge_name') fd = pluginlib.exists(args, 'fd') cmd_args = ['brctl', 'setfd', bridge_name, fd] return _run_command(cmd_args) def _brctl_set_stp(args): bridge_name = pluginlib.exists(args, 'bridge_name') option = pluginlib.exists(args, 'option') cmd_args = ['brctl', 'stp', bridge_name, option] return _run_command(cmd_args) def _brctl_add_if(args): bridge_name = pluginlib.exists(args, 'bridge_name') if_name = pluginlib.exists(args, 'interface_name') cmd_args = ['brctl', 'addif', bridge_name, if_name] return _run_command(cmd_args) def _brctl_del_if(args): bridge_name = pluginlib.exists(args, 'bridge_name') if_name = pluginlib.exists(args, 'interface_name') cmd_args = ['brctl', 'delif', bridge_name, if_name] return _run_command(cmd_args) ALLOWED_NETWORK_CMDS = { # allowed cmds to config OVS bridge 'ovs_add_patch_port': _ovs_add_patch_port, 'ovs_add_port': _ovs_add_port, 'ovs_create_port': _ovs_create_port, 'ovs_del_port': _ovs_del_port, 'ovs_del_br': _ovs_del_br, 'ovs_set_if_external_id': _ovs_set_if_external_id, 'ip_link_add_veth_pair': _ip_link_add_veth_pair, 'ip_link_del_dev': _ip_link_del_dev, 'ip_link_get_dev': _ip_link_get_dev, 'ip_link_set_dev': _ip_link_set_dev, 'ip_link_set_promisc': _ip_link_set_promisc, 'brctl_add_br': _brctl_add_br, 'brctl_add_if': _brctl_add_if, 'brctl_del_br': _brctl_del_br, 'brctl_del_if': _brctl_del_if, 'brctl_set_fd': _brctl_set_fd, 'brctl_set_stp': _brctl_set_stp } def network_config(session, args): """network config functions""" cmd = pluginlib.exists(args, 'cmd') if not isinstance(cmd, six.string_types): msg = "invalid command '%s'" % str(cmd) raise pluginlib.PluginError(msg) return if cmd not in ALLOWED_NETWORK_CMDS: msg = "Dom0 execution of '%s' is not permitted" % cmd raise pluginlib.PluginError(msg) return cmd_args = pluginlib.exists(args, 'args') return ALLOWED_NETWORK_CMDS[cmd](cmd_args) 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. The pool's 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", "false") == "false": 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" 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_memory"] = omm # 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", "") ocd = {} if oc: for oc_fld in oc.split("; "): ock, ocv = strip_kv(oc_fld) ocd[ock] = ocv out["host_other-config"] = ocd 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", "") 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_cpu_info"] = ocp # 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', 'network_config']: utils.register_plugin_calls(query_gc, get_pci_device_details, get_pci_type, network_config) 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, "host_uptime": host_uptime})