
Currently the tox py34 target uses testtools.run to run a subset of our test harness. We need to be able to use pretty_tox.sh just like py27 as we make progress with py34 support. The first step is to make sure we can discover all the tests using: python -m subunit.run discover -t . ./nova/tests/ --list So, we need to fix a bunch of things for the discovery to work including updating to a new version of websockify. In the xen code, we should keep the original import and add except for py34 as xen uses an older python that does not work with six.moves. Depends-On: Ib4ef2e79b28b7180e564b3d6dc2188456c66c08a Change-Id: I88b6746da6136a7386a173f6cacd42f0b470deee
461 lines
15 KiB
Python
Executable File
461 lines
15 KiB
Python
Executable File
#!/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
|
|
#
|
|
|
|
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 _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,
|
|
"host_uptime": host_uptime})
|