commit cda53731c1807aad916f1ca07392fc13eacfb9d0 Author: Radek Novacek rnovacek@redhat.com Date: Tue Jun 3 16:37:08 2014 +0200
Restructure the code
Many improvements in the code structure, some highlights: * All the virt backend are inherited from base class * All the subscription managers are inherited from base class * Modules moved to subdirectories * Ability to use configuration files * Use daemon library for daemonization *
Makefile | 12 +- config.py | 150 ++++ daemon/daemon.py | 775 ++++++++++++++++++++ event.py | 383 ---------- log.py | 5 +- manager/__init__.py | 2 + manager/manager.py | 24 + manager/satellite/__init__.py | 2 + satellite.py => manager/satellite/satellite.py | 61 +- manager/subscriptionmanager/__init__.py | 2 + .../subscriptionmanager/subscriptionmanager.py | 41 +- test.py | 83 --- virt-who | 6 +- virt-who-config.5 | 50 ++ virt-who.spec | 9 +- virt.py | 148 ---- virt/__init__.py | 3 + virt/esx/__init__.py | 2 + vsphere.py => virt/esx/esx.py | 52 +- virt/hyperv/__init__.py | 2 + hyperv.py => virt/hyperv/hyperv.py | 61 ++- ntlm.py => virt/hyperv/ntlm.py | 224 +++--- virt/libvirtd/__init__.py | 2 + virt/libvirtd/libvirtd.py | 168 +++++ virt/rhevm/__init__.py | 2 + rhevm.py => virt/rhevm/rhevm.py | 9 +- virt/vdsm/__init__.py | 2 + vdsm.py => virt/vdsm/vdsm.py | 15 +- virt/virt.py | 88 +++ virt-who.py => virtwho.py | 442 ++++-------- 30 files changed, 1731 insertions(+), 1094 deletions(-) --- diff --git a/Makefile b/Makefile index dffebad..bc6d07e 100644 --- a/Makefile +++ b/Makefile @@ -13,13 +13,23 @@ check: pyflakes *.py
install: - install -d $(DESTDIR)/usr/share/$(name)/ $(DESTDIR)/usr/bin $(DESTDIR)/etc/rc.d/init.d $(DESTDIR)/etc/sysconfig $(DESTDIR)/usr/share/man/man8/ + for dir in {daemon,password,manager,virt} manager/{satellite,subscriptionmanager} virt/{esx,hyperv,libvirtd,rhevm,vdsm}; do \ + echo $$dir ; \ + install -d $(DESTDIR)/usr/share/$(name)/$$dir/ ; \ + install -pm 0644 $$dir/*.py $(DESTDIR)/usr/share/$(name)/$$dir/ ; \ + done install -pm 0644 *.py $(DESTDIR)/usr/share/$(name)/ + install -d $(DESTDIR)/usr/bin $(DESTDIR)/etc/rc.d/init.d $(DESTDIR)/etc/sysconfig $(DESTDIR)/usr/share/man/man8/ $(DESTDIR)/usr/share/man/man5/ $(DESTDIR)/var/lib/virt-who/ install virt-who $(DESTDIR)/usr/bin/ + install virt-who-password $(DESTDIR)/usr/bin/ install virt-who-initscript $(DESTDIR)/etc/rc.d/init.d/virt-who install -pm 0644 virt-who.conf $(DESTDIR)/etc/sysconfig/virt-who gzip -c virt-who.8 > virt-who.8.gz install -pm 0644 virt-who.8.gz $(DESTDIR)/usr/share/man/man8/ + gzip -c virt-who-password.8 > virt-who-password.8.gz + install -pm 0644 virt-who-password.8.gz $(DESTDIR)/usr/share/man/man8/ + gzip -c virt-who-config.5 > virt-who-config.5.gz + install -pm 0644 virt-who-config.5.gz $(DESTDIR)/usr/share/man/man5/
srpm: pack rpmbuild --define "_sourcedir $(PWD)" --define "_specdir $(PWD)" --define "_srcrpmdir $(PWD)" -bs $(name).spec diff --git a/config.py b/config.py new file mode 100644 index 0000000..f366180 --- /dev/null +++ b/config.py @@ -0,0 +1,150 @@ +""" +Module for reading configuration files + +Copyright (C) 2011 Radek Novacek rnovacek@redhat.com + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty 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. +""" + +import os +import logging +from ConfigParser import SafeConfigParser, NoOptionError, Error, MissingSectionHeaderError +from password import Password + + +VIRTWHO_CONF_DIR = "/etc/virt-who.d/" +VIRTWHO_TYPES = ("libvirt", "vdsm", "esx", "rhevm", "hyperv") + + +class InvalidOption(Error): + pass + + +class Config(object): + def __init__(self, name, type, server=None, username=None, password=None, owner=None, env=None): + self._name = name + self._type = type + if self._type not in VIRTWHO_TYPES: + raise InvalidOption('Invalid type "%s", must be one of following %s' % (self._type, ", ".join(VIRTWHO_TYPES))) + self._server = server + self._username = username + self._password = password + self._owner = owner + self._env = env + + def _read_password(self, name, parser): + try: + password = parser.get(name, "password") + except NoOptionError: + password = None + if password is None: + try: + crypted = parser.get(name, "encrypted_password") + password = Password.decrypt(password) + except NoOptionError: + return None + + @classmethod + def fromParser(self, name, parser): + type = parser.get(name, "type").lower() + server = username = password = owner = env = None + if type != 'libvirt': + server = parser.get(name, "server") + username = parser.get(name, "username") + + try: + password = parser.get(name, "password") + except NoOptionError: + password = None + if password is None: + try: + crypted = parser.get(name, "encrypted_password") + password = Password.decrypt(crypted) + except NoOptionError: + password = None + + try: + owner = parser.get(name, "owner") + except NoOptionError: + owner = None + try: + env = parser.get(name, "env") + except NoOptionError: + env = None + return Config(name, type, server, username, password, owner, env) + + @property + def name(self): + return self._name + + @property + def type(self): + return self._type + + @property + def server(self): + return self._server + + @property + def username(self): + return self._username + + @property + def password(self): + return self._password + + @property + def owner(self): + return self._owner + + @property + def env(self): + return self._env + + +class ConfigManager(object): + def __init__(self, config_dir=VIRTWHO_CONF_DIR): + self._parser = SafeConfigParser() + self._configs = [] + try: + config_dir_content = os.listdir(config_dir) + except OSError: + logging.warn("Configuration directory '%s' doesn't exist or is not accessible" % config_dir) + return + for conf in config_dir_content: + try: + filename = self._parser.read(os.path.join(config_dir, conf)) + if len(filename) == 0: + logging.error("Unable to read configuration file %s" % conf) + except MissingSectionHeaderError: + logging.error("Configuration file %s contains no section headers" % conf) + + self._readConfig() + + def _readConfig(self): + self._configs = [] + for section in self._parser.sections(): + try: + config = Config.fromParser(section, self._parser) + self._configs.append(config) + except NoOptionError, e: + logging.error(str(e)) + + @property + def configs(self): + return self._configs + + def addConfig(self, config): + self._configs.append(config) diff --git a/daemon/__init__.py b/daemon/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/daemon/daemon.py b/daemon/daemon.py new file mode 100644 index 0000000..2749322 --- /dev/null +++ b/daemon/daemon.py @@ -0,0 +1,775 @@ +# -*- coding: utf-8 -*- + +# daemon/daemon.py +# Part of python-daemon, an implementation of PEP 3143. +# +# Copyright © 2008–2010 Ben Finney ben+python@benfinney.id.au +# Copyright © 2007–2008 Robert Niederreiter, Jens Klein +# Copyright © 2004–2005 Chad J. Schroeder +# Copyright © 2003 Clark Evans +# Copyright © 2002 Noah Spurrier +# Copyright © 2001 Jürgen Hermann +# +# This is free software: you may copy, modify, and/or distribute this work +# under the terms of the Python Software Foundation License, version 2 or +# later as published by the Python Software Foundation. +# No warranty expressed or implied. See the file LICENSE.PSF-2 for details. + +""" Daemon process behaviour. + """ + +import os +import sys +import resource +import errno +import signal +import socket +import atexit + + +class DaemonError(Exception): + """ Base exception class for errors from this module. """ + + +class DaemonOSEnvironmentError(DaemonError, OSError): + """ Exception raised when daemon OS environment setup receives error. """ + + +class DaemonProcessDetachError(DaemonError, OSError): + """ Exception raised when process detach fails. """ + + +class DaemonContext(object): + """ Context for turning the current program into a daemon process. + + A `DaemonContext` instance represents the behaviour settings and + process context for the program when it becomes a daemon. The + behaviour and environment is customised by setting options on the + instance, before calling the `open` method. + + Each option can be passed as a keyword argument to the `DaemonContext` + constructor, or subsequently altered by assigning to an attribute on + the instance at any time prior to calling `open`. That is, for + options named `wibble` and `wubble`, the following invocation:: + + foo = daemon.DaemonContext(wibble=bar, wubble=baz) + foo.open() + + is equivalent to:: + + foo = daemon.DaemonContext() + foo.wibble = bar + foo.wubble = baz + foo.open() + + The following options are defined. + + `files_preserve` + :Default: ``None`` + + List of files that should *not* be closed when starting the + daemon. If ``None``, all open file descriptors will be closed. + + Elements of the list are file descriptors (as returned by a file + object's `fileno()` method) or Python `file` objects. Each + specifies a file that is not to be closed during daemon start. + + `chroot_directory` + :Default: ``None`` + + Full path to a directory to set as the effective root directory of + the process. If ``None``, specifies that the root directory is not + to be changed. + + `working_directory` + :Default: ``'/'`` + + Full path of the working directory to which the process should + change on daemon start. + + Since a filesystem cannot be unmounted if a process has its + current working directory on that filesystem, this should either + be left at default or set to a directory that is a sensible “home + directory” for the daemon while it is running. + + `umask` + :Default: ``0`` + + File access creation mask (“umask”) to set for the process on + daemon start. + + Since a process inherits its umask from its parent process, + starting the daemon will reset the umask to this value so that + files are created by the daemon with access modes as it expects. + + `pidfile` + :Default: ``None`` + + Context manager for a PID lock file. When the daemon context opens + and closes, it enters and exits the `pidfile` context manager. + + `detach_process` + :Default: ``None`` + + If ``True``, detach the process context when opening the daemon + context; if ``False``, do not detach. + + If unspecified (``None``) during initialisation of the instance, + this will be set to ``True`` by default, and ``False`` only if + detaching the process is determined to be redundant; for example, + in the case when the process was started by `init`, by `initd`, or + by `inetd`. + + `signal_map` + :Default: system-dependent + + Mapping from operating system signals to callback actions. + + The mapping is used when the daemon context opens, and determines + the action for each signal's signal handler: + + * A value of ``None`` will ignore the signal (by setting the + signal action to ``signal.SIG_IGN``). + + * A string value will be used as the name of an attribute on the + ``DaemonContext`` instance. The attribute's value will be used + as the action for the signal handler. + + * Any other value will be used as the action for the + signal handler. See the ``signal.signal`` documentation + for details of the signal handler interface. + + The default value depends on which signals are defined on the + running system. Each item from the list below whose signal is + actually defined in the ``signal`` module will appear in the + default map: + + * ``signal.SIGTTIN``: ``None`` + + * ``signal.SIGTTOU``: ``None`` + + * ``signal.SIGTSTP``: ``None`` + + * ``signal.SIGTERM``: ``'terminate'`` + + Depending on how the program will interact with its child + processes, it may need to specify a signal map that + includes the ``signal.SIGCHLD`` signal (received when a + child process exits). See the specific operating system's + documentation for more detail on how to determine what + circumstances dictate the need for signal handlers. + + `uid` + :Default: ``os.getuid()`` + + `gid` + :Default: ``os.getgid()`` + + The user ID (“UID”) value and group ID (“GID”) value to switch + the process to on daemon start. + + The default values, the real UID and GID of the process, will + relinquish any effective privilege elevation inherited by the + process. + + `prevent_core` + :Default: ``True`` + + If true, prevents the generation of core files, in order to avoid + leaking sensitive information from daemons run as `root`. + + `stdin` + :Default: ``None`` + + `stdout` + :Default: ``None`` + + `stderr` + :Default: ``None`` + + Each of `stdin`, `stdout`, and `stderr` is a file-like object + which will be used as the new file for the standard I/O stream + `sys.stdin`, `sys.stdout`, and `sys.stderr` respectively. The file + should therefore be open, with a minimum of mode 'r' in the case + of `stdin`, and mode 'w+' in the case of `stdout` and `stderr`. + + If the object has a `fileno()` method that returns a file + descriptor, the corresponding file will be excluded from being + closed during daemon start (that is, it will be treated as though + it were listed in `files_preserve`). + + If ``None``, the corresponding system stream is re-bound to the + file named by `os.devnull`. + + """ + + def __init__( + self, + chroot_directory=None, + working_directory='/', + umask=0, + uid=None, + gid=None, + prevent_core=True, + detach_process=None, + files_preserve=None, + pidfile=None, + stdin=None, + stdout=None, + stderr=None, + signal_map=None): + """ Set up a new instance. """ + self.chroot_directory = chroot_directory + self.working_directory = working_directory + self.umask = umask + self.prevent_core = prevent_core + self.files_preserve = files_preserve + self.pidfile = pidfile + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + + if uid is None: + uid = os.getuid() + self.uid = uid + if gid is None: + gid = os.getgid() + self.gid = gid + + if detach_process is None: + detach_process = is_detach_process_context_required() + self.detach_process = detach_process + + if signal_map is None: + signal_map = make_default_signal_map() + self.signal_map = signal_map + + self._is_open = False + + @property + def is_open(self): + """ ``True`` if the instance is currently open. """ + return self._is_open + + def open(self): + """ Become a daemon process. + :Return: ``None`` + + Open the daemon context, turning the current program into a daemon + process. This performs the following steps: + + * If this instance's `is_open` property is true, return + immediately. This makes it safe to call `open` multiple times on + an instance. + + * If the `prevent_core` attribute is true, set the resource limits + for the process to prevent any core dump from the process. + + * If the `chroot_directory` attribute is not ``None``, set the + effective root directory of the process to that directory (via + `os.chroot`). + + This allows running the daemon process inside a “chroot gaol” + as a means of limiting the system's exposure to rogue behaviour + by the process. Note that the specified directory needs to + already be set up for this purpose. + + * Set the process UID and GID to the `uid` and `gid` attribute + values. + + * Close all open file descriptors. This excludes those listed in + the `files_preserve` attribute, and those that correspond to the + `stdin`, `stdout`, or `stderr` attributes. + + * Change current working directory to the path specified by the + `working_directory` attribute. + + * Reset the file access creation mask to the value specified by + the `umask` attribute. + + * If the `detach_process` option is true, detach the current + process into its own process group, and disassociate from any + controlling terminal. + + * Set signal handlers as specified by the `signal_map` attribute. + + * If any of the attributes `stdin`, `stdout`, `stderr` are not + ``None``, bind the system streams `sys.stdin`, `sys.stdout`, + and/or `sys.stderr` to the files represented by the + corresponding attributes. Where the attribute has a file + descriptor, the descriptor is duplicated (instead of re-binding + the name). + + * If the `pidfile` attribute is not ``None``, enter its context + manager. + + * Mark this instance as open (for the purpose of future `open` and + `close` calls). + + * Register the `close` method to be called during Python's exit + processing. + + When the function returns, the running program is a daemon + process. + + """ + if self.is_open: + return + + if self.chroot_directory is not None: + change_root_directory(self.chroot_directory) + + if self.prevent_core: + prevent_core_dump() + + change_file_creation_mask(self.umask) + change_working_directory(self.working_directory) + change_process_owner(self.uid, self.gid) + + if self.detach_process: + detach_process_context() + + signal_handler_map = self._make_signal_handler_map() + set_signal_handlers(signal_handler_map) + + exclude_fds = self._get_exclude_file_descriptors() + close_all_open_files(exclude=exclude_fds) + + redirect_stream(sys.stdin, self.stdin) + redirect_stream(sys.stdout, self.stdout) + redirect_stream(sys.stderr, self.stderr) + + if self.pidfile is not None: + self.pidfile.__enter__() + + self._is_open = True + + register_atexit_function(self.close) + + def __enter__(self): + """ Context manager entry point. """ + self.open() + return self + + def close(self): + """ Exit the daemon process context. + :Return: ``None`` + + Close the daemon context. This performs the following steps: + + * If this instance's `is_open` property is false, return + immediately. This makes it safe to call `close` multiple times + on an instance. + + * If the `pidfile` attribute is not ``None``, exit its context + manager. + + * Mark this instance as closed (for the purpose of future `open` + and `close` calls). + + """ + if not self.is_open: + return + + if self.pidfile is not None: + # Follow the interface for telling a context manager to exit, + # URL:http://docs.python.org/library/stdtypes.html#typecontextmanager. + self.pidfile.__exit__(None, None, None) + + self._is_open = False + + def __exit__(self, exc_type, exc_value, traceback): + """ Context manager exit point. """ + self.close() + + def terminate(self, signal_number, stack_frame): + """ Signal handler for end-process signals. + :Return: ``None`` + + Signal handler for the ``signal.SIGTERM`` signal. Performs the + following step: + + * Raise a ``SystemExit`` exception explaining the signal. + + """ + exception = SystemExit( + "Terminating on signal %(signal_number)r" % vars()) + raise exception + + def _get_exclude_file_descriptors(self): + """ Return the set of file descriptors to exclude closing. + + Returns a set containing the file descriptors for the + items in `files_preserve`, and also each of `stdin`, + `stdout`, and `stderr`: + + * If the item is ``None``, it is omitted from the return + set. + + * If the item has a ``fileno()`` method, that method's + return value is in the return set. + + * Otherwise, the item is in the return set verbatim. + + """ + files_preserve = self.files_preserve + if files_preserve is None: + files_preserve = [] + files_preserve.extend( + item for item in [self.stdin, self.stdout, self.stderr] + if hasattr(item, 'fileno')) + exclude_descriptors = set() + for item in files_preserve: + if item is None: + continue + if hasattr(item, 'fileno'): + exclude_descriptors.add(item.fileno()) + else: + exclude_descriptors.add(item) + return exclude_descriptors + + def _make_signal_handler(self, target): + """ Make the signal handler for a specified target object. + + If `target` is ``None``, returns ``signal.SIG_IGN``. If + `target` is a string, returns the attribute of this + instance named by that string. Otherwise, returns `target` + itself. + + """ + if target is None: + result = signal.SIG_IGN + elif isinstance(target, basestring): + name = target + result = getattr(self, name) + else: + result = target + + return result + + def _make_signal_handler_map(self): + """ Make the map from signals to handlers for this instance. + + Constructs a map from signal numbers to handlers for this + context instance, suitable for passing to + `set_signal_handlers`. + + """ + signal_handler_map = dict( + (signal_number, self._make_signal_handler(target)) + for (signal_number, target) in self.signal_map.items()) + return signal_handler_map + + +def change_working_directory(directory): + """ Change the working directory of this process. + """ + try: + os.chdir(directory) + except Exception, exc: + error = DaemonOSEnvironmentError( + "Unable to change working directory (%(exc)s)" + % vars()) + raise error + + +def change_root_directory(directory): + """ Change the root directory of this process. + + Sets the current working directory, then the process root + directory, to the specified `directory`. Requires appropriate + OS privileges for this process. + + """ + try: + os.chdir(directory) + os.chroot(directory) + except Exception, exc: + error = DaemonOSEnvironmentError( + "Unable to change root directory (%(exc)s)" + % vars()) + raise error + + +def change_file_creation_mask(mask): + """ Change the file creation mask for this process. + """ + try: + os.umask(mask) + except Exception, exc: + error = DaemonOSEnvironmentError( + "Unable to change file creation mask (%(exc)s)" + % vars()) + raise error + + +def change_process_owner(uid, gid): + """ Change the owning UID and GID of this process. + + Sets the GID then the UID of the process (in that order, to + avoid permission errors) to the specified `gid` and `uid` + values. Requires appropriate OS privileges for this process. + + """ + try: + os.setgid(gid) + os.setuid(uid) + except Exception, exc: + error = DaemonOSEnvironmentError( + "Unable to change file creation mask (%(exc)s)" + % vars()) + raise error + + +def prevent_core_dump(): + """ Prevent this process from generating a core dump. + + Sets the soft and hard limits for core dump size to zero. On + Unix, this prevents the process from creating core dump + altogether. + + """ + core_resource = resource.RLIMIT_CORE + + try: + # Ensure the resource limit exists on this platform, by requesting + # its current value + core_limit_prev = resource.getrlimit(core_resource) + except ValueError, exc: + error = DaemonOSEnvironmentError( + "System does not support RLIMIT_CORE resource limit (%(exc)s)" + % vars()) + raise error + + # Set hard and soft limits to zero, i.e. no core dump at all + core_limit = (0, 0) + resource.setrlimit(core_resource, core_limit) + + +def detach_process_context(): + """ Detach the process context from parent and session. + + Detach from the parent process and session group, allowing the + parent to exit while this process continues running. + + Reference: “Advanced Programming in the Unix Environment”, + section 13.3, by W. Richard Stevens, published 1993 by + Addison-Wesley. + + """ + + def fork_then_exit_parent(error_message): + """ Fork a child process, then exit the parent process. + + If the fork fails, raise a ``DaemonProcessDetachError`` + with ``error_message``. + + """ + try: + pid = os.fork() + if pid > 0: + os._exit(0) + except OSError, exc: + exc_errno = exc.errno + exc_strerror = exc.strerror + error = DaemonProcessDetachError( + "%(error_message)s: [%(exc_errno)d] %(exc_strerror)s" % vars()) + raise error + + fork_then_exit_parent(error_message="Failed first fork") + os.setsid() + fork_then_exit_parent(error_message="Failed second fork") + + +def is_process_started_by_init(): + """ Determine if the current process is started by `init`. + + The `init` process has the process ID of 1; if that is our + parent process ID, return ``True``, otherwise ``False``. + + """ + result = False + + init_pid = 1 + if os.getppid() == init_pid: + result = True + + return result + + +def is_socket(fd): + """ Determine if the file descriptor is a socket. + + Return ``False`` if querying the socket type of `fd` raises an + error; otherwise return ``True``. + + """ + result = False + + file_socket = socket.fromfd(fd, socket.AF_INET, socket.SOCK_RAW) + + try: + socket_type = file_socket.getsockopt( + socket.SOL_SOCKET, socket.SO_TYPE) + except socket.error, exc: + exc_errno = exc.args[0] + if exc_errno == errno.ENOTSOCK: + # Socket operation on non-socket + pass + else: + # Some other socket error + result = True + else: + # No error getting socket type + result = True + + return result + + +def is_process_started_by_superserver(): + """ Determine if the current process is started by the superserver. + + The internet superserver creates a network socket, and + attaches it to the standard streams of the child process. If + that is the case for this process, return ``True``, otherwise + ``False``. + + """ + result = False + + stdin_fd = sys.__stdin__.fileno() + if is_socket(stdin_fd): + result = True + + return result + + +def is_detach_process_context_required(): + """ Determine whether detaching process context is required. + + Return ``True`` if the process environment indicates the + process is already detached: + + * Process was started by `init`; or + + * Process was started by `inetd`. + + """ + result = True + if is_process_started_by_init() or is_process_started_by_superserver(): + result = False + + return result + + +def close_file_descriptor_if_open(fd): + """ Close a file descriptor if already open. + + Close the file descriptor `fd`, suppressing an error in the + case the file was not open. + + """ + try: + os.close(fd) + except OSError, exc: + if exc.errno == errno.EBADF: + # File descriptor was not open + pass + else: + error = DaemonOSEnvironmentError( + "Failed to close file descriptor %(fd)d" + " (%(exc)s)" + % vars()) + raise error + + +MAXFD = 2048 + + +def get_maximum_file_descriptors(): + """ Return the maximum number of open file descriptors for this process. + + Return the process hard resource limit of maximum number of + open file descriptors. If the limit is “infinity”, a default + value of ``MAXFD`` is returned. + + """ + limits = resource.getrlimit(resource.RLIMIT_NOFILE) + result = limits[1] + if result == resource.RLIM_INFINITY: + result = MAXFD + return result + + +def close_all_open_files(exclude=set()): + """ Close all open file descriptors. + + Closes every file descriptor (if open) of this process. If + specified, `exclude` is a set of file descriptors to *not* + close. + + """ + maxfd = get_maximum_file_descriptors() + for fd in reversed(range(maxfd)): + if fd not in exclude: + close_file_descriptor_if_open(fd) + + +def redirect_stream(system_stream, target_stream): + """ Redirect a system stream to a specified file. + + `system_stream` is a standard system stream such as + ``sys.stdout``. `target_stream` is an open file object that + should replace the corresponding system stream object. + + If `target_stream` is ``None``, defaults to opening the + operating system's null device and using its file descriptor. + + """ + if target_stream is None: + target_fd = os.open(os.devnull, os.O_RDWR) + else: + target_fd = target_stream.fileno() + os.dup2(target_fd, system_stream.fileno()) + + +def make_default_signal_map(): + """ Make the default signal map for this system. + + The signals available differ by system. The map will not + contain any signals not defined on the running system. + + """ + name_map = { + 'SIGTSTP': None, + 'SIGTTIN': None, + 'SIGTTOU': None, + 'SIGTERM': 'terminate', + } + signal_map = dict( + (getattr(signal, name), target) + for (name, target) in name_map.items() + if hasattr(signal, name)) + + return signal_map + + +def set_signal_handlers(signal_handler_map): + """ Set the signal handlers as specified. + + The `signal_handler_map` argument is a map from signal number + to signal handler. See the `signal` module for details. + + """ + for (signal_number, handler) in signal_handler_map.items(): + signal.signal(signal_number, handler) + + +def register_atexit_function(func): + """ Register a function for processing at program exit. + + The function `func` is registered for a call with no arguments + at program exit. + + """ + atexit.register(func) diff --git a/log.py b/log.py index e415001..76052e2 100644 --- a/log.py +++ b/log.py @@ -24,6 +24,7 @@ import logging.handlers import os import sys
+ def getLogger(debug, background): logger = logging.getLogger("rhsm-app") logger.setLevel(logging.DEBUG) @@ -42,7 +43,7 @@ def getLogger(debug, background): if debug: fileHandler.setLevel(logging.DEBUG) else: - fileHandler.setLevel(logging.WARNING) + fileHandler.setLevel(logging.INFO) logger.addHandler(fileHandler) except Exception, e: sys.stderr.write("Unable to log to %s: %s\n" % (path, e)) @@ -53,7 +54,7 @@ def getLogger(debug, background): if debug: streamHandler.setLevel(logging.DEBUG) else: - streamHandler.setLevel(logging.WARNING) + streamHandler.setLevel(logging.INFO)
# Don't print exceptions to stdout in non-debug mode f = logging.Filter() diff --git a/manager/__init__.py b/manager/__init__.py new file mode 100644 index 0000000..31406e7 --- /dev/null +++ b/manager/__init__.py @@ -0,0 +1,2 @@ + +from manager import Manager, ManagerError diff --git a/manager/manager.py b/manager/manager.py new file mode 100644 index 0000000..ed104c2 --- /dev/null +++ b/manager/manager.py @@ -0,0 +1,24 @@ + + +class ManagerError(Exception): + pass + + +class Manager(object): + def sendVirtGuests(self, domains): + raise NotImplementedError() + + def hypervisorCheckIn(self, owner, env, mapping, type=None): + raise NotImplementedError() + + @classmethod + def fromOptions(cls, logger, options): + # Imports can't be top-level, it would be circular dependency + import subscriptionmanager + import satellite + + for subcls in cls.__subclasses__(): + if subcls.smType == options.smType: + return subcls(logger, options) + + raise KeyError("Invalid config type: %s" % options.smType) diff --git a/manager/satellite/__init__.py b/manager/satellite/__init__.py new file mode 100644 index 0000000..23b7650 --- /dev/null +++ b/manager/satellite/__init__.py @@ -0,0 +1,2 @@ + +from satellite import Satellite, SatelliteError diff --git a/satellite.py b/manager/satellite/satellite.py similarity index 79% rename from satellite.py rename to manager/satellite/satellite.py index 3c0a195..c1d6484 100644 --- a/satellite.py +++ b/manager/satellite/satellite.py @@ -18,37 +18,46 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """
-import sys -import os import xmlrpclib import pickle
-class SatelliteError(Exception): +from manager import Manager, ManagerError + + +class SatelliteError(ManagerError): def __init__(self, message): self.message = message
def __str__(self): return self.message
-class Satellite(object): + +class Satellite(Manager): + smType = "satellite" """ Class for interacting with satellite (RHN Classic). """ - HYPERVISOR_SYSTEMID_FILE="/var/lib/virt-who/hypervisor-systemid-%s" - def __init__(self, logger): + HYPERVISOR_SYSTEMID_FILE = "/var/lib/virt-who/hypervisor-systemid-%s" + + def __init__(self, logger, options): self.logger = logger self.server = None + self.options = options
- def connect(self, server, username, password, options=None, force_register=False): - if not server.startswith("http://") and not server.startswith("https://"): - server = "https://%s" % server - if not server.endswith("XMLRPC"): - server = "%s/XMLRPC" % server + def _connect(self): + if not self.options.server.startswith("http://") and not self.options.server.startswith("https://"): + self.options.server = "https://%s" % self.options.server + if not self.options.server.endswith("XMLRPC"): + self.options.server = "%s/XMLRPC" % self.options.server
- self.username = username - self.password = password + self.username = self.options.username + self.password = self.options.password + try: + self.force_register = self.options.force_register + except AttributeError: + self.force_register = False
- self.logger.debug("Initializing satellite connection to %s" % server) + self.logger.debug("Initializing satellite connection to %s" % self.options.server) try: - self.server = xmlrpclib.Server(server, verbose=0) + self.server = xmlrpclib.Server(self.options.server, verbose=0) except Exception: self.logger.exception("Unable to connect to the Satellite server") raise SatelliteError("Unable to connect to the Satellite server") @@ -58,14 +67,17 @@ class Satellite(object): systemid_filename = self.HYPERVISOR_SYSTEMID_FILE % hypervisor_uuid # attempt to read the existing systemid file for the hypervisor try: + if self.force_register: + raise IOError() self.logger.debug("Loading system id info from %s" % systemid_filename) new_system = pickle.load(open(systemid_filename, "rb")) except IOError: # assume file was not found, create a new hypervisor try: # TODO: what to do here? 6Server will consume subscription - new_system = self.server.registration.new_system_user_pass("%s hypervisor %s" % (type, hypervisor_uuid), - "unknown", "6Server", "x86_64", self.username, self.password, {}) + new_system = self.server.registration.new_system_user_pass( + "%s hypervisor %s" % (type, hypervisor_uuid), + "unknown", "6Server", "x86_64", self.username, self.password, {}) self.server.registration.refresh_hw_profile(new_system['system_id'], []) except Exception, e: self.logger.exception("Unable to refresh HW profile") @@ -95,17 +107,15 @@ class Satellite(object):
def _assemble_plan(self, hypervisor_mapping, hypervisor_uuid, type):
- # Get rid of dashes from UUID, spacewalk does not like them - #hypervisor_uuid = (str(hypervisor_uuid).replace("-", "")) events = []
# the stub_instance_info is not used by the report. When the guest system checks in, it will provide # actual hardware info stub_instance_info = { - 'vcpus' : 1, - 'memory_size' : 0, - 'virt_type' : 'fully_virtualized', - 'state' : 'running', + 'vcpus': 1, + 'memory_size': 0, + 'virt_type': 'fully_virtualized', + 'state': 'running', }
# again, remove dashes @@ -113,7 +123,6 @@ class Satellite(object): for g_uuid in hypervisor_mapping: guest_uuids.append(str(g_uuid).replace("-", ""))
- # TODO: spacewalk wants all zeroes for the hypervisor uuid?? events.append([0, 'exists', 'system', {'identity': 'host', 'uuid': '0000000000000000'}])
@@ -131,13 +140,15 @@ class Satellite(object): raise SatelliteError("virt-who does not support sending local hypervisor data to satellite; use rhn-virtualization-host instead")
def hypervisorCheckIn(self, owner, env, mapping, type=None): + self._connect()
+ self.logger.info("Sending update in hosts-to-guests mapping: %s" % mapping) if len(mapping) == 0: self.logger.info("no hypervisors found, not sending data to satellite")
for hypervisor_uuid, guest_uuids in mapping.items(): self.logger.debug("Loading systemid for %s" % hypervisor_uuid) - hypervisor_systemid = self._load_hypervisor(hypervisor_uuid, type=type) + hypervisor_systemid = self._load_hypervisor(hypervisor_uuid, type=type)
self.logger.debug("Building plan for hypervisor %s: %s" % (hypervisor_uuid, guest_uuids)) plan = self._assemble_plan(guest_uuids, hypervisor_uuid, type=type) diff --git a/manager/subscriptionmanager/__init__.py b/manager/subscriptionmanager/__init__.py new file mode 100644 index 0000000..771d9b9 --- /dev/null +++ b/manager/subscriptionmanager/__init__.py @@ -0,0 +1,2 @@ + +from subscriptionmanager import SubscriptionManager diff --git a/subscriptionmanager.py b/manager/subscriptionmanager/subscriptionmanager.py similarity index 80% rename from subscriptionmanager.py rename to manager/subscriptionmanager/subscriptionmanager.py index 0045d38..3b6ce5a 100644 --- a/subscriptionmanager.py +++ b/manager/subscriptionmanager/subscriptionmanager.py @@ -19,12 +19,14 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """
import os -import sys
import rhsm.connection as rhsm_connection import rhsm.certificate as rhsm_certificate import rhsm.config as rhsm_config
+from ..manager import Manager + + class SubscriptionManagerError(Exception): def __init__(self, message): self.message = message @@ -32,10 +34,14 @@ class SubscriptionManagerError(Exception): def __str__(self): return self.message
-class SubscriptionManager: + +class SubscriptionManager(Manager): + smType = "sam" + """ Class for interacting subscription-manager. """ - def __init__(self, logger): + def __init__(self, logger, options): self.logger = logger + self.options = options self.cert_uuid = None
self.config = rhsm_config.initConfig(rhsm_config.DEFAULT_CONFIG_PATH) @@ -55,17 +61,17 @@ class SubscriptionManager: if not os.access(self.cert_file, os.R_OK): raise SubscriptionManagerError("Unable to read certificate, system is not registered or you are not root")
- def connect(self, Connection=rhsm_connection.UEPConnection): + def _connect(self): """ Connect to the subscription-manager. """ - self.connection = Connection( - host=self.config.get('server', 'hostname'), - ssl_port=int(self.config.get('server', 'port')), - handler=self.config.get('server', 'prefix'), - proxy_hostname=self.config.get('server', 'proxy_hostname'), - proxy_port=self.config.get('server', 'proxy_port'), - proxy_user=self.config.get('server', 'proxy_user'), - proxy_password=self.config.get('server', 'proxy_password'), - cert_file=self.cert_file, key_file=self.key_file) + self.connection = rhsm_connection.UEPConnection( + host=self.config.get('server', 'hostname'), + ssl_port=int(self.config.get('server', 'port')), + handler=self.config.get('server', 'prefix'), + proxy_hostname=self.config.get('server', 'proxy_hostname'), + proxy_port=self.config.get('server', 'proxy_port'), + proxy_user=self.config.get('server', 'proxy_user'), + proxy_password=self.config.get('server', 'proxy_password'), + cert_file=self.cert_file, key_file=self.key_file) if not self.connection.ping()['result']: raise SubscriptionManagerError("Unable to obtain status from server, UEPConnection is likely not usable.")
@@ -88,6 +94,8 @@ class SubscriptionManager: :type domain: list of str or list of dict domains """
+ self._connect() + # Sort the list key = None if len(domains) > 0: @@ -96,17 +104,18 @@ class SubscriptionManager: domains.sort(key=key)
if key is not None: - self.logger.debug("Sending list of uuids: %s" % [domain[key] for domain in domains]) + self.logger.info("Sending list of uuids: %s" % [domain[key] for domain in domains]) else: - self.logger.debug("Sending list of uuids: %s" % domains) + self.logger.info("Sending list of uuids: %s" % domains)
# Send list of guest uuids to the server self.connection.updateConsumer(self.uuid(), guest_uuids=domains)
def hypervisorCheckIn(self, owner, env, mapping, type=None): """ Send hosts to guests mapping to subscription manager. """ + self.logger.info("Sending update in hosts-to-guests mapping: %s" % mapping)
- self.logger.debug("Sending update in hosts-to-guests mapping: %s" % mapping) + self._connect()
# Send the mapping return self.connection.hypervisorCheckIn(owner, env, mapping) diff --git a/virt-who b/virt-who index 959997b..f2ca438 100755 --- a/virt-who +++ b/virt-who @@ -1,10 +1,10 @@ #!/bin/sh
-if [ -f ./virt-who.py ]; +if [ -f ./virtwho.py ]; then # Run it from local directory when available - exec /usr/bin/python ./virt-who.py "$@" + exec /usr/bin/python ./virtwho.py "$@" else # Run it from /usr/share/virt-who - exec /usr/bin/python /usr/share/virt-who/virt-who.py "$@" + exec /usr/bin/python /usr/share/virt-who/virtwho.py "$@" fi diff --git a/virt-who-config.5 b/virt-who-config.5 new file mode 100644 index 0000000..a055912 --- /dev/null +++ b/virt-who-config.5 @@ -0,0 +1,50 @@ +.TH VIRT-WHO-CONFIG "5" "June 2014" "virt-who" +.SH NAME +virt-who-config - configuration for virt-who +.SH SYNOPISIS +/etc/virt-who.d/* +.SH DESCRIPTION +Configuration format is ini-like, where group name (in square brackets) is arbitrary name of the configuration. + +Only required key is \fBtype\fR that has to have one of the allowed virtualization backend names, see virt-who(8). + +Another options that could be supplied are: +.TP +\fBserver\fR +hostname, IP address or URL of the server that provides virtualization information (not applicable for libvirt and vdsm mode). +.TP +\fBusername\fR +username for authentication to the server (not applicable for libvirt and vdsm mode). +.TP +\fBpassword\fR +password for authentication to the server (not applicable for libvirt and vdsm mode). +.TP +\fBencrypted_password\fR +encrypted password that is generated by virt-who-password(8) utility +.TP +\fBowner\fR +owner for use with Subscription Asset Manager (not applicable for Satellite) +.TP +\fBenv\fR +environment for use with Subscription Asset Manager (not applicable for Satellite) + +.SH EXAMPLE +[test-esx] +.br +type=esx +.br +server=1.2.3.4 +.br +username=admin +.br +password=password +.br +owner=test +.br +env=staging + +.SH AUTHOR +Radek Novacek <rnovacek at redhat dot com> + +.SH SEE ALSO +virt-who(8), virt-who-password(8) diff --git a/virt-who.spec b/virt-who.spec index a5ef0b6..81619fb 100644 --- a/virt-who.spec +++ b/virt-who.spec @@ -39,6 +39,8 @@ rm -rf $RPM_BUILD_ROOT
make DESTDIR=$RPM_BUILD_ROOT install mkdir -p %{buildroot}/%{_sharedstatedir}/%{name} +mkdir -p %{buildroot}/%{_sysconfdir}/virt-who.d +touch %{buildroot}/%{_sharedstatedir}/%{name}/key
# Don't run test suite in check section, because it need the system to be # registered to subscription-manager server @@ -65,11 +67,16 @@ fi %files %doc README LICENSE %{_bindir}/virt-who +%{_bindir}/virt-who-password %{_datadir}/virt-who/ %{_sysconfdir}/rc.d/init.d/virt-who +%attr(600, root, root) %dir %{_sysconfdir}/virt-who.d %attr(600, root, root) %config(noreplace) %{_sysconfdir}/sysconfig/virt-who %{_mandir}/man8/virt-who.8.gz -%{_sharedstatedir}/%{name} +%{_mandir}/man8/virt-who-password.8.gz +%{_mandir}/man5/virt-who-config.5.gz +%attr(600, root, root) %{_sharedstatedir}/%{name} +%ghost %{_sharedstatedir}/%{name}/key
%changelog diff --git a/virt/__init__.py b/virt/__init__.py new file mode 100644 index 0000000..aebc492 --- /dev/null +++ b/virt/__init__.py @@ -0,0 +1,3 @@ + + +from virt import Virt, DirectVirt, HypervisorVirt, VirtError, Domain diff --git a/virt/esx/__init__.py b/virt/esx/__init__.py new file mode 100644 index 0000000..87db45d --- /dev/null +++ b/virt/esx/__init__.py @@ -0,0 +1,2 @@ + +from esx import Esx diff --git a/vsphere.py b/virt/esx/esx.py similarity index 90% rename from vsphere.py rename to virt/esx/esx.py index 20febd4..7330299 100644 --- a/vsphere.py +++ b/virt/esx/esx.py @@ -20,6 +20,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import sys import suds +from urllib2 import URLError + +import virt +
def get_search_filter_spec(client, begin_entity, property_spec): """ Build a PropertyFilterSpec capable of full inventory traversal. @@ -104,15 +108,17 @@ def get_search_filter_spec(client, begin_entity, property_spec): return pfs
-class VSphere: - def __init__(self, logger, url, username, password): +class Esx(virt.HypervisorVirt): + CONFIG_TYPE = "esx" + + def __init__(self, logger, config): self.logger = logger - self.url = url - self.username = username - self.password = password + self.url = config.server + self.username = config.username + self.password = config.password
# Url must contain protocol (usualy https://) - if not "://" in self.url: + if "://" not in self.url: self.url = "https://%s" % self.url
self.clusters = {} @@ -121,12 +127,16 @@ class VSphere:
def scan(self): """ - Scan method does full inventory traversal on the vCenter machine. It finds - all ComputeResources, Hosts and VirtualMachines. + Scan method does full inventory traversal on the vCenter machine. + It finds all ComputeResources, Hosts and VirtualMachines. """
# Connect to the vCenter server - self.client = suds.client.Client("%s/sdk/vimService.wsdl" % self.url) + try: + self.client = suds.client.Client("%s/sdk/vimService.wsdl" % self.url) + except URLError, e: + self.logger.exception("Unable to connect to ESX") + raise virt.VirtError(str(e))
self.client.set_options(location="%s/sdk" % self.url)
@@ -138,7 +148,11 @@ class VSphere: self.sc = self.client.service.RetrieveServiceContent(_this=self.moRef)
# Login to server using given credentials - self.client.service.Login(_this=self.sc.sessionManager, userName=self.username, password=self.password) + try: + self.client.service.Login(_this=self.sc.sessionManager, userName=self.username, password=self.password) + except suds.WebFault, e: + self.logger.exception("Unable to login to ESX") + raise virt.VirtError(str(e))
# Clear results from last run self.clusters = {} @@ -151,15 +165,17 @@ class VSphere: ts.pathSet = 'name' ts.all = True try: - retrieve_result = self.client.service.RetrievePropertiesEx(_this=self.sc.propertyCollector, - specSet=[get_search_filter_spec(self.client, self.sc.rootFolder, [ts])]) + retrieve_result = self.client.service.RetrievePropertiesEx( + _this=self.sc.propertyCollector, + specSet=[get_search_filter_spec(self.client, self.sc.rootFolder, [ts])]) if retrieve_result is None: object_content = [] else: object_content = retrieve_result[0] except suds.MethodNotFound: - object_content = self.client.service.RetrieveProperties(_this=self.sc.propertyCollector, - specSet=[get_search_filter_spec(self.client, self.sc.rootFolder, [ts])]) + object_content = self.client.service.RetrieveProperties( + _this=self.sc.propertyCollector, + specSet=[get_search_filter_spec(self.client, self.sc.rootFolder, [ts])])
# Get properties of each cluster clusterObjs = [] # List of objs for 'ComputeResource' query @@ -233,7 +249,6 @@ class VSphere: # This means that there is no guest on given host pass
- def ping(self): return True
@@ -311,16 +326,19 @@ class VSphere: for vm in host.vms: print "\t\tVirtualMachine: %s" % vm.uuid
+ class Cluster: def __init__(self, name): self.name = name self.hosts = []
+ class Host: def __init__(self): self.uuid = None self.vms = []
+ class VM: def __init__(self): self.uuid = None @@ -333,6 +351,8 @@ if __name__ == '__main__':
import logging logger = logging.Logger("") - vsphere = VSphere(logger, sys.argv[1], sys.argv[2], sys.argv[3]) + from config import Config + config = Config('esx', 'esx', sys.argv[1], sys.argv[2], sys.argv[3]) + vsphere = Esx(logger, config) vsphere.scan() vsphere.printLayout() diff --git a/virt/hyperv/__init__.py b/virt/hyperv/__init__.py new file mode 100644 index 0000000..28ff934 --- /dev/null +++ b/virt/hyperv/__init__.py @@ -0,0 +1,2 @@ + +from hyperv import HyperV diff --git a/hyperv.py b/virt/hyperv/hyperv.py similarity index 86% rename from hyperv.py rename to virt/hyperv/hyperv.py index 189366e..8b48c46 100644 --- a/hyperv.py +++ b/virt/hyperv/hyperv.py @@ -1,13 +1,35 @@ +""" +Module for communcating with Hyper-V, part of virt-who + +Copyright (C) 2014 Radek Novacek rnovacek@redhat.com + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty 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. +"""
import sys import httplib import urlparse import base64
+import virt + try: from uuid import uuid1 except ImportError: import subprocess + def uuid1(): # fallback to calling commandline uuidgen return subprocess.Popen(["uuidgen"], stdout=subprocess.PIPE).communicate()[0].strip() @@ -21,10 +43,10 @@ except ImportError: import ntlm
NAMESPACES = { - 's': 'http://www.w3.org/2003/05/soap-envelope', - 'wsa': 'http://schemas.xmlsoap.org/ws/2004/08/addressing', + 's': 'http://www.w3.org/2003/05/soap-envelope', + 'wsa': 'http://schemas.xmlsoap.org/ws/2004/08/addressing', 'wsman': 'http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd', - 'wsen': 'http://schemas.xmlsoap.org/ws/2004/09/enumeration' + 'wsen': 'http://schemas.xmlsoap.org/ws/2004/09/enumeration' }
ENVELOPE = """<?xml version="1.0" encoding="UTF-8"?> @@ -33,6 +55,7 @@ ENVELOPE = """<?xml version="1.0" encoding="UTF-8"?> %s </s:Envelope>"""
+ def getHeader(action): return """<s:Header> <wsa:Action s:mustUnderstand="true">""" + NAMESPACES['wsen'] + "/" + action + """</wsa:Action> @@ -44,21 +67,25 @@ def getHeader(action): </wsa:ReplyTo> </s:Header>"""
+ ENUMERATE_BODY = """<s:Body> wsen:Enumerate <wsman:Filter Dialect="http://schemas.microsoft.com/wbem/wsman/1/WQL%22%3E%(query)s</wsman:Filter> </wsen:Enumerate> </s:Body>"""
+ PULL_BODY = """<s:Body> wsen:Pull wsen:EnumerationContext%(EnumerationContext)s</wsen:EnumerationContext> </wsen:Pull> </s:Body>"""
+ ENUMERATE_XML = ENVELOPE % (getHeader("Enumerate"), ENUMERATE_BODY) PULL_XML = ENVELOPE % (getHeader("Pull"), PULL_BODY)
+ class HyperVSoap(object): def __init__(self, url, connection, headers): self.url = url @@ -92,7 +119,7 @@ class HyperVSoap(object): return properties
def Enumerate(self, query, namespace="root/virtualization"): - data = ENUMERATE_XML % { 'url': self.url, 'query': query, 'namespace': namespace } + data = ENUMERATE_XML % {'url': self.url, 'query': query, 'namespace': namespace} response = self.post(data) d = response.read() xml = ElementTree.fromstring(d) @@ -110,7 +137,7 @@ class HyperVSoap(object): return contexts[0].text
def _PullOne(self, uuid, namespace): - data = PULL_XML % { 'url': self.url, 'EnumerationContext': uuid, 'namespace': namespace } + data = PULL_XML % {'url': self.url, 'EnumerationContext': uuid, 'namespace': namespace} response = self.post(data) d = response.read() xml = ElementTree.fromstring(d) @@ -140,18 +167,22 @@ class HyperVSoap(object): return instances
-class HyperVException(Exception): +class HyperVException(virt.VirtError): pass
+ class HyperVAuthFailed(HyperVException): pass
-class HyperV: - def __init__(self, logger, url, username, password): +class HyperV(virt.HypervisorVirt): + CONFIG_TYPE = "hyperv" + + def __init__(self, logger, config): self.logger = logger - self.username = username - self.password = password + url = config.server + self.username = config.username + self.password = config.password
# First try to use old API (root/virtualization namespace) if doesn't # work, go with root/virtualization/v2 @@ -177,7 +208,7 @@ class HyperV: logger.debug("Hyper-V url: %s" % self.url)
# Check if we have domain defined and set flags accordingly - user_parts = username.split('\', 1) + user_parts = self.username.split('\', 1) if len(user_parts) == 1: self.username = user_parts[0] self.domainname = '' @@ -285,8 +316,10 @@ class HyperV: else: # Filter out Planned VMs and snapshots, see # http://msdn.microsoft.com/en-us/library/hh850257%28v=vs.85%29.aspx - uuid = hypervsoap.Enumerate("select BIOSGUID from Msvm_VirtualSystemSettingData " - "where VirtualSystemType = 'Microsoft:Hyper-V:System:Realized'", "root/virtualization/v2") + uuid = hypervsoap.Enumerate( + "select BIOSGUID from Msvm_VirtualSystemSettingData " + "where VirtualSystemType = 'Microsoft:Hyper-V:System:Realized'", + "root/virtualization/v2") except HyperVException, e: if not self.useNewApi: self.logger.debug("Error when enumerating using root/virtualization namespace, trying root/virtualization/v2 namespace") @@ -300,7 +333,7 @@ class HyperV: host = None for instance in hypervsoap.Pull(uuid, "root/cimv2"): host = HyperV.decodeWinUUID(instance["UUID"]) - return { host: guests } + return {host: guests}
def ping(self): return True diff --git a/ntlm.py b/virt/hyperv/ntlm.py similarity index 74% rename from ntlm.py rename to virt/hyperv/ntlm.py index 32d33c3..c8d9c36 100644 --- a/ntlm.py +++ b/virt/hyperv/ntlm.py @@ -29,16 +29,20 @@ from socket import gethostname
import M2Crypto
+ # Use md4 from hashlib or directly from openssl class OpenSslMd4(object): def __init__(self, data): self.data = data + def digest(self): return subprocess.Popen(["openssl", "md4", "-binary"], stdin=subprocess.PIPE, stdout=subprocess.PIPE).communicate(self.data)[0]
+ class HashlibMd4(object): def __init__(self, data): self.md4 = hashlib.new('md4', data) + def digest(self): return self.md4.digest()
@@ -47,6 +51,7 @@ if 'md4' in algorithms: else: md4 = OpenSslMd4
+ # DES handling functions
def des_encrypt(key_str, plain_text): @@ -55,36 +60,41 @@ def des_encrypt(key_str, plain_text): key_str = '' for i in k: key_str += chr(i & 0xFF) - des = M2Crypto.EVP.Cipher("des_ecb", key=key_str, op=M2Crypto.encrypt, iv='\0'*16) + des = M2Crypto.EVP.Cipher("des_ecb", key=key_str, op=M2Crypto.encrypt, iv='\0' * 16)
return des.update(plain_text)
+ def str_to_key56(key_str): if len(key_str) < 7: key_str = key_str + '\000\000\000\000\000\000\000'[:(7 - len(key_str))] key_56 = [] - for i in key_str[:7]: key_56.append(ord(i)) + for i in key_str[:7]: + key_56.append(ord(i))
return key_56
+ def key56_to_key64(key_56): "" key = [] - for i in range(8): key.append(0) - - key[0] = key_56[0]; - key[1] = ((key_56[0] << 7) & 0xFF) | (key_56[1] >> 1); - key[2] = ((key_56[1] << 6) & 0xFF) | (key_56[2] >> 2); - key[3] = ((key_56[2] << 5) & 0xFF) | (key_56[3] >> 3); - key[4] = ((key_56[3] << 4) & 0xFF) | (key_56[4] >> 4); - key[5] = ((key_56[4] << 3) & 0xFF) | (key_56[5] >> 5); - key[6] = ((key_56[5] << 2) & 0xFF) | (key_56[6] >> 6); - key[7] = (key_56[6] << 1) & 0xFF; + for i in range(8): + key.append(0) + + key[0] = key_56[0] + key[1] = ((key_56[0] << 7) & 0xFF) | (key_56[1] >> 1) + key[2] = ((key_56[1] << 6) & 0xFF) | (key_56[2] >> 2) + key[3] = ((key_56[2] << 5) & 0xFF) | (key_56[3] >> 3) + key[4] = ((key_56[3] << 4) & 0xFF) | (key_56[4] >> 4) + key[5] = ((key_56[4] << 3) & 0xFF) | (key_56[5] >> 5) + key[6] = ((key_56[5] << 2) & 0xFF) | (key_56[6] >> 6) + key[7] = (key_56[6] << 1) & 0xFF
key = set_key_odd_parity(key)
return key
+ def set_key_odd_parity(key): "" for i in range(len(key)): @@ -132,25 +142,25 @@ NTLM_NegotiateKeyExchange = 0x40000000 NTLM_Negotiate56 = 0x80000000
# we send these flags with our type 1 message -NTLM_TYPE1_FLAGS = (NTLM_NegotiateUnicode | \ - NTLM_NegotiateOEM | \ - NTLM_RequestTarget | \ - NTLM_NegotiateNTLM | \ - NTLM_NegotiateOemDomainSupplied | \ - NTLM_NegotiateOemWorkstationSupplied | \ - NTLM_NegotiateAlwaysSign | \ - NTLM_NegotiateExtendedSecurity | \ - NTLM_NegotiateVersion | \ - NTLM_Negotiate128 | \ - NTLM_Negotiate56 ) -NTLM_TYPE2_FLAGS = (NTLM_NegotiateUnicode | \ - NTLM_RequestTarget | \ - NTLM_NegotiateNTLM | \ - NTLM_NegotiateAlwaysSign | \ - NTLM_NegotiateExtendedSecurity | \ - NTLM_NegotiateTargetInfo | \ - NTLM_NegotiateVersion | \ - NTLM_Negotiate128 | \ +NTLM_TYPE1_FLAGS = (NTLM_NegotiateUnicode | + NTLM_NegotiateOEM | + NTLM_RequestTarget | + NTLM_NegotiateNTLM | + NTLM_NegotiateOemDomainSupplied | + NTLM_NegotiateOemWorkstationSupplied | + NTLM_NegotiateAlwaysSign | + NTLM_NegotiateExtendedSecurity | + NTLM_NegotiateVersion | + NTLM_Negotiate128 | + NTLM_Negotiate56) +NTLM_TYPE2_FLAGS = (NTLM_NegotiateUnicode | + NTLM_RequestTarget | + NTLM_NegotiateNTLM | + NTLM_NegotiateAlwaysSign | + NTLM_NegotiateExtendedSecurity | + NTLM_NegotiateTargetInfo | + NTLM_NegotiateVersion | + NTLM_Negotiate128 | NTLM_Negotiate56)
NTLM_MsvAvEOL = 0 # Indicates that this is the last AV_PAIR in the list. AvLen MUST be 0. This type of information MUST be present in the AV pair list. @@ -161,7 +171,7 @@ NTLM_MsvAvDnsDomainName = 4 # The server's Active Directory DNS domain name. T NTLM_MsvAvDnsTreeName = 5 # The server's Active Directory (AD) DNS forest tree name. The name MUST be in Unicode, and is not null-terminated. NTLM_MsvAvFlags = 6 # A field containing a 32-bit value indicating server or client configuration. 0x00000001: indicates to the client that the account authentication is constrained. 0x00000002: indicates that the client is providing message integrity in the MIC field (section 2.2.1.3) in the AUTHENTICATE_MESSAGE. NTLM_MsvAvTimestamp = 7 # A FILETIME structure ([MS-DTYP] section 2.3.1) in little-endian byte order that contains the server local time.<12> -NTLM_MsAvRestrictions = 8 #A Restriction_Encoding structure (section 2.2.2.2). The Value field contains a structure representing the integrity level of the security principal, as well as a MachineID created at computer startup to identify the calling machine. <13> +NTLM_MsAvRestrictions = 8 # A Restriction_Encoding structure (section 2.2.2.2). The Value field contains a structure representing the integrity level of the security principal, as well as a MachineID created at computer startup to identify the calling machine. <13>
""" @@ -183,6 +193,8 @@ http://sourceforge.net/projects/ntlmaps/ Optimized Attack for NTLM2 Session Response http://www.blackhat.com/presentations/bh-asia-04/bh-jp-04-pdfs/bh-jp-04-seki... """ + + def dump_NegotiateFlags(NegotiateFlags): if NegotiateFlags & NTLM_NegotiateUnicode: print "NTLM_NegotiateUnicode set" @@ -249,14 +261,15 @@ def dump_NegotiateFlags(NegotiateFlags): if NegotiateFlags & NTLM_Negotiate56: print "NTLM_Negotiate56 set"
+ def create_NTLM_NEGOTIATE_MESSAGE(user, type1_flags=NTLM_TYPE1_FLAGS): BODY_LENGTH = 40 Payload_start = BODY_LENGTH # in bytes - protocol = 'NTLMSSP\0' #name + protocol = 'NTLMSSP\0' # name
- type = struct.pack('<I',1) #type 1 + type = struct.pack('<I', 1) # type 1
- flags = struct.pack('<I', type1_flags) + flags = struct.pack('<I', type1_flags) Workstation = gethostname().upper().encode('ascii') user_parts = user.split('\', 1) DomainName = user_parts[0].upper().encode('ascii') @@ -268,7 +281,7 @@ def create_NTLM_NEGOTIATE_MESSAGE(user, type1_flags=NTLM_TYPE1_FLAGS): Payload_start += len(Workstation) DomainNameLen = struct.pack('<H', len(DomainName)) DomainNameMaxLen = struct.pack('<H', len(DomainName)) - DomainNameBufferOffset = struct.pack('<I',Payload_start) + DomainNameBufferOffset = struct.pack('<I', Payload_start) Payload_start += len(DomainName) ProductMajorVersion = struct.pack('<B', 5) ProductMinorVersion = struct.pack('<B', 1) @@ -279,51 +292,52 @@ def create_NTLM_NEGOTIATE_MESSAGE(user, type1_flags=NTLM_TYPE1_FLAGS): NTLMRevisionCurrent = struct.pack('<B', 15)
msg1 = protocol + type + flags + \ - DomainNameLen + DomainNameMaxLen + DomainNameBufferOffset + \ - WorkstationLen + WorkstationMaxLen + WorkstationBufferOffset + \ - ProductMajorVersion + ProductMinorVersion + ProductBuild + \ - VersionReserved1 + VersionReserved2 + VersionReserved3 + NTLMRevisionCurrent - assert BODY_LENGTH==len(msg1), "BODY_LENGTH: %d != msg1: %d" % (BODY_LENGTH,len(msg1)) + DomainNameLen + DomainNameMaxLen + DomainNameBufferOffset + \ + WorkstationLen + WorkstationMaxLen + WorkstationBufferOffset + \ + ProductMajorVersion + ProductMinorVersion + ProductBuild + \ + VersionReserved1 + VersionReserved2 + VersionReserved3 + NTLMRevisionCurrent + assert BODY_LENGTH == len(msg1), "BODY_LENGTH: %d != msg1: %d" % (BODY_LENGTH, len(msg1)) msg1 += Workstation + DomainName msg1 = base64.encodestring(msg1) msg1 = string.replace(msg1, '\n', '') return msg1
+ def parse_NTLM_CHALLENGE_MESSAGE(msg2): "" msg2 = base64.decodestring(msg2) Signature = msg2[0:8] - msg_type = struct.unpack("<I",msg2[8:12])[0] - assert(msg_type==2) - TargetNameLen = struct.unpack("<H",msg2[12:14])[0] - TargetNameMaxLen = struct.unpack("<H",msg2[14:16])[0] - TargetNameOffset = struct.unpack("<I",msg2[16:20])[0] - TargetName = msg2[TargetNameOffset:TargetNameOffset+TargetNameMaxLen] - NegotiateFlags = struct.unpack("<I",msg2[20:24])[0] + msg_type = struct.unpack("<I", msg2[8:12])[0] + assert(msg_type == 2) + TargetNameLen = struct.unpack("<H", msg2[12:14])[0] + TargetNameMaxLen = struct.unpack("<H", msg2[14:16])[0] + TargetNameOffset = struct.unpack("<I", msg2[16:20])[0] + TargetName = msg2[TargetNameOffset:TargetNameOffset + TargetNameMaxLen] + NegotiateFlags = struct.unpack("<I", msg2[20:24])[0] ServerChallenge = msg2[24:32] Reserved = msg2[32:40] - TargetInfoLen = struct.unpack("<H",msg2[40:42])[0] - TargetInfoMaxLen = struct.unpack("<H",msg2[42:44])[0] - TargetInfoOffset = struct.unpack("<I",msg2[44:48])[0] - TargetInfo = msg2[TargetInfoOffset:TargetInfoOffset+TargetInfoLen] - i=0 - TimeStamp = '\0'*8 - while(i<TargetInfoLen): - AvId = struct.unpack("<H",TargetInfo[i:i+2])[0] - AvLen = struct.unpack("<H",TargetInfo[i+2:i+4])[0] - AvValue = TargetInfo[i+4:i+4+AvLen] - i = i+4+AvLen + TargetInfoLen = struct.unpack("<H", msg2[40:42])[0] + TargetInfoMaxLen = struct.unpack("<H", msg2[42:44])[0] + TargetInfoOffset = struct.unpack("<I", msg2[44:48])[0] + TargetInfo = msg2[TargetInfoOffset:TargetInfoOffset + TargetInfoLen] + i = 0 + TimeStamp = '\0' * 8 + while i < TargetInfoLen: + AvId = struct.unpack("<H", TargetInfo[i:i + 2])[0] + AvLen = struct.unpack("<H", TargetInfo[i + 2:i + 4])[0] + AvValue = TargetInfo[i + 4:i + 4 + AvLen] + i = i + 4 + AvLen if AvId == NTLM_MsvAvTimestamp: TimeStamp = AvValue - #~ print AvId, AvValue.decode('utf-16') return (ServerChallenge, NegotiateFlags)
+ def create_NTLM_AUTHENTICATE_MESSAGE(nonce, user, domain, password, NegotiateFlags): "" - is_unicode = NegotiateFlags & NTLM_NegotiateUnicode + is_unicode = NegotiateFlags & NTLM_NegotiateUnicode is_NegotiateExtendedSecurity = NegotiateFlags & NTLM_NegotiateExtendedSecurity
- flags = struct.pack('<I',NTLM_TYPE2_FLAGS) + flags = struct.pack('<I', NTLM_TYPE2_FLAGS)
BODY_LENGTH = 72 Payload_start = BODY_LENGTH # in bytes @@ -344,10 +358,10 @@ def create_NTLM_AUTHENTICATE_MESSAGE(nonce, user, domain, password, NegotiateFla pwhash = create_NT_hashed_password_v1(password, UserName, DomainName) ClientChallenge = "" for i in range(8): - ClientChallenge+= chr(random.getrandbits(8)) - (NtChallengeResponse, LmChallengeResponse) = ntlm2sr_calc_resp(pwhash, nonce, ClientChallenge) #='\x39 e3 f4 cd 59 c5 d8 60') + ClientChallenge += chr(random.getrandbits(8)) + (NtChallengeResponse, LmChallengeResponse) = ntlm2sr_calc_resp(pwhash, nonce, ClientChallenge) # ='\x39 e3 f4 cd 59 c5 d8 60') Signature = 'NTLMSSP\0' - MessageType = struct.pack('<I',3) #type 3 + MessageType = struct.pack('<I', 3) # type 3
DomainNameLen = struct.pack('<H', len(DomainName)) DomainNameMaxLen = struct.pack('<H', len(DomainName)) @@ -376,8 +390,8 @@ def create_NTLM_AUTHENTICATE_MESSAGE(nonce, user, domain, password, NegotiateFla
EncryptedRandomSessionKeyLen = struct.pack('<H', len(EncryptedRandomSessionKey)) EncryptedRandomSessionKeyMaxLen = struct.pack('<H', len(EncryptedRandomSessionKey)) - EncryptedRandomSessionKeyOffset = struct.pack('<I',Payload_start) - Payload_start += len(EncryptedRandomSessionKey) + EncryptedRandomSessionKeyOffset = struct.pack('<I', Payload_start) + Payload_start += len(EncryptedRandomSessionKey) NegotiateFlags = flags
ProductMajorVersion = struct.pack('<B', 5) @@ -388,24 +402,25 @@ def create_NTLM_AUTHENTICATE_MESSAGE(nonce, user, domain, password, NegotiateFla VersionReserved3 = struct.pack('<B', 0) NTLMRevisionCurrent = struct.pack('<B', 15)
- MIC = struct.pack('<IIII',0,0,0,0) + MIC = struct.pack('<IIII', 0, 0, 0, 0) msg3 = Signature + MessageType + \ - LmChallengeResponseLen + LmChallengeResponseMaxLen + LmChallengeResponseOffset + \ - NtChallengeResponseLen + NtChallengeResponseMaxLen + NtChallengeResponseOffset + \ - DomainNameLen + DomainNameMaxLen + DomainNameOffset + \ - UserNameLen + UserNameMaxLen + UserNameOffset + \ - WorkstationLen + WorkstationMaxLen + WorkstationOffset + \ - EncryptedRandomSessionKeyLen + EncryptedRandomSessionKeyMaxLen + EncryptedRandomSessionKeyOffset + \ - NegotiateFlags + \ - ProductMajorVersion + ProductMinorVersion + ProductBuild + \ - VersionReserved1 + VersionReserved2 + VersionReserved3 + NTLMRevisionCurrent - assert BODY_LENGTH==len(msg3), "BODY_LENGTH: %d != msg3: %d" % (BODY_LENGTH,len(msg3)) + LmChallengeResponseLen + LmChallengeResponseMaxLen + LmChallengeResponseOffset + \ + NtChallengeResponseLen + NtChallengeResponseMaxLen + NtChallengeResponseOffset + \ + DomainNameLen + DomainNameMaxLen + DomainNameOffset + \ + UserNameLen + UserNameMaxLen + UserNameOffset + \ + WorkstationLen + WorkstationMaxLen + WorkstationOffset + \ + EncryptedRandomSessionKeyLen + EncryptedRandomSessionKeyMaxLen + EncryptedRandomSessionKeyOffset + \ + NegotiateFlags + \ + ProductMajorVersion + ProductMinorVersion + ProductBuild + \ + VersionReserved1 + VersionReserved2 + VersionReserved3 + NTLMRevisionCurrent + assert BODY_LENGTH == len(msg3), "BODY_LENGTH: %d != msg3: %d" % (BODY_LENGTH, len(msg3)) Payload = DomainName + UserName + Workstation + LmChallengeResponse + NtChallengeResponse + EncryptedRandomSessionKey msg3 += Payload msg3 = base64.encodestring(msg3) msg3 = string.replace(msg3, '\n', '') return msg3
+ def calc_resp(password_hash, server_challenge): """calc_resp generates the LM response given a 16-byte password hash and the challenge from the Type-2 message. @@ -418,28 +433,31 @@ def calc_resp(password_hash, server_challenge): """ # padding with zeros to make the hash 21 bytes long password_hash = password_hash + '\0' * (21 - len(password_hash)) - return des_encrypt(password_hash[ 0: 7], server_challenge[0:8]) + \ - des_encrypt(password_hash[ 7:14], server_challenge[0:8]) + \ - des_encrypt(password_hash[14:21], server_challenge[0:8]) + return des_encrypt(password_hash[0: 7], server_challenge[0:8]) + \ + des_encrypt(password_hash[7:14], server_challenge[0:8]) + \ + des_encrypt(password_hash[14:21], server_challenge[0:8]) +
-def ComputeResponse(ResponseKeyNT, ResponseKeyLM, ServerChallenge, ServerName, ClientChallenge='\xaa'*8, Time='\0'*8): - LmChallengeResponse = hmac.new(ResponseKeyLM, ServerChallenge+ClientChallenge).digest() + ClientChallenge +def ComputeResponse(ResponseKeyNT, ResponseKeyLM, ServerChallenge, ServerName, ClientChallenge='\xaa' * 8, Time='\0' * 8): + LmChallengeResponse = hmac.new(ResponseKeyLM, ServerChallenge + ClientChallenge).digest() + ClientChallenge
Responserversion = '\x01' HiResponserversion = '\x01' - temp = Responserversion + HiResponserversion + '\0'*6 + Time + ClientChallenge + '\0'*4 + ServerChallenge + '\0'*4 - NTProofStr = hmac.new(ResponseKeyNT, ServerChallenge + temp).digest() + temp = Responserversion + HiResponserversion + '\0' * 6 + Time + ClientChallenge + '\0' * 4 + ServerChallenge + '\0' * 4 + NTProofStr = hmac.new(ResponseKeyNT, ServerChallenge + temp).digest() NtChallengeResponse = NTProofStr + temp
SessionBaseKey = hmac.new(ResponseKeyNT, NTProofStr).digest() return (NtChallengeResponse, LmChallengeResponse)
-def ntlm2sr_calc_resp(ResponseKeyNT, ServerChallenge, ClientChallenge='\xaa'*8): - LmChallengeResponse = ClientChallenge + '\0'*16 - sess = md5(ServerChallenge+ClientChallenge).digest() + +def ntlm2sr_calc_resp(ResponseKeyNT, ServerChallenge, ClientChallenge='\xaa' * 8): + LmChallengeResponse = ClientChallenge + '\0' * 16 + sess = md5(ServerChallenge + ClientChallenge).digest() NtChallengeResponse = calc_resp(ResponseKeyNT, sess[0:8]) return (NtChallengeResponse, LmChallengeResponse)
+ def create_LM_hashed_password_v1(passwd): "setup LanManager password" "create LanManager hashed password" @@ -454,66 +472,70 @@ def create_LM_hashed_password_v1(passwd):
return des_encrypt(lm_pw[0:7], magic_str) + des_encrypt(lm_pw[7:14], magic_str)
+ def create_NT_hashed_password_v1(passwd, user=None, domain=None): "create NT hashed password" digest = md4(passwd.encode('utf-16le')).digest() return digest
+ def create_NT_hashed_password_v2(passwd, user, domain): "create NT hashed password" digest = create_NT_hashed_password_v1(passwd)
- return hmac.new(digest, (user.upper()+domain).encode('utf-16le')).digest() + return hmac.new(digest, (user.upper() + domain).encode('utf-16le')).digest() return digest
+ def create_sessionbasekey(password): return md4(create_NT_hashed_password_v1(password)).digest()
+ if __name__ == "__main__": - def ByteToHex( byteStr ): + def ByteToHex(byteStr): """ Convert a byte string to it's hex string representation e.g. for output. """ - return ' '.join( [ "%02X" % ord( x ) for x in byteStr ] ) + return ' '.join(["%02X" % ord(x) for x in byteStr])
- def HexToByte( hexStr ): + def HexToByte(hexStr): """ Convert a string hex byte values into a byte string. The Hex Byte values may or may not be space separated. """ bytes = []
- hexStr = ''.join( hexStr.split(" ") ) + hexStr = ''.join(hexStr.split(" "))
for i in range(0, len(hexStr), 2): - bytes.append( chr( int (hexStr[i:i+2], 16 ) ) ) + bytes.append(chr(int(hexStr[i:i + 2], 16)))
- return ''.join( bytes ) + return ''.join(bytes)
ServerChallenge = HexToByte("01 23 45 67 89 ab cd ef") - ClientChallenge = '\xaa'*8 - Time = '\x00'*8 + ClientChallenge = '\xaa' * 8 + Time = '\x00' * 8 Workstation = "COMPUTER".encode('utf-16-le') ServerName = "Server".encode('utf-16-le') User = "User" Domain = "Domain" Password = "Password" - RandomSessionKey = '\55'*16 + RandomSessionKey = '\55' * 16 assert HexToByte("e5 2c ac 67 41 9a 9a 22 4a 3b 10 8f 3f a6 cb 6d") == create_LM_hashed_password_v1(Password) # [MS-NLMP] page 72 assert HexToByte("a4 f4 9c 40 65 10 bd ca b6 82 4e e7 c3 0f d8 52") == create_NT_hashed_password_v1(Password) # [MS-NLMP] page 73 assert HexToByte("d8 72 62 b0 cd e4 b1 cb 74 99 be cc cd f1 07 84") == create_sessionbasekey(Password) assert HexToByte("67 c4 30 11 f3 02 98 a2 ad 35 ec e6 4f 16 33 1c 44 bd be d9 27 84 1f 94") == calc_resp(create_NT_hashed_password_v1(Password), ServerChallenge) assert HexToByte("98 de f7 b8 7f 88 aa 5d af e2 df 77 96 88 a1 72 de f1 1c 7d 5c cd ef 13") == calc_resp(create_LM_hashed_password_v1(Password), ServerChallenge)
- (NTLMv1Response,LMv1Response) = ntlm2sr_calc_resp(create_NT_hashed_password_v1(Password), ServerChallenge, ClientChallenge) + (NTLMv1Response, LMv1Response) = ntlm2sr_calc_resp(create_NT_hashed_password_v1(Password), ServerChallenge, ClientChallenge) assert HexToByte("aa aa aa aa aa aa aa aa 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00") == LMv1Response # [MS-NLMP] page 75 assert HexToByte("75 37 f8 03 ae 36 71 28 ca 45 82 04 bd e7 ca f8 1e 97 ed 26 83 26 72 32") == NTLMv1Response
assert HexToByte("0c 86 8a 40 3b fd 7a 93 a3 00 1e f2 2e f0 2e 3f") == create_NT_hashed_password_v2(Password, User, Domain) # [MS-NLMP] page 76 ResponseKeyLM = ResponseKeyNT = create_NT_hashed_password_v2(Password, User, Domain) - (NTLMv2Response,LMv2Response) = ComputeResponse(ResponseKeyNT, ResponseKeyLM, ServerChallenge, ServerName, ClientChallenge, Time) + (NTLMv2Response, LMv2Response) = ComputeResponse(ResponseKeyNT, ResponseKeyLM, ServerChallenge, ServerName, ClientChallenge, Time) assert HexToByte("86 c3 50 97 ac 9c ec 10 25 54 76 4a 57 cc cc 19 aa aa aa aa aa aa aa aa") == LMv2Response # [MS-NLMP] page 76
# expected failure # According to the spec in section '3.3.2 NTLM v2 Authentication' the NTLMv2Response should be longer than the value given on page 77 (this suggests a mistake in the spec) - #~ assert HexToByte("68 cd 0a b8 51 e5 1c 96 aa bc 92 7b eb ef 6a 1c") == NTLMv2Response, "\nExpected: 68 cd 0a b8 51 e5 1c 96 aa bc 92 7b eb ef 6a 1c\nActual: %s" % ByteToHex(NTLMv2Response) # [MS-NLMP] page 77 + # ~ assert HexToByte("68 cd 0a b8 51 e5 1c 96 aa bc 92 7b eb ef 6a 1c") == NTLMv2Response, "\nExpected: 68 cd 0a b8 51 e5 1c 96 aa bc 92 7b eb ef 6a 1c\nActual: %s" % ByteToHex(NTLMv2Response) # [MS-NLMP] page 77 diff --git a/virt/libvirtd/__init__.py b/virt/libvirtd/__init__.py new file mode 100644 index 0000000..4c8b00c --- /dev/null +++ b/virt/libvirtd/__init__.py @@ -0,0 +1,2 @@ + +from libvirtd import Libvirtd diff --git a/virt/libvirtd/libvirtd.py b/virt/libvirtd/libvirtd.py new file mode 100644 index 0000000..b0b8999 --- /dev/null +++ b/virt/libvirtd/libvirtd.py @@ -0,0 +1,168 @@ +""" +Module for communcating with libvirt, part of virt-who + +Copyright (C) 2014 Radek Novacek rnovacek@redhat.com + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty 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. +""" + +import time +import logging +import libvirt +import threading +from event import virEventLoopPureStart + +import virt + + +eventLoopThread = None + + +class LibvirtMonitor(threading.Thread): + """ Singleton class that performs background event monitoring. """ + _instance = None + + def __new__(cls, *args, **kwargs): + if not cls._instance: + cls._instance = super(LibvirtMonitor, cls).__new__(cls, *args, **kwargs) + cls._instance.event = None + cls._instance.terminate = threading.Event() + cls._instance.domainIds = [] + cls._instance.definedDomains = [] + return cls._instance + + STATUS_NOT_STARTED, STATUS_RUNNING, STATUS_DISABLED = range(3) + + def _prepare(self): + self.logger = logging.getLogger("rhsm-app") + self.running = threading.Event() + self.status = LibvirtMonitor.STATUS_NOT_STARTED + + def run(self): + self._prepare() + while not self.terminate.isSet(): + self._checkChange() + time.sleep(5) + + def _checkChange(self): + try: + virt = libvirt.openReadOnly('') + except libvirt.libvirtError, e: + # Show error only once + if self.status != LibvirtMonitor.STATUS_DISABLED: + self.logger.debug("Unable to connect to libvirtd, disabling event monitoring") + self.logger.exception(e) + self.status = LibvirtMonitor.STATUS_DISABLED + return + + domainIds = virt.listDomainsID() + definedDomains = virt.listDefinedDomains() + + changed = domainIds != self.domainIds or definedDomains != self.definedDomains + + self.domainIds = domainIds + self.definedDomains = definedDomains + + virt.close() + + if changed and self.event is not None and self.status != LibvirtMonitor.STATUS_NOT_STARTED: + self.event.set() + + if self.status == LibvirtMonitor.STATUS_DISABLED: + self.logger.debug("Event monitoring resumed") + self.status = LibvirtMonitor.STATUS_RUNNING + + def set_event(self, event): + self.event = event + + def stop(self): + self.terminate.set() + + +class Libvirtd(virt.DirectVirt): + """ Class for interacting with libvirt. """ + CONFIG_TYPE = "libvirt" + + def __init__(self, logger, config, registerEvents=True): + self.changedCallback = None + self.logger = logger + self.registerEvents = registerEvents + libvirt.registerErrorHandler(lambda ctx, error: None, None) + + def _connect(self): + monitor = LibvirtMonitor() + try: + self.virt = libvirt.openReadOnly('') + except libvirt.libvirtError, e: + self.logger.exception("Error in libvirt backend") + raise virt.VirtError(str(e)) + + def listDomains(self): + """ Get list of all domains. """ + domains = [] + self._connect() + + try: + # Active domains + for domainID in self.virt.listDomainsID(): + domain = self.virt.lookupByID(domainID) + if domain.UUIDString() == "00000000-0000-0000-0000-000000000000": + # Don't send Domain-0 on xen (zeroed uuid) + continue + domains.append(virt.Domain(self.virt, domain)) + self.logger.debug("Virtual machine found: %s: %s" % (domain.name(), domain.UUIDString())) + + # Non active domains + for domainName in self.virt.listDefinedDomains(): + domain = self.virt.lookupByName(domainName) + domains.append(virt.Domain(self.virt, domain)) + self.logger.debug("Virtual machine found: %s: %s" % (domainName, domain.UUIDString())) + except libvirt.libvirtError, e: + raise virt.VirtError(str(e)) + return domains + + def canMonitor(self): + return True + + def startMonitoring(self, event): + monitor = LibvirtMonitor() + if not monitor.isAlive(): + monitor.set_event(event) + monitor.start() + + +def eventToString(event): + eventStrings = ("Defined", "Undefined", "Started", "Suspended", "Resumed", + "Stopped", "Shutdown") + try: + return eventStrings[event] + except IndexError: + return "Unknown (%d)" % event + + +def detailToString(event, detail): + eventStrings = ( + ("Added", "Updated"), # Defined + ("Removed", ), # Undefined + ("Booted", "Migrated", "Restored", "Snapshot", "Wakeup"), # Started + ("Paused", "Migrated", "IOError", "Watchdog", "Restored", "Snapshot"), # Suspended + ("Unpaused", "Migrated", "Snapshot"), # Resumed + ("Shutdown", "Destroyed", "Crashed", "Migrated", "Saved", "Failed", "Snapshot"), # Stopped + ("Finished",), # Shutdown + ) + try: + return eventStrings[event][detail] + except IndexError: + return "Unknown (%d)" % detail diff --git a/virt/rhevm/__init__.py b/virt/rhevm/__init__.py new file mode 100644 index 0000000..3787775 --- /dev/null +++ b/virt/rhevm/__init__.py @@ -0,0 +1,2 @@ + +from rhevm import RhevM diff --git a/rhevm.py b/virt/rhevm/rhevm.py similarity index 95% rename from rhevm.py rename to virt/rhevm/rhevm.py index 188c4fa..570fa7b 100644 --- a/rhevm.py +++ b/virt/rhevm/rhevm.py @@ -23,13 +23,18 @@ import urlparse import urllib2 import base64
+import virt + # Import XML parser try: from elementtree import ElementTree except ImportError: from xml.etree import ElementTree
-class RHEVM: + +class RhevM(virt.HypervisorVirt): + CONFIG_TYPE = "rhevm" + def __init__(self, logger, url, username, password): self.logger = logger self.url = url @@ -101,5 +106,5 @@ if __name__ == '__main__':
import logging logger = logging.Logger("") - rhevm = RHEVM(logger, sys.argv[1], sys.argv[2], sys.argv[3]) + rhevm = RhevM(logger, sys.argv[1], sys.argv[2], sys.argv[3]) rhevm.getHostGuestMapping() diff --git a/virt/vdsm/__init__.py b/virt/vdsm/__init__.py new file mode 100644 index 0000000..9173255 --- /dev/null +++ b/virt/vdsm/__init__.py @@ -0,0 +1,2 @@ + +from vdsm import Vdsm diff --git a/vdsm.py b/virt/vdsm/vdsm.py similarity index 92% rename from vdsm.py rename to virt/vdsm/vdsm.py index 2802f75..06b27a3 100644 --- a/vdsm.py +++ b/virt/vdsm/vdsm.py @@ -25,10 +25,16 @@ import xmlrpclib from ConfigParser import SafeConfigParser, NoSectionError, NoOptionError import subprocess
+from virt import DirectVirt + + class VdsmError(Exception): pass
-class VDSM: + +class Vdsm(DirectVirt): + CONFIG_TYPE = "vdsm" + def __init__(self, logger): self.logger = logger self._readConfig("/etc/vdsm/vdsm.conf") @@ -48,8 +54,9 @@ class VDSM: raise VdsmError("Error in vdsm configuration file: %s" % str(e))
def _getLocalVdsName(self, tsPath): - p = subprocess.Popen(['/usr/bin/openssl', 'x509', '-noout', '-subject', '-in', - '%s/certs/vdsmcert.pem' % tsPath], stdout=subprocess.PIPE, close_fds=True) + p = subprocess.Popen([ + '/usr/bin/openssl', 'x509', '-noout', '-subject', '-in', + '%s/certs/vdsmcert.pem' % tsPath], stdout=subprocess.PIPE, close_fds=True) out, err = p.communicate() if p.returncode != 0: return '0' @@ -100,5 +107,5 @@ class VDSM: if __name__ == '__main__': import logging logger = logging.getLogger("rhsm-app." + __name__) - vdsm = VDSM(logger) + vdsm = Vdsm(logger) print vdsm.listDomains() diff --git a/virt/virt.py b/virt/virt.py new file mode 100644 index 0000000..72f924a --- /dev/null +++ b/virt/virt.py @@ -0,0 +1,88 @@ +""" +Module for abstraction of all virtualization backends, part of virt-who + +Copyright (C) 2014 Radek Novacek rnovacek@redhat.com + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty 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. +""" + + +class VirtError(Exception): + pass + + +class Domain(dict): + def __init__(self, virt, domain): + self['guestId'] = domain.UUIDString() + self['attributes'] = { + 'hypervisorType': virt.getType(), + 'virtWhoType': "libvirt", + 'active': 0 + } + if domain.isActive(): + self['attributes']['active'] = 1 + try: + self['state'] = domain.state(0)[0] + except AttributeError: + # Some versions of libvirt doesn't have domain.state() method + pass + + +class Virt(object): + def __init__(self, config): + self.config = config + + @classmethod + def fromConfig(cls, logger, config): + """ + Create instance of inherited class based on the config. + """ + + # Imports can't be top-level, it would be circular dependency + import libvirtd + import esx + import rhevm + import vdsm + + for subcls in cls.__subclasses__(): + for subsubcls in subcls.__subclasses__(): + if config.type == subsubcls.CONFIG_TYPE: + return subsubcls(logger, config) + raise KeyError("Invalid config type: %s" % config.type) + + def canMonitor(self): + """ + Return true if inherited class can perform background monitoring + for changes in host/guest association. + """ + return False + + def startMonitoring(self, event): + """ + Start the monitoring for changes in host/guest association. + + This should set the 'event' to force resending of host/guest associations. + """ + raise NotImplementedError() + + +class DirectVirt(Virt): + def listDomains(self): + raise NotImplementedError() + + +class HypervisorVirt(Virt): + def getHostGuestMapping(self): + raise NotImplementedError() diff --git a/virt-who.py b/virtwho.py similarity index 59% rename from virt-who.py rename to virtwho.py index 9262ebc..6baac51 100644 --- a/virt-who.py +++ b/virtwho.py @@ -24,21 +24,19 @@ import time import atexit import signal import errno +import threading
+from daemon import daemon from virt import Virt, VirtError -from vdsm import VDSM -from vsphere import VSphere -from rhevm import RHEVM -from hyperv import HyperV -from event import virEventLoopPureStart -from subscriptionmanager import SubscriptionManager, SubscriptionManagerError -from satellite import Satellite, SatelliteError +from manager import Manager, ManagerError +from config import Config, ConfigManager
import logging import log
from optparse import OptionParser, OptionGroup
+ class OptionParserEpilog(OptionParser): """ Epilog is new in Python 2.5, we need to support Python 2.4. """ def __init__(self, usage="%prog [options]", description=None, epilog=None): @@ -66,6 +64,7 @@ DefaultInterval = 3600 # Once per hour
PIDFILE = "/var/run/virt-who.pid"
+ class VirtWho(object): def __init__(self, logger, options): """ @@ -77,89 +76,16 @@ class VirtWho(object): """ self.logger = logger self.options = options + self.sync_event = threading.Event()
- self.virt = None - self.subscriptionManager = None + self.configManager = ConfigManager() + for config in self.configManager.configs: + logger.debug("Using config named '%s'" % config.name)
self.unableToRecoverStr = "Unable to recover" if not options.oneshot: self.unableToRecoverStr += ", retry in %d seconds." % RetryInterval
- # True if reload is queued - self.doReload = False - - - def initVirt(self): - """ - Connect to the virtualization supervisor (libvirt or VDSM) - """ - if self.options.virtType == "vdsm": - self.virt = VDSM(self.logger) - elif self.options.virtType == "libvirt": - self.virt = Virt(self.logger, registerEvents=self.options.background) - # We can listen for libvirt events - self.tryRegisterEventCallback() - elif self.options.virtType == "rhevm": - self.virt = RHEVM(self.logger, self.options.server, self.options.username, self.options.password) - elif self.options.virtType == "hyperv": - self.virt = HyperV(self.logger, self.options.server, self.options.username, self.options.password) - else: - # ESX - self.virt = VSphere(self.logger, self.options.server, self.options.username, self.options.password) - - def initSM(self): - """ - Connect to the subscription manager (candlepin). - """ - try: - if self.options.smType == "sam": - self.subscriptionManager = SubscriptionManager(self.logger) - self.subscriptionManager.connect() - elif self.options.smType == "satellite": - self.subscriptionManager = Satellite(self.logger) - self.subscriptionManager.connect(self.options.sat_server, self.options.sat_username, self.options.sat_password) - except NoOptionError, e: - self.logger.exception("Error in reading configuration file (/etc/rhsm/rhsm.conf):") - raise - except SubscriptionManagerError, e: - self.logger.exception("Unable to obtain status from server, UEPConnection is likely not usable:") - raise - except SatelliteError, e: - self.logger.exception("Unable to connect to the RHN Satellite:") - raise - except (KeyboardInterrupt, SystemExit): - raise - except Exception, e: - exceptionCheck(e) - self.logger.exception("Unknown error") - raise - - if self.options.virtType == "libvirt": - self.tryRegisterEventCallback() - - def tryRegisterEventCallback(self): - """ - This method register the handler which listen to guest changes - - If virt-who is running in background mode with libvirt backend, it can - monitor virt guests changes and send updates as soon as the change happens, - - """ - if self.options.background and self.options.virtType == "libvirt": - if self.virt is not None and self.subscriptionManager is not None: - # Send list of virt guests when something changes in libvirt - self.virt.domainEventRegisterCallback(self.subscriptionManager.sendVirtGuests) - - def checkConnections(self): - """ - Check if connection to subscription manager and virtualization supervisor - is established and reconnect if needed. - """ - if self.subscriptionManager is None: - self.initSM() - if self.virt is None: - self.initVirt() - def send(self): """ Send list of uuids to subscription manager @@ -167,9 +93,13 @@ class VirtWho(object): return - True if sending is successful, False otherwise """ # Try to send it twice - return self._send(True) + result = True + for config in self.configManager.configs: + if not self._send(config, True): + result = False + return result
- def _send(self, retry): + def _send(self, config, retry): """ Send list of uuids to subscription manager. This method will call itself once if sending fails. @@ -177,188 +107,85 @@ class VirtWho(object): retry - Should be True on first run, False on second. return - True if sending is successful, False otherwise """ - logger = self.logger - try: - self.checkConnections() - except (KeyboardInterrupt, SystemExit): - raise - except Exception, e: - exceptionCheck(e) - if retry: - logger.exception("Unable to create connection:") - return self._send(False) - else: - logger.error(self.unableToRecoverStr) - return False - try: - if self.options.virtType not in ["esx", "rhevm", "hyperv"]: - virtualGuests = self.virt.listDomains() - else: - virtualGuests = self.virt.getHostGuestMapping() + virtualGuests = self._readGuests(config) except (KeyboardInterrupt, SystemExit): raise except Exception, e: exceptionCheck(e) - # Communication with virtualization supervisor failed - self.virt = None # Retry once if retry: - logger.exception("Error in communication with virt backend, trying to recover:") - return self._send(False) + self.logger.exception("Error in communication with virtualization backend, trying to recover:") + return self._send(config, False) else: - logger.error(self.unableToRecoverStr) + self.logger.error(self.unableToRecoverStr) return False
try: - if self.options.virtType not in ["esx", "rhevm", "hyperv"]: - self.subscriptionManager.sendVirtGuests(virtualGuests) - else: - result = self.subscriptionManager.hypervisorCheckIn(self.options.owner, self.options.env, virtualGuests, type=self.options.virtType) - - # Show the result of hypervisorCheckIn - for fail in result['failedUpdate']: - logger.error("Error during update list of guests: %s", str(fail)) - for updated in result['updated']: - guests = [x['guestId'] for x in updated['guestIds']] - logger.info("Updated host: %s with guests: [%s]", updated['uuid'], ", ".join(guests)) - for created in result['created']: - guests = [x['guestId'] for x in created['guestIds']] - logger.info("Created host: %s with guests: [%s]", created['uuid'], ", ".join(guests)) + self._sendGuests(config, virtualGuests) except (KeyboardInterrupt, SystemExit): raise except Exception, e: - exceptionCheck(e) # Communication with subscription manager failed - self.subscriptionManager = None + exceptionCheck(e) # Retry once if retry: - logger.exception("Error in communication with subscription manager, trying to recover:") - return self._send(False) + self.logger.exception("Error in communication with subscription manager, trying to recover:") + return self._send(config, False) else: - logger.error(self.unableToRecoverStr) + self.logger.error(self.unableToRecoverStr) return False - return True
- def ping(self): - """ - Test if connection to virtualization manager is alive. - - return - True if connection is alive, False otherwise - """ - if self.virt is None: - return False - return self.virt.ping() - - def queueReload(self, *p): - """ - Reload virt-who configuration. Called on SIGHUP signal arrival. - """ - self.doReload = True - - def reloadConfig(self): - try: - self.virt.virt.close() - except AttributeError: - pass - self.virt = None - self.subscriptionManager = None - try: - self.checkConnections() - except (KeyboardInterrupt, SystemExit): - raise - except Exception, e: - exceptionCheck(e) - pass - self.logger.debug("virt-who configution reloaded") - self.doReload = False - - -def daemonize(debugMode): - """ Perform double-fork and redirect std* to /dev/null """ + def _readGuests(self, config): + virt = Virt.fromConfig(self.logger, config) + if not self.options.oneshot and virt.canMonitor(): + virt.startMonitoring(self.sync_event) + if config.type not in ["esx", "rhevm", "hyperv"]: + return virt.listDomains() + else: + return virt.getHostGuestMapping()
- # First fork - try: - pid = os.fork() - except OSError: - return False + def _sendGuests(self, config, virtualGuests): + manager = Manager.fromOptions(self.logger, self.options) + if config.type not in ["esx", "rhevm", "hyperv"]: + manager.sendVirtGuests(virtualGuests) + else: + result = manager.hypervisorCheckIn(config, virtualGuests) + + # Show the result of hypervisorCheckIn + for fail in result['failedUpdate']: + logger.error("Error during update list of guests: %s", str(fail)) + for updated in result['updated']: + guests = [x['guestId'] for x in updated['guestIds']] + logger.info("Updated host: %s with guests: [%s]", updated['uuid'], ", ".join(guests)) + for created in result['created']: + guests = [x['guestId'] for x in created['guestIds']] + logger.info("Created host: %s with guests: [%s]", created['uuid'], ", ".join(guests)) + + def run(self): + if self.options.background and self.options.virtType == "libvirt": + self.logger.debug("Starting infinite loop with %d seconds interval and event handling" % self.options.interval) + else: + self.logger.debug("Starting infinite loop with %d seconds interval" % self.options.interval)
- if pid > 0: - # Parent process - os._exit(0) + while 1: + # Run in infinite loop and send list of UUIDs every 'options.interval' seconds
- # First child process + if self.send(): + timeout = self.options.interval + else: + timeout = RetryInterval
- # Create session and set process group ID - os.setsid() + self.sync_event.wait(timeout) + self.sync_event.clear()
- # Second fork - try: - pid = os.fork() - except OSError: - return False - - if pid > 0: - # Parent process - os._exit(0) - - # Second child process - - # Redirect std* to /dev/null - devnull = os.open("/dev/null", os.O_RDWR) - os.dup2(devnull, 0) - os.dup2(devnull, 1) - # Don't redirect stderr in debug mode, we need to write debugging output there - if not debugMode: - os.dup2(devnull, 2) - - # Reset file creation mask - os.umask(0) - # Forget current working directory - os.chdir("/") - return True - -def checkPidFile(): - try: - f = open(PIDFILE, "r") - pid = int(f.read().strip()) + def reload(self): try: - os.kill(pid, 0) - print >>sys.stderr, "virt-who seems to be already running. If not, remove %s" % PIDFILE - sys.exit(1) - except OSError: - # Process no longer exists - print >>sys.stderr, "PID file exists but associated process does not, deleting PID file" - os.remove(PIDFILE) - except (KeyboardInterrupt, SystemExit): - raise - except Exception: - pass - -def createPidFile(logger=None): - atexit.register(cleanup) - signal.signal(signal.SIGINT, cleanup) - signal.signal(signal.SIGTERM, cleanup) - - # Write pid to pidfile - try: - f = open(PIDFILE, "w") - f.write("%d" % os.getpid()) - f.close() - except (KeyboardInterrupt, SystemExit): - raise - except Exception, e: - if logger is not None: - logger.error("Unable to create pid file: %s" % str(e)) + self.sync_event.set() + except Exception: + raise
-def cleanup(sig=None, stack=None): - try: - os.remove(PIDFILE) - except (KeyboardInterrupt, SystemExit): - raise - except Exception: - pass
def exceptionCheck(e): try: @@ -370,10 +197,8 @@ def exceptionCheck(e): except Exception: pass
-def main(): - checkPidFile() - createPidFile()
+def parseOptions(): parser = OptionParserEpilog(usage="virt-who [-d] [-i INTERVAL] [-b] [-o] [--sam|--satellite] [--libvirt|--vdsm|--esx|--rhevm|--hyperv]", description="Agent for reporting virtual guest IDs to subscription manager", epilog="virt-who also reads enviromental variables. They have the same name as command line arguments but uppercased, with underscore instead of dash and prefixed with VIRTWHO_ (e.g. VIRTWHO_ONE_SHOT). Empty variables are considered as disabled, non-empty as enabled") @@ -383,7 +208,7 @@ def main(): parser.add_option("-i", "--interval", type="int", dest="interval", default=0, help="Acquire and send list of virtual guest each N seconds")
virtGroup = OptionGroup(parser, "Virtualization backend", "Choose virtualization backend that should be used to gather host/guest associations") - virtGroup.add_option("--libvirt", action="store_const", dest="virtType", const="libvirt", default="libvirt", help="Use libvirt to list virtual guests [default]") + virtGroup.add_option("--libvirt", action="store_const", dest="virtType", const="libvirt", default=None, help="Use libvirt to list virtual guests [default]") virtGroup.add_option("--vdsm", action="store_const", dest="virtType", const="vdsm", help="Use vdsm to list virtual guests") virtGroup.add_option("--esx", action="store_const", dest="virtType", const="esx", help="Register ESX machines using vCenter") virtGroup.add_option("--rhevm", action="store_const", dest="virtType", const="rhevm", help="Register guests using RHEV-M") @@ -458,6 +283,10 @@ def main(): if env in ["1", "true"]: options.smType = "satellite"
+ env = os.getenv("VIRTWHO_LIBVIRT", "0").strip().lower() + if env in ["1", "true"]: + options.virtType = "libvirt" + env = os.getenv("VIRTWHO_VDSM", "0").strip().lower() if env in ["1", "true"]: options.virtType = "vdsm" @@ -474,7 +303,6 @@ def main(): if env in ["1", "true"]: options.virtType = "hyperv"
- def checkEnv(variable, option, name): """ If `option` is empty, check enviromental `variable` and return its value. @@ -533,75 +361,99 @@ def main(): # (e.g. libvirtd restart) options.interval = DefaultInterval
+ return (logger, options) + + +class PIDLock(object): + def __init__(self, filename): + self.filename = filename + + def is_locked(self): + try: + f = open(self.filename, "r") + pid = int(f.read().strip()) + try: + os.kill(pid, 0) + return True + except OSError: + # Process no longer exists + print >>sys.stderr, "PID file exists but associated process does not, deleting PID file" + os.remove(self.filename) + return False + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + return False + + def __enter__(self): + # Write pid to pidfile + try: + f = open(self.filename, "w") + f.write("%d" % os.getpid()) + f.close() + except (KeyboardInterrupt, SystemExit): + raise + except Exception, e: + if logger is not None: + logger.error("Unable to create pid file: %s" % str(e)) + + def __exit__(self, exc_type, exc_value, traceback): + try: + os.remove(self.filename) + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + pass + + +def main(): + lock = PIDLock(PIDFILE) + if lock.is_locked(): + print >>sys.stderr, "virt-who seems to be already running. If not, remove %s" % PIDFILE + sys.exit(1) + logger, options = parseOptions() + if options.background: - # Do a double-fork and other daemon initialization - if not daemonize(options.debug): - logger.error("Unable to fork, continuing in foreground") - createPidFile(logger) - - if not options.oneshot: - if options.background and options.virtType == "libvirt": - logger.debug("Starting event loop") - virEventLoopPureStart() - else: - logger.warning("Listening for events is not available in VDSM, ESX, RHEV-M or Hyper-V mode") + # Do a daemon initialization + with daemon.DaemonContext(pidfile=lock, files_preserve=[logger.handlers[0].stream]): + _main(logger, options) + else: + with lock: + _main(logger, options)
+ +def _main(logger, options): global RetryInterval if options.interval < RetryInterval: RetryInterval = options.interval
virtWho = VirtWho(logger, options) - signal.signal(signal.SIGHUP, virtWho.queueReload) - try: - virtWho.checkConnections() - except (KeyboardInterrupt, SystemExit): - raise - except Exception: - pass + if options.virtType is not None: + config = Config("virt-who", options.virtType, options.server, + options.username, options.password, options.owner, options.env) + virtWho.configManager.addConfig(config) + if len(virtWho.configManager.configs) == 0: + logger.error("No configurations found") + else: + for config in virtWho.configManager.configs: + logger.info("Using virt-who configuration: %s" % config.name)
- logger.debug("Virt-who is running in %s mode" % options.virtType) + def reload(signal, stackframe): + virtWho.reload() + signal.signal(signal.SIGHUP, reload)
if options.oneshot: # Send list of virtual guests and exit virtWho.send() else: - if options.background: - logger.debug("Starting infinite loop with %d seconds interval and event handling" % options.interval) - else: - logger.debug("Starting infinite loop with %d seconds interval" % options.interval) - - while 1: - # Run in infinite loop and send list of UUIDs every 'options.interval' seconds + virtWho.run()
- if virtWho.send(): - # Check if connection is established each 'RetryInterval' seconds - slept = 0 - while slept < options.interval: - # Sleep 'RetryInterval' or the rest of options.interval - t = min(RetryInterval, options.interval - slept) - # But sleep at least one second - t = max(t, 1) - time.sleep(t) - slept += t - - # Reload configuration if queued - if virtWho.doReload: - virtWho.reloadConfig() - break - - # Check the connection - if not virtWho.ping(): - # End the cycle - break - else: - # If last send fails, new try will be sooner - time.sleep(RetryInterval)
if __name__ == '__main__': try: main() except (SystemExit, KeyboardInterrupt): - raise + sys.exit(1) except Exception, e: logger = log.getLogger(False, False) logger.exception("Fatal error:")