Merge "Refactor the CloudStack Metadata Service"

This commit is contained in:
Jenkins 2017-02-21 18:29:51 +00:00 committed by Gerrit Code Review
commit c2db56a10b
3 changed files with 97 additions and 96 deletions

View File

@ -31,6 +31,10 @@ class CloudStackOptions(conf_base.Options):
help="The base URL where the service looks for metadata", help="The base URL where the service looks for metadata",
deprecated_name="cloudstack_metadata_ip", deprecated_name="cloudstack_metadata_ip",
deprecated_group="DEFAULT"), deprecated_group="DEFAULT"),
cfg.IntOpt(
"password_server_port", default=8080,
help="The port number used by the Password Server."
),
cfg.BoolOpt( cfg.BoolOpt(
"https_allow_insecure", default=False, "https_allow_insecure", default=False,
help="Whether to disable the validation of HTTPS " help="Whether to disable the validation of HTTPS "

View File

@ -27,25 +27,34 @@ from cloudbaseinit.utils import encoding
CONF = cloudbaseinit_conf.CONF CONF = cloudbaseinit_conf.CONF
LOG = oslo_logging.getLogger(__name__) LOG = oslo_logging.getLogger(__name__)
BAD_REQUEST = b"bad_request" BAD_REQUEST = "bad_request"
SAVED_PASSWORD = b"saved_password" SAVED_PASSWORD = "saved_password"
TIMEOUT = 10 TIMEOUT = 10
class CloudStack(base.BaseHTTPMetadataService): class CloudStack(base.BaseHTTPMetadataService):
"""Metadata service for Apache CloudStack.
Apache CloudStack is an open source software designed to deploy and
manage large networks of virtual machines, as a highly available,
highly scalable Infrastructure as a Service (IaaS) cloud computing
platform.
"""
def __init__(self): def __init__(self):
# Note: The base url used by the current metadata service will be
# updated later by the `_test_api` method.
super(CloudStack, self).__init__( super(CloudStack, self).__init__(
# Note(alexcoman): The base url used by the current metadata
# service will be updated later by the `_test_api` method.
base_url=None, base_url=None,
https_allow_insecure=CONF.cloudstack.https_allow_insecure, https_allow_insecure=CONF.cloudstack.https_allow_insecure,
https_ca_bundle=CONF.cloudstack.https_ca_bundle) https_ca_bundle=CONF.cloudstack.https_ca_bundle)
self._osutils = osutils_factory.get_os_utils() self._osutils = osutils_factory.get_os_utils()
self._router_ip = None self._metadata_host = None
def _get_path(self, resource, version="latest"): @staticmethod
def _get_path(resource, version="latest"):
"""Get the relative path for the received resource.""" """Get the relative path for the received resource."""
return posixpath.normpath( return posixpath.normpath(
posixpath.join(version, "meta-data", resource)) posixpath.join(version, "meta-data", resource))
@ -68,7 +77,7 @@ class CloudStack(base.BaseHTTPMetadataService):
LOG.debug('Available services: %s', response) LOG.debug('Available services: %s', response)
netloc = urllib.parse.urlparse(metadata_url).netloc netloc = urllib.parse.urlparse(metadata_url).netloc
self._router_ip = netloc.split(":")[0] self._metadata_host = netloc.split(":")[0]
return True return True
def load(self): def load(self):
@ -114,13 +123,37 @@ class CloudStack(base.BaseHTTPMetadataService):
ssh_keys.append(ssh_key) ssh_keys.append(ssh_key)
return ssh_keys return ssh_keys
def _password_client(self, body=None, headers=None, decode=True):
"""Client for the Password Server."""
port = CONF.cloudstack.password_server_port
with contextlib.closing(http_client.HTTPConnection(
self._metadata_host, port, timeout=TIMEOUT)) as connection:
try:
connection.request("GET", "/", body=body, headers=headers)
response = connection.getresponse()
except http_client.HTTPException as exc:
LOG.error("Request failed: %s", exc)
raise
content = response.read()
if decode:
content = encoding.get_as_string(content)
if response.status != 200:
raise http_client.HTTPException(
"%(status)s %(reason)s - %(message)r",
{"status": response.status, "reason": response.reason,
"message": content})
return content
def _get_password(self): def _get_password(self):
"""Get the password from the Password Server. """Get the password from the Password Server.
The Password Server can be found on the DHCP_SERVER on the port 8080. The Password Server can be found on the DHCP_SERVER on the port 8080.
.. note: .. note:
The Password Server can return the following values: The Password Server can return the following values:
* `bad_request`: the Password Server did not recognise * `bad_request`: the Password Server did not recognize
the request the request
* `saved_password`: the password was already deleted from * `saved_password`: the password was already deleted from
the Password Server the Password Server
@ -132,45 +165,33 @@ class CloudStack(base.BaseHTTPMetadataService):
headers = {"DomU_Request": "send_my_password"} headers = {"DomU_Request": "send_my_password"}
password = None password = None
with contextlib.closing(http_client.HTTPConnection( for _ in range(CONF.retry_count):
self._router_ip, 8080, timeout=TIMEOUT)) as connection: try:
for _ in range(CONF.retry_count): content = self._password_client(headers=headers).strip()
try: except http_client.HTTPConnection as exc:
connection.request("GET", "/", headers=headers) LOG.error("Getting password failed: %s", exc)
response = connection.getresponse() continue
except http_client.HTTPException as exc:
LOG.exception(exc)
continue
if response.status != 200: if not content:
LOG.warning("Getting password failed: %(status)s " LOG.warning("The Password Server did not have any "
"%(reason)s - %(message)r", "password for the current instance.")
{"status": response.status, continue
"reason": response.reason,
"message": response.read()})
continue
content = response.read().strip() if content == BAD_REQUEST:
if not content: LOG.error("The Password Server did not recognize the "
LOG.warning("The Password Server did not have any " "request.")
"password for the current instance.")
continue
if content == BAD_REQUEST:
LOG.error("The Password Server did not recognise the "
"request.")
break
if content == SAVED_PASSWORD:
LOG.warning("For this instance the password was already "
"taken from the Password Server.")
break
LOG.info("The password server return a valid password "
"for the current instance.")
password = encoding.get_as_string(content)
break break
if content == SAVED_PASSWORD:
LOG.warning("The password was already taken from the "
"Password Server for the current instance.")
break
LOG.info("The password server returned a valid password "
"for the current instance.")
password = content
break
return password return password
def _delete_password(self): def _delete_password(self):
@ -182,21 +203,15 @@ class CloudStack(base.BaseHTTPMetadataService):
LOG.debug("Remove the password for this instance from the " LOG.debug("Remove the password for this instance from the "
"Password Server.") "Password Server.")
headers = {"DomU_Request": "saved_password"} headers = {"DomU_Request": "saved_password"}
connection = http_client.HTTPConnection(self._router_ip, 8080,
timeout=TIMEOUT)
for _ in range(CONF.retry_count): for _ in range(CONF.retry_count):
connection.request("GET", "/", headers=headers) try:
response = connection.getresponse() content = self._password_client(headers=headers).strip()
if response.status != 200: except http_client.HTTPConnection as exc:
LOG.warning("Removing password failed: %(status)s " LOG.error("Removing password failed: %s", exc)
"%(reason)s - %(message)r",
{"status": response.status,
"reason": response.reason,
"message": response.read()})
continue continue
content = response.read() if content != BAD_REQUEST:
if content != BAD_REQUEST: # comparing bytes with bytes
LOG.info("The password was removed from the Password Server.") LOG.info("The password was removed from the Password Server.")
break break
else: else:

View File

@ -170,20 +170,15 @@ class CloudStackTest(unittest.TestCase):
response = self._service.get_public_keys() response = self._service.get_public_keys()
self.assertEqual([], response) self.assertEqual([], response)
@mock.patch('six.moves.http_client.HTTPConnection') @mock.patch('cloudbaseinit.metadata.services.cloudstack.CloudStack'
def test_get_password(self, mock_http_connection): '._password_client')
def test_get_password(self, mock_password_client):
headers = {"DomU_Request": "send_my_password"} headers = {"DomU_Request": "send_my_password"}
mock_connection = mock.Mock() expected_password = "password"
mock_http_connection.return_value = mock_connection mock_password_client.return_value = expected_password
mock_response = mock_connection.getresponse()
mock_request = mock_connection.request
mock_response.status = 200
expected_password = b"password"
mock_response.read.side_effect = [expected_password]
self._service._router_ip = mock.sentinel.router_ip
expected_output = [ expected_output = [
"Try to get password from the Password Server.", "Try to get password from the Password Server.",
"The password server return a valid password " "The password server returned a valid password "
"for the current instance." "for the current instance."
] ]
@ -191,29 +186,22 @@ class CloudStackTest(unittest.TestCase):
'cloudstack') as snatcher: 'cloudstack') as snatcher:
password = self._service._get_password() password = self._service._get_password()
mock_http_connection.assert_called_once_with( mock_password_client.assert_called_once_with(headers=headers)
mock.sentinel.router_ip, 8080, timeout=cloudstack.TIMEOUT) self.assertEqual(expected_password, password)
mock_request.assert_called_once_with("GET", "/", headers=headers)
self.assertEqual(expected_password.decode(), password)
self.assertEqual(expected_output, snatcher.output) self.assertEqual(expected_output, snatcher.output)
@mock.patch('six.moves.http_client.HTTPConnection') @mock.patch('cloudbaseinit.metadata.services.cloudstack.CloudStack'
def test_get_password_fail(self, mock_http_connection): '._password_client')
mock_connection = mock.Mock() def test_get_password_fail(self, mock_password_client):
mock_http_connection.return_value = mock_connection mock_password_client.side_effect = ["", cloudstack.BAD_REQUEST,
mock_response = mock_connection.getresponse() cloudstack.SAVED_PASSWORD]
mock_request = mock_connection.request
mock_response.status = 200
mock_response.read.side_effect = [b"", cloudstack.BAD_REQUEST,
cloudstack.SAVED_PASSWORD]
expected_output = [ expected_output = [
["Try to get password from the Password Server.", ["Try to get password from the Password Server.",
"For this instance the password was already taken from " "The password was already taken from the Password Server "
"the Password Server."], "for the current instance."],
["Try to get password from the Password Server.", ["Try to get password from the Password Server.",
"The Password Server did not recognise the request."], "The Password Server did not recognize the request."],
["Try to get password from the Password Server.", ["Try to get password from the Password Server.",
"The Password Server did not have any password for the " "The Password Server did not have any password for the "
@ -225,21 +213,16 @@ class CloudStackTest(unittest.TestCase):
self.assertIsNone(self._service._get_password()) self.assertIsNone(self._service._get_password())
self.assertEqual(expected_output.pop(), snatcher.output) self.assertEqual(expected_output.pop(), snatcher.output)
self.assertEqual(3, mock_request.call_count) self.assertEqual(3, mock_password_client.call_count)
@mock.patch('six.moves.http_client.HTTPConnection') @mock.patch('cloudbaseinit.metadata.services.cloudstack.CloudStack'
def test_delete_password(self, mock_http_connection): '._password_client')
mock_connection = mock.Mock() def test_delete_password(self, mock_password_client):
mock_http_connection.return_value = mock_connection mock_password_client.side_effect = [cloudstack.BAD_REQUEST,
mock_response = mock_connection.getresponse() cloudstack.SAVED_PASSWORD]
mock_request = mock_connection.request
mock_response.read.side_effect = [cloudstack.BAD_REQUEST,
cloudstack.SAVED_PASSWORD]
mock_response.status = 400
expected_output = [ expected_output = [
'Remove the password for this instance from the ' 'Remove the password for this instance from the '
'Password Server.', 'Password Server.',
'Removing password failed',
'Fail to remove the password from the Password Server.', 'Fail to remove the password from the Password Server.',
'Remove the password for this instance from the ' 'Remove the password for this instance from the '
@ -251,9 +234,8 @@ class CloudStackTest(unittest.TestCase):
with testutils.LogSnatcher('cloudbaseinit.metadata.services.' with testutils.LogSnatcher('cloudbaseinit.metadata.services.'
'cloudstack') as snatcher: 'cloudstack') as snatcher:
self.assertIsNone(self._service._delete_password()) self.assertIsNone(self._service._delete_password())
mock_response.status = 200
self.assertIsNone(self._service._delete_password()) self.assertIsNone(self._service._delete_password())
self.assertEqual(2, mock_request.call_count) self.assertEqual(2, mock_password_client.call_count)
for expected, output in zip(expected_output, snatcher.output): for expected, output in zip(expected_output, snatcher.output):
self.assertTrue(output.startswith(expected)) self.assertTrue(output.startswith(expected))