Add proxy related parameters to agent driver
This change adds image_http_proxy, image_https_proxy, image_no_proxy parameters to a node's driver_info for use by the agent driver. If set, they will be passed to ironic python agent ramdisk and used during image download. Also this change updates the unit test TestAgentVendor.test_continue_deploy_image_source_is_url() as it was was incorrect. Partial-bug: #1526222 Change-Id: I7890f761f4adbe768f76831ef5b48b691a70b2d1
This commit is contained in:
parent
60aad94497
commit
0ed100d6f4
@ -16,6 +16,7 @@ from oslo_config import cfg
|
|||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
from oslo_utils import excutils
|
from oslo_utils import excutils
|
||||||
from oslo_utils import units
|
from oslo_utils import units
|
||||||
|
import six.moves.urllib_parse as urlparse
|
||||||
|
|
||||||
from ironic.common import dhcp_factory
|
from ironic.common import dhcp_factory
|
||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
@ -29,6 +30,7 @@ from ironic.common import images
|
|||||||
from ironic.common import paths
|
from ironic.common import paths
|
||||||
from ironic.common import raid
|
from ironic.common import raid
|
||||||
from ironic.common import states
|
from ironic.common import states
|
||||||
|
from ironic.common import utils
|
||||||
from ironic.conductor import task_manager
|
from ironic.conductor import task_manager
|
||||||
from ironic.conductor import utils as manager_utils
|
from ironic.conductor import utils as manager_utils
|
||||||
from ironic.drivers import base
|
from ironic.drivers import base
|
||||||
@ -93,7 +95,22 @@ REQUIRED_PROPERTIES = {
|
|||||||
'deploy_ramdisk': _('UUID (from Glance) of the ramdisk with agent that is '
|
'deploy_ramdisk': _('UUID (from Glance) of the ramdisk with agent that is '
|
||||||
'used at deploy time. Required.'),
|
'used at deploy time. Required.'),
|
||||||
}
|
}
|
||||||
COMMON_PROPERTIES = REQUIRED_PROPERTIES
|
|
||||||
|
OPTIONAL_PROPERTIES = {
|
||||||
|
'image_http_proxy': _('URL of a proxy server for HTTP connections. '
|
||||||
|
'Optional.'),
|
||||||
|
'image_https_proxy': _('URL of a proxy server for HTTPS connections. '
|
||||||
|
'Optional.'),
|
||||||
|
'image_no_proxy': _('A comma-separated list of host names, IP addresses '
|
||||||
|
'and domain names (with optional :port) that will be '
|
||||||
|
'excluded from proxying. To denote a doman name, use '
|
||||||
|
'a dot to prefix the domain name. This value will be '
|
||||||
|
'ignored if ``image_http_proxy`` and '
|
||||||
|
'``image_https_proxy`` are not specified. Optional.'),
|
||||||
|
}
|
||||||
|
|
||||||
|
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
|
||||||
|
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
|
||||||
|
|
||||||
|
|
||||||
def build_instance_info_for_deploy(task):
|
def build_instance_info_for_deploy(task):
|
||||||
@ -172,6 +189,42 @@ def check_image_size(task, image_source):
|
|||||||
raise exception.InvalidParameterValue(msg)
|
raise exception.InvalidParameterValue(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_image_proxies(node):
|
||||||
|
"""Check that the provided proxy parameters are valid.
|
||||||
|
|
||||||
|
:param node: an Ironic node.
|
||||||
|
:raises: InvalidParameterValue if any of the provided proxy parameters are
|
||||||
|
incorrect.
|
||||||
|
"""
|
||||||
|
invalid_proxies = {}
|
||||||
|
for scheme in ('http', 'https'):
|
||||||
|
proxy_param = 'image_%s_proxy' % scheme
|
||||||
|
proxy = node.driver_info.get(proxy_param)
|
||||||
|
if proxy:
|
||||||
|
chunks = urlparse.urlparse(proxy)
|
||||||
|
# NOTE(vdrok) If no scheme specified, this is still a valid
|
||||||
|
# proxy address. It is also possible for a proxy to have a
|
||||||
|
# scheme different from the one specified in the image URL,
|
||||||
|
# e.g. it is possible to use https:// proxy for downloading
|
||||||
|
# http:// image.
|
||||||
|
if chunks.scheme not in ('', 'http', 'https'):
|
||||||
|
invalid_proxies[proxy_param] = proxy
|
||||||
|
msg = ''
|
||||||
|
if invalid_proxies:
|
||||||
|
msg += _("Proxy URL should either have HTTP(S) scheme "
|
||||||
|
"or no scheme at all, the following URLs are "
|
||||||
|
"invalid: %s.") % invalid_proxies
|
||||||
|
no_proxy = node.driver_info.get('image_no_proxy')
|
||||||
|
if no_proxy is not None and not utils.is_valid_no_proxy(no_proxy):
|
||||||
|
msg += _(
|
||||||
|
"image_no_proxy should be a list of host names, IP addresses "
|
||||||
|
"or domain names to exclude from proxying, the specified list "
|
||||||
|
"%s is incorrect. To denote a domain name, prefix it with a dot "
|
||||||
|
"(instead of e.g. '.*').") % no_proxy
|
||||||
|
if msg:
|
||||||
|
raise exception.InvalidParameterValue(msg)
|
||||||
|
|
||||||
|
|
||||||
class AgentDeploy(base.DeployInterface):
|
class AgentDeploy(base.DeployInterface):
|
||||||
"""Interface for deploy-related actions."""
|
"""Interface for deploy-related actions."""
|
||||||
|
|
||||||
@ -228,6 +281,8 @@ class AgentDeploy(base.DeployInterface):
|
|||||||
# Validate node capabilities
|
# Validate node capabilities
|
||||||
deploy_utils.validate_capabilities(node)
|
deploy_utils.validate_capabilities(node)
|
||||||
|
|
||||||
|
validate_image_proxies(node)
|
||||||
|
|
||||||
@task_manager.require_exclusive_lock
|
@task_manager.require_exclusive_lock
|
||||||
def deploy(self, task):
|
def deploy(self, task):
|
||||||
"""Perform a deployment to a node.
|
"""Perform a deployment to a node.
|
||||||
@ -398,6 +453,18 @@ class AgentVendorInterface(agent_base_vendor.BaseAgentVendor):
|
|||||||
'stream_raw_images': CONF.agent.stream_raw_images,
|
'stream_raw_images': CONF.agent.stream_raw_images,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proxies = {}
|
||||||
|
for scheme in ('http', 'https'):
|
||||||
|
proxy_param = 'image_%s_proxy' % scheme
|
||||||
|
proxy = node.driver_info.get(proxy_param)
|
||||||
|
if proxy:
|
||||||
|
proxies[scheme] = proxy
|
||||||
|
if proxies:
|
||||||
|
image_info['proxies'] = proxies
|
||||||
|
no_proxy = node.driver_info.get('image_no_proxy')
|
||||||
|
if no_proxy is not None:
|
||||||
|
image_info['no_proxy'] = no_proxy
|
||||||
|
|
||||||
# Tell the client to download and write the image with the given args
|
# Tell the client to download and write the image with the given args
|
||||||
self._client.prepare_image(node, image_info)
|
self._client.prepare_image(node, image_info)
|
||||||
|
|
||||||
|
@ -313,6 +313,22 @@ class TestAgentDeploy(db_base.DbTestCase):
|
|||||||
task.driver.boot, task)
|
task.driver.boot, task)
|
||||||
show_mock.assert_called_once_with(self.context, 'fake-image')
|
show_mock.assert_called_once_with(self.context, 'fake-image')
|
||||||
|
|
||||||
|
@mock.patch.object(images, 'image_show', autospec=True)
|
||||||
|
@mock.patch.object(pxe.PXEBoot, 'validate', autospec=True)
|
||||||
|
def test_validate_invalid_proxies(self, pxe_boot_validate_mock, show_mock):
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
|
shared=True) as task:
|
||||||
|
task.node.driver_info.update({
|
||||||
|
'image_https_proxy': 'git://spam.ni',
|
||||||
|
'image_http_proxy': 'http://spam.ni',
|
||||||
|
'image_no_proxy': '1' * 500})
|
||||||
|
self.assertRaisesRegexp(exception.InvalidParameterValue,
|
||||||
|
'image_https_proxy.*image_no_proxy',
|
||||||
|
task.driver.deploy.validate, task)
|
||||||
|
pxe_boot_validate_mock.assert_called_once_with(
|
||||||
|
task.driver.boot, task)
|
||||||
|
show_mock.assert_called_once_with(self.context, 'fake-image')
|
||||||
|
|
||||||
@mock.patch('ironic.conductor.utils.node_power_action', autospec=True)
|
@mock.patch('ironic.conductor.utils.node_power_action', autospec=True)
|
||||||
def test_deploy(self, power_mock):
|
def test_deploy(self, power_mock):
|
||||||
with task_manager.acquire(
|
with task_manager.acquire(
|
||||||
@ -495,10 +511,13 @@ class TestAgentVendor(db_base.DbTestCase):
|
|||||||
}
|
}
|
||||||
self.node = object_utils.create_test_node(self.context, **n)
|
self.node = object_utils.create_test_node(self.context, **n)
|
||||||
|
|
||||||
def test_continue_deploy(self):
|
def _test_continue_deploy(self, additional_driver_info=None,
|
||||||
CONF.set_override('stream_raw_images', False, 'agent')
|
additional_expected_image_info=None):
|
||||||
self.node.provision_state = states.DEPLOYWAIT
|
self.node.provision_state = states.DEPLOYWAIT
|
||||||
self.node.target_provision_state = states.ACTIVE
|
self.node.target_provision_state = states.ACTIVE
|
||||||
|
driver_info = self.node.driver_info
|
||||||
|
driver_info.update(additional_driver_info or {})
|
||||||
|
self.node.driver_info = driver_info
|
||||||
self.node.save()
|
self.node.save()
|
||||||
test_temp_url = 'http://image'
|
test_temp_url = 'http://image'
|
||||||
expected_image_info = {
|
expected_image_info = {
|
||||||
@ -507,8 +526,9 @@ class TestAgentVendor(db_base.DbTestCase):
|
|||||||
'checksum': 'checksum',
|
'checksum': 'checksum',
|
||||||
'disk_format': 'qcow2',
|
'disk_format': 'qcow2',
|
||||||
'container_format': 'bare',
|
'container_format': 'bare',
|
||||||
'stream_raw_images': False,
|
'stream_raw_images': CONF.agent.stream_raw_images,
|
||||||
}
|
}
|
||||||
|
expected_image_info.update(additional_expected_image_info or {})
|
||||||
|
|
||||||
client_mock = mock.MagicMock(spec_set=['prepare_image'])
|
client_mock = mock.MagicMock(spec_set=['prepare_image'])
|
||||||
self.passthru._client = client_mock
|
self.passthru._client = client_mock
|
||||||
@ -523,32 +543,34 @@ class TestAgentVendor(db_base.DbTestCase):
|
|||||||
self.assertEqual(states.ACTIVE,
|
self.assertEqual(states.ACTIVE,
|
||||||
task.node.target_provision_state)
|
task.node.target_provision_state)
|
||||||
|
|
||||||
|
def test_continue_deploy(self):
|
||||||
|
self._test_continue_deploy()
|
||||||
|
|
||||||
|
def test_continue_deploy_with_proxies(self):
|
||||||
|
self._test_continue_deploy(
|
||||||
|
additional_driver_info={'image_https_proxy': 'https://spam.ni',
|
||||||
|
'image_http_proxy': 'spam.ni',
|
||||||
|
'image_no_proxy': '.eggs.com'},
|
||||||
|
additional_expected_image_info={
|
||||||
|
'proxies': {'https': 'https://spam.ni',
|
||||||
|
'http': 'spam.ni'},
|
||||||
|
'no_proxy': '.eggs.com'}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_continue_deploy_with_no_proxy_without_proxies(self):
|
||||||
|
self._test_continue_deploy(
|
||||||
|
additional_driver_info={'image_no_proxy': '.eggs.com'}
|
||||||
|
)
|
||||||
|
|
||||||
def test_continue_deploy_image_source_is_url(self):
|
def test_continue_deploy_image_source_is_url(self):
|
||||||
self.node.provision_state = states.DEPLOYWAIT
|
instance_info = self.node.instance_info
|
||||||
self.node.target_provision_state = states.ACTIVE
|
instance_info['image_source'] = 'http://example.com/woof.img'
|
||||||
self.node.save()
|
self.node.instance_info = instance_info
|
||||||
test_temp_url = 'http://image'
|
self._test_continue_deploy(
|
||||||
expected_image_info = {
|
additional_expected_image_info={
|
||||||
'urls': [test_temp_url],
|
'id': 'woof.img'
|
||||||
'id': self.node.instance_info['image_source'],
|
}
|
||||||
'checksum': 'checksum',
|
)
|
||||||
'disk_format': 'qcow2',
|
|
||||||
'container_format': 'bare',
|
|
||||||
'stream_raw_images': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
client_mock = mock.MagicMock(spec_set=['prepare_image'])
|
|
||||||
self.passthru._client = client_mock
|
|
||||||
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
self.passthru.continue_deploy(task)
|
|
||||||
|
|
||||||
client_mock.prepare_image.assert_called_with(task.node,
|
|
||||||
expected_image_info)
|
|
||||||
self.assertEqual(states.DEPLOYWAIT, task.node.provision_state)
|
|
||||||
self.assertEqual(states.ACTIVE,
|
|
||||||
task.node.target_provision_state)
|
|
||||||
|
|
||||||
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
|
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
|
||||||
@mock.patch.object(fake.FakePower, 'get_power_state',
|
@mock.patch.object(fake.FakePower, 'get_power_state',
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Pass proxy information from agent driver to IPA ramdisk, so that images
|
||||||
|
can be cached on the proxy server.
|
||||||
|
issues:
|
||||||
|
- When using caching proxy with ``agent_*`` drivers, caching the image on the
|
||||||
|
proxy server might involve increasing [glance]swift_temp_url_duration
|
||||||
|
config option value. This way, the cached entry will be valid for a period
|
||||||
|
of time long enough to see the benefits of caching. Large temporary URL
|
||||||
|
duration might become a security issue in some cases.
|
||||||
|
upgrade:
|
||||||
|
- Adds a [glance]swift_temp_url_cache_enabled configuration option to enable
|
||||||
|
Swift temporary URL caching. It is only useful if the caching proxy is
|
||||||
|
used. Also adds [glance]swift_temp_url_expected_download_start_delay, which
|
||||||
|
is used to check if the Swift temporary URL duration is long enough to let
|
||||||
|
the image download to start, and, if temporary URL caching is enabled, to
|
||||||
|
determine if a cached entry will be still valid when download starts. The
|
||||||
|
value of [glance]swift_temp_url_expected_download_start_delay must be less
|
||||||
|
than the value for the [glance]swift_temp_url_duration configuration
|
||||||
|
option.
|
Loading…
Reference in New Issue
Block a user