Merge "Refactor the CloudStack Metadata Service"
This commit is contained in:
commit
c2db56a10b
@ -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 "
|
||||||
|
@ -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:
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user