commit cda53731c1807aad916f1ca07392fc13eacfb9d0
Author: Radek Novacek <rnovacek(a)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(a)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(a)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(a)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">%(qu...
</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-s...
"""
+
+
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(a)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(a)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:")