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",
deprecated_name="cloudstack_metadata_ip",
deprecated_group="DEFAULT"),
cfg.IntOpt(
"password_server_port", default=8080,
help="The port number used by the Password Server."
),
cfg.BoolOpt(
"https_allow_insecure", default=False,
help="Whether to disable the validation of HTTPS "

View File

@ -27,25 +27,34 @@ from cloudbaseinit.utils import encoding
CONF = cloudbaseinit_conf.CONF
LOG = oslo_logging.getLogger(__name__)
BAD_REQUEST = b"bad_request"
SAVED_PASSWORD = b"saved_password"
BAD_REQUEST = "bad_request"
SAVED_PASSWORD = "saved_password"
TIMEOUT = 10
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):
# Note: The base url used by the current metadata service will be
# updated later by the `_test_api` method.
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,
https_allow_insecure=CONF.cloudstack.https_allow_insecure,
https_ca_bundle=CONF.cloudstack.https_ca_bundle)
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."""
return posixpath.normpath(
posixpath.join(version, "meta-data", resource))
@ -68,7 +77,7 @@ class CloudStack(base.BaseHTTPMetadataService):
LOG.debug('Available services: %s', response)
netloc = urllib.parse.urlparse(metadata_url).netloc
self._router_ip = netloc.split(":")[0]
self._metadata_host = netloc.split(":")[0]
return True
def load(self):
@ -114,13 +123,37 @@ class CloudStack(base.BaseHTTPMetadataService):
ssh_keys.append(ssh_key)
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):
"""Get the password from the Password Server.
The Password Server can be found on the DHCP_SERVER on the port 8080.
.. note:
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
* `saved_password`: the password was already deleted from
the Password Server
@ -132,45 +165,33 @@ class CloudStack(base.BaseHTTPMetadataService):
headers = {"DomU_Request": "send_my_password"}
password = None
with contextlib.closing(http_client.HTTPConnection(
self._router_ip, 8080, timeout=TIMEOUT)) as connection:
for _ in range(CONF.retry_count):
try:
connection.request("GET", "/", headers=headers)
response = connection.getresponse()
except http_client.HTTPException as exc:
LOG.exception(exc)
continue
for _ in range(CONF.retry_count):
try:
content = self._password_client(headers=headers).strip()
except http_client.HTTPConnection as exc:
LOG.error("Getting password failed: %s", exc)
continue
if response.status != 200:
LOG.warning("Getting password failed: %(status)s "
"%(reason)s - %(message)r",
{"status": response.status,
"reason": response.reason,
"message": response.read()})
continue
if not content:
LOG.warning("The Password Server did not have any "
"password for the current instance.")
continue
content = response.read().strip()
if not content:
LOG.warning("The Password Server did not have any "
"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)
if content == BAD_REQUEST:
LOG.error("The Password Server did not recognize the "
"request.")
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
def _delete_password(self):
@ -182,21 +203,15 @@ class CloudStack(base.BaseHTTPMetadataService):
LOG.debug("Remove the password for this instance from the "
"Password Server.")
headers = {"DomU_Request": "saved_password"}
connection = http_client.HTTPConnection(self._router_ip, 8080,
timeout=TIMEOUT)
for _ in range(CONF.retry_count):
connection.request("GET", "/", headers=headers)
response = connection.getresponse()
if response.status != 200:
LOG.warning("Removing password failed: %(status)s "
"%(reason)s - %(message)r",
{"status": response.status,
"reason": response.reason,
"message": response.read()})
try:
content = self._password_client(headers=headers).strip()
except http_client.HTTPConnection as exc:
LOG.error("Removing password failed: %s", exc)
continue
content = response.read()
if content != BAD_REQUEST: # comparing bytes with bytes
if content != BAD_REQUEST:
LOG.info("The password was removed from the Password Server.")
break
else:

View File

@ -170,20 +170,15 @@ class CloudStackTest(unittest.TestCase):
response = self._service.get_public_keys()
self.assertEqual([], response)
@mock.patch('six.moves.http_client.HTTPConnection')
def test_get_password(self, mock_http_connection):
@mock.patch('cloudbaseinit.metadata.services.cloudstack.CloudStack'
'._password_client')
def test_get_password(self, mock_password_client):
headers = {"DomU_Request": "send_my_password"}
mock_connection = mock.Mock()
mock_http_connection.return_value = mock_connection
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_password = "password"
mock_password_client.return_value = expected_password
expected_output = [
"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."
]
@ -191,29 +186,22 @@ class CloudStackTest(unittest.TestCase):
'cloudstack') as snatcher:
password = self._service._get_password()
mock_http_connection.assert_called_once_with(
mock.sentinel.router_ip, 8080, timeout=cloudstack.TIMEOUT)
mock_request.assert_called_once_with("GET", "/", headers=headers)
self.assertEqual(expected_password.decode(), password)
mock_password_client.assert_called_once_with(headers=headers)
self.assertEqual(expected_password, password)
self.assertEqual(expected_output, snatcher.output)
@mock.patch('six.moves.http_client.HTTPConnection')
def test_get_password_fail(self, mock_http_connection):
mock_connection = mock.Mock()
mock_http_connection.return_value = mock_connection
mock_response = mock_connection.getresponse()
mock_request = mock_connection.request
mock_response.status = 200
mock_response.read.side_effect = [b"", cloudstack.BAD_REQUEST,
cloudstack.SAVED_PASSWORD]
@mock.patch('cloudbaseinit.metadata.services.cloudstack.CloudStack'
'._password_client')
def test_get_password_fail(self, mock_password_client):
mock_password_client.side_effect = ["", cloudstack.BAD_REQUEST,
cloudstack.SAVED_PASSWORD]
expected_output = [
["Try to get password from the Password Server.",
"For this instance the password was already taken from "
"the Password Server."],
"The password was already taken from the Password Server "
"for the current instance."],
["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.",
"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.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')
def test_delete_password(self, mock_http_connection):
mock_connection = mock.Mock()
mock_http_connection.return_value = mock_connection
mock_response = mock_connection.getresponse()
mock_request = mock_connection.request
mock_response.read.side_effect = [cloudstack.BAD_REQUEST,
cloudstack.SAVED_PASSWORD]
mock_response.status = 400
@mock.patch('cloudbaseinit.metadata.services.cloudstack.CloudStack'
'._password_client')
def test_delete_password(self, mock_password_client):
mock_password_client.side_effect = [cloudstack.BAD_REQUEST,
cloudstack.SAVED_PASSWORD]
expected_output = [
'Remove the password for this instance from the '
'Password Server.',
'Removing password failed',
'Fail to remove the password from the Password Server.',
'Remove the password for this instance from the '
@ -251,9 +234,8 @@ class CloudStackTest(unittest.TestCase):
with testutils.LogSnatcher('cloudbaseinit.metadata.services.'
'cloudstack') as snatcher:
self.assertIsNone(self._service._delete_password())
mock_response.status = 200
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):
self.assertTrue(output.startswith(expected))