glance Windows support

This change will allow glance services to run on Windows, using
eventlet wsgi for API services.

This change will:
* avoid monkey patching the os module on Windows (which causes Popen
  to fail)
* avoiding unavailable signals
* avoid renaming in-use files or leaking handles
* update the check that ensures that just one scrubber process may
  run at a time. We can't rely on process names as there might be
  wrapper processes that have similar names (no she-bangs on Windows,
  so the scripts are called a bit differently). We'll use a global
  named mutex instead.

A subsequent change will leverage Windows job objects as a
replacement for process groups, also avoiding forking when spawning
workers.

At the moment, some Glance tests cannot run on Windows, which is
also covered by subsequent patches.

DocImpact

blueprint windows-support

Change-Id: I3bca69638685ceb11a1a316511ad9a298c630ad5
This commit is contained in:
Lucian Petrut 2019-01-04 10:07:57 +00:00
parent d7321ada06
commit 5759ec0b1c
11 changed files with 147 additions and 71 deletions

View File

@ -91,6 +91,7 @@ class _Convert(task.Task):
# specified. There's no "sane" default for this # specified. There's no "sane" default for this
# because the dest format may work differently depending # because the dest format may work differently depending
# on the environment OpenStack is running in. # on the environment OpenStack is running in.
abs_file_path = file_path.split("file://")[-1]
conversion_format = CONF.taskflow_executor.conversion_format conversion_format = CONF.taskflow_executor.conversion_format
if conversion_format is None: if conversion_format is None:
if not _Convert.conversion_missing_warned: if not _Convert.conversion_missing_warned:
@ -123,7 +124,8 @@ class _Convert(task.Task):
if stderr: if stderr:
raise RuntimeError(stderr) raise RuntimeError(stderr)
os.rename(dest_path, file_path.split("file://")[-1]) os.unlink(abs_file_path)
os.rename(dest_path, abs_file_path)
return file_path return file_path
def revert(self, image_id, result=None, **kwargs): def revert(self, image_id, result=None, **kwargs):

View File

@ -82,6 +82,7 @@ class _OVF_Process(task.Task):
:param file_path: Path to the OVA package :param file_path: Path to the OVA package
""" """
file_abs_path = file_path.split("file://")[-1]
image = self.image_repo.get(image_id) image = self.image_repo.get(image_id)
# Expect 'ova' as image container format for OVF_Process task # Expect 'ova' as image container format for OVF_Process task
if image.container_format == 'ova': if image.container_format == 'ova':
@ -92,6 +93,8 @@ class _OVF_Process(task.Task):
# the context as a short-cut. # the context as a short-cut.
if image.context and image.context.is_admin: if image.context and image.context.is_admin:
extractor = OVAImageExtractor() extractor = OVAImageExtractor()
data_iter = None
try:
data_iter = self._get_ova_iter_objects(file_path) data_iter = self._get_ova_iter_objects(file_path)
disk, properties = extractor.extract(data_iter) disk, properties = extractor.extract(data_iter)
image.extra_properties.update(properties) image.extra_properties.update(properties)
@ -100,9 +103,13 @@ class _OVF_Process(task.Task):
dest_path = self._get_extracted_file_path(image_id) dest_path = self._get_extracted_file_path(image_id)
with open(dest_path, 'wb') as f: with open(dest_path, 'wb') as f:
shutil.copyfileobj(disk, f, 4096) shutil.copyfileobj(disk, f, 4096)
finally:
if data_iter:
data_iter.close()
# Overwrite the input ova file since it is no longer needed # Overwrite the input ova file since it is no longer needed
os.rename(dest_path, file_path.split("file://")[-1]) os.unlink(file_abs_path)
os.rename(dest_path, file_abs_path)
else: else:
raise RuntimeError(_('OVA extract is limited to admin')) raise RuntimeError(_('OVA extract is limited to admin'))

View File

@ -20,6 +20,10 @@
""" """
Glance API Server Glance API Server
""" """
import os
import sys
import eventlet import eventlet
# NOTE(jokke): As per the eventlet commit # NOTE(jokke): As per the eventlet commit
# b756447bab51046dfc6f1e0e299cc997ab343701 there's circular import happening # b756447bab51046dfc6f1e0e299cc997ab343701 there's circular import happening
@ -27,10 +31,13 @@ import eventlet
# before calling monkey_patch(). This is solved in eventlet 0.22.0 but we # before calling monkey_patch(). This is solved in eventlet 0.22.0 but we
# need to address it before that is widely used around. # need to address it before that is widely used around.
eventlet.hubs.get_hub() eventlet.hubs.get_hub()
eventlet.patcher.monkey_patch()
import os if os.name == 'nt':
import sys # eventlet monkey patching the os module causes subprocess.Popen to fail
# on Windows when using pipes due to missing non-blocking IO support.
eventlet.patcher.monkey_patch(os=False)
else:
eventlet.patcher.monkey_patch()
from oslo_utils import encodeutils from oslo_utils import encodeutils

View File

@ -20,6 +20,10 @@
""" """
Reference implementation server for Glance Registry Reference implementation server for Glance Registry
""" """
import os
import sys
import eventlet import eventlet
# NOTE(jokke): As per the eventlet commit # NOTE(jokke): As per the eventlet commit
# b756447bab51046dfc6f1e0e299cc997ab343701 there's circular import happening # b756447bab51046dfc6f1e0e299cc997ab343701 there's circular import happening
@ -27,11 +31,13 @@ import eventlet
# before calling monkey_patch(). This is solved in eventlet 0.22.0 but we # before calling monkey_patch(). This is solved in eventlet 0.22.0 but we
# need to address it before that is widely used around. # need to address it before that is widely used around.
eventlet.hubs.get_hub() eventlet.hubs.get_hub()
eventlet.patcher.monkey_patch()
import os
import sys
if os.name == 'nt':
# eventlet monkey patching the os module causes subprocess.Popen to fail
# on Windows when using pipes due to missing non-blocking IO support.
eventlet.patcher.monkey_patch(os=False)
else:
eventlet.patcher.monkey_patch()
from oslo_utils import encodeutils from oslo_utils import encodeutils

View File

@ -18,6 +18,10 @@
""" """
Glance Scrub Service Glance Scrub Service
""" """
import os
import sys
import eventlet import eventlet
# NOTE(jokke): As per the eventlet commit # NOTE(jokke): As per the eventlet commit
# b756447bab51046dfc6f1e0e299cc997ab343701 there's circular import happening # b756447bab51046dfc6f1e0e299cc997ab343701 there's circular import happening
@ -25,11 +29,15 @@ import eventlet
# before calling monkey_patch(). This is solved in eventlet 0.22.0 but we # before calling monkey_patch(). This is solved in eventlet 0.22.0 but we
# need to address it before that is widely used around. # need to address it before that is widely used around.
eventlet.hubs.get_hub() eventlet.hubs.get_hub()
eventlet.patcher.monkey_patch()
import os if os.name == 'nt':
# eventlet monkey patching the os module causes subprocess.Popen to fail
# on Windows when using pipes due to missing non-blocking IO support.
eventlet.patcher.monkey_patch(os=False)
else:
eventlet.patcher.monkey_patch()
import subprocess import subprocess
import sys
# If ../glance/__init__.py exists, add ../ to Python search path, so that # If ../glance/__init__.py exists, add ../ to Python search path, so that
# it will override what happens to be installed in /usr/(local/)lib/python... # it will override what happens to be installed in /usr/(local/)lib/python...
@ -40,6 +48,7 @@ if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')):
sys.path.insert(0, possible_topdir) sys.path.insert(0, possible_topdir)
import glance_store import glance_store
from os_win import utilsfactory as os_win_utilsfactory
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
@ -54,10 +63,21 @@ CONF.set_default(name='use_stderr', default=True)
def main(): def main():
# Used on Window, ensuring that a single scrubber can run at a time.
mutex = None
mutex_acquired = False
try:
if os.name == 'nt':
# We can't rely on process names on Windows as there may be
# wrappers with the same name.
mutex = os_win_utilsfactory.get_mutex(
name='Global\\glance-scrubber')
mutex_acquired = mutex.acquire(timeout_ms=0)
CONF.register_cli_opts(scrubber.scrubber_cmd_cli_opts) CONF.register_cli_opts(scrubber.scrubber_cmd_cli_opts)
CONF.register_opts(scrubber.scrubber_cmd_opts) CONF.register_opts(scrubber.scrubber_cmd_opts)
try:
config.parse_args() config.parse_args()
logging.setup(CONF, 'glance') logging.setup(CONF, 'glance')
@ -65,21 +85,48 @@ def main():
glance_store.create_stores(config.CONF) glance_store.create_stores(config.CONF)
glance_store.verify_default_store() glance_store.verify_default_store()
app = scrubber.Scrubber(glance_store)
if CONF.restore and CONF.daemon: if CONF.restore and CONF.daemon:
sys.exit("ERROR: The restore and daemon options should not be set " sys.exit("ERROR: The restore and daemon options should not be set "
"together. Please use either of them in one request.") "together. Please use either of them in one request.")
app = scrubber.Scrubber(glance_store)
if CONF.restore: if CONF.restore:
if os.name == 'nt':
scrubber_already_running = not mutex_acquired
else:
scrubber_already_running = scrubber_already_running_posix()
if scrubber_already_running:
already_running_msg = (
"ERROR: glance-scrubber is already running. "
"Please ensure that the daemon is stopped.")
sys.exit(already_running_msg)
app.revert_image_status(CONF.restore)
elif CONF.daemon:
server = scrubber.Daemon(CONF.wakeup_time)
server.start(app)
server.wait()
else:
app.run()
except (exception.ImageNotFound, exception.Conflict) as e:
sys.exit("ERROR: %s" % e)
except RuntimeError as e:
sys.exit("ERROR: %s" % e)
finally:
if mutex and mutex_acquired:
mutex.release()
def scrubber_already_running_posix():
# Try to check the glance-scrubber is running or not. # Try to check the glance-scrubber is running or not.
# 1. Try to find the pid file if scrubber is controlled by # 1. Try to find the pid file if scrubber is controlled by
# glance-control # glance-control
# 2. Try to check the process name. # 2. Try to check the process name.
error_str = ("ERROR: The glance-scrubber process is running under "
"daemon. Please stop it first.")
pid_file = '/var/run/glance/glance-scrubber.pid' pid_file = '/var/run/glance/glance-scrubber.pid'
if os.path.exists(os.path.abspath(pid_file)): if os.path.exists(os.path.abspath(pid_file)):
sys.exit(error_str) return True
for glance_scrubber_name in ['glance-scrubber', for glance_scrubber_name in ['glance-scrubber',
'glance.cmd.scrubber']: 'glance.cmd.scrubber']:
@ -98,23 +145,14 @@ def main():
if pids.count('\n') > 1 and str(self_pid) in pids: if pids.count('\n') > 1 and str(self_pid) in pids:
# One process is self, so if the process number is > 1, it # One process is self, so if the process number is > 1, it
# means that another glance-scrubber process is running. # means that another glance-scrubber process is running.
sys.exit(error_str) return True
elif pids.count('\n') > 0 and str(self_pid) not in pids: elif pids.count('\n') > 0 and str(self_pid) not in pids:
# If self is not in result and the pids number is still # If self is not in result and the pids number is still
# > 0, it means that the another glance-scrubber process is # > 0, it means that the another glance-scrubber process is
# running. # running.
sys.exit(error_str) return True
app.revert_image_status(CONF.restore)
elif CONF.daemon: return False
server = scrubber.Daemon(CONF.wakeup_time)
server.start(app)
server.wait()
else:
app.run()
except (exception.ImageNotFound, exception.Conflict) as e:
sys.exit("ERROR: %s" % e)
except RuntimeError as e:
sys.exit("ERROR: %s" % e)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -495,8 +495,10 @@ class Server(object):
try: try:
# NOTE(flaper87): Make sure this process # NOTE(flaper87): Make sure this process
# runs in its own process group. # runs in its own process group.
# NOTE(lpetrut): This isn't available on Windows, so we're going
# to use job objects instead.
os.setpgid(self.pgid, self.pgid) os.setpgid(self.pgid, self.pgid)
except OSError: except (OSError, AttributeError):
# NOTE(flaper87): When running glance-control, # NOTE(flaper87): When running glance-control,
# (glance's functional tests, for example) # (glance's functional tests, for example)
# setpgid fails with EPERM as glance-control # setpgid fails with EPERM as glance-control
@ -508,18 +510,25 @@ class Server(object):
# shouldn't raise any error here. # shouldn't raise any error here.
self.pgid = 0 self.pgid = 0
@staticmethod
def set_signal_handler(signal_name, handler):
# Some signals may not be available on this platform.
sig = getattr(signal, signal_name, None)
if sig is not None:
signal.signal(sig, handler)
def hup(self, *args): def hup(self, *args):
""" """
Reloads configuration files with zero down time Reloads configuration files with zero down time
""" """
signal.signal(signal.SIGHUP, signal.SIG_IGN) self.set_signal_handler("SIGHUP", signal.SIG_IGN)
raise exception.SIGHUPInterrupt raise exception.SIGHUPInterrupt
def kill_children(self, *args): def kill_children(self, *args):
"""Kills the entire process group.""" """Kills the entire process group."""
signal.signal(signal.SIGTERM, signal.SIG_IGN) self.set_signal_handler("SIGTERM", signal.SIG_IGN)
signal.signal(signal.SIGINT, signal.SIG_IGN) self.set_signal_handler("SIGINT", signal.SIG_IGN)
signal.signal(signal.SIGCHLD, signal.SIG_IGN) self.set_signal_handler("SIGCHLD", signal.SIG_IGN)
self.running = False self.running = False
os.killpg(self.pgid, signal.SIGTERM) os.killpg(self.pgid, signal.SIGTERM)
@ -544,9 +553,9 @@ class Server(object):
return return
else: else:
LOG.info(_LI("Starting %d workers"), workers) LOG.info(_LI("Starting %d workers"), workers)
signal.signal(signal.SIGTERM, self.kill_children) self.set_signal_handler("SIGTERM", self.kill_children)
signal.signal(signal.SIGINT, self.kill_children) self.set_signal_handler("SIGINT", self.kill_children)
signal.signal(signal.SIGHUP, self.hup) self.set_signal_handler("SIGHUP", self.hup)
while len(self.children) < workers: while len(self.children) < workers:
self.run_child() self.run_child()
@ -655,18 +664,18 @@ class Server(object):
def run_child(self): def run_child(self):
def child_hup(*args): def child_hup(*args):
"""Shuts down child processes, existing requests are handled.""" """Shuts down child processes, existing requests are handled."""
signal.signal(signal.SIGHUP, signal.SIG_IGN) self.set_signal_handler("SIGHUP", signal.SIG_IGN)
eventlet.wsgi.is_accepting = False eventlet.wsgi.is_accepting = False
self.sock.close() self.sock.close()
pid = os.fork() pid = os.fork()
if pid == 0: if pid == 0:
signal.signal(signal.SIGHUP, child_hup) self.set_signal_handler("SIGHUP", child_hup)
signal.signal(signal.SIGTERM, signal.SIG_DFL) self.set_signal_handler("SIGTERM", signal.SIG_DFL)
# ignore the interrupt signal to avoid a race whereby # ignore the interrupt signal to avoid a race whereby
# a child worker receives the signal before the parent # a child worker receives the signal before the parent
# and is respawned unnecessarily as a result # and is respawned unnecessarily as a result
signal.signal(signal.SIGINT, signal.SIG_IGN) self.set_signal_handler("SIGINT", signal.SIG_IGN)
# The child has no need to stash the unwrapped # The child has no need to stash the unwrapped
# socket, and the reference prevents a clean # socket, and the reference prevents a clean
# exit on sighup # exit on sighup

View File

@ -351,8 +351,7 @@ class TestScrubber(functional.FunctionalTest):
cmd = ("%s --restore fake_image_id" % exe_cmd) cmd = ("%s --restore fake_image_id" % exe_cmd)
exitcode, out, err = execute(cmd, raise_error=False) exitcode, out, err = execute(cmd, raise_error=False)
self.assertEqual(1, exitcode) self.assertEqual(1, exitcode)
self.assertIn('The glance-scrubber process is running under daemon', self.assertIn('glance-scrubber is already running', str(err))
str(err))
self.stop_server(self.scrubber_daemon) self.stop_server(self.scrubber_daemon)
@ -363,7 +362,7 @@ class TestScrubber(functional.FunctionalTest):
# Sometimes the glance-scrubber process which is setup by the # Sometimes the glance-scrubber process which is setup by the
# previous test can't be shutdown immediately, so if we get the "daemon # previous test can't be shutdown immediately, so if we get the "daemon
# running" message we sleep and try again. # running" message we sleep and try again.
not_down_msg = 'The glance-scrubber process is running under daemon' not_down_msg = 'glance-scrubber is already running'
total_wait = 15 total_wait = 15
for _ in range(total_wait): for _ in range(total_wait):
exitcode, out, err = func() exitcode, out, err = func()

View File

@ -78,7 +78,8 @@ class TestImportTask(test_utils.BaseTestCase):
group='taskflow_executor') group='taskflow_executor')
glance_store.create_stores(CONF) glance_store.create_stores(CONF)
def test_convert_success(self): @mock.patch.object(os, 'unlink')
def test_convert_success(self, mock_unlink):
image_convert = convert._Convert(self.task.task_id, image_convert = convert._Convert(self.task.task_id,
self.task_type, self.task_type,
self.img_repo) self.img_repo)

View File

@ -61,6 +61,7 @@ openstackdocstheme==1.18.1
os-api-ref==1.4.0 os-api-ref==1.4.0
os-client-config==1.29.0 os-client-config==1.29.0
os-testr==1.0.0 os-testr==1.0.0
os-win==3.0.0
oslo.cache==1.29.0 oslo.cache==1.29.0
oslo.concurrency==3.26.0 oslo.concurrency==3.26.0
oslo.config==5.2.0 oslo.config==5.2.0

View File

@ -0,0 +1,4 @@
---
features:
- |
Glance services can now run on Windows.

View File

@ -56,3 +56,5 @@ cursive>=0.2.1 # Apache-2.0
# timeutils # timeutils
iso8601>=0.1.11 # MIT iso8601>=0.1.11 # MIT
os-win>=3.0.0 # Apache-2.0