Add support for change admin password
The admin password can be obtained from the second API from the DHCP_SERVER which is on port 8080. Implements: blueprint cloudstack-metadata Change-Id: I7dc73eba33ba923a09d313546b771f96f6f6076c
This commit is contained in:
parent
40cc7b8fdb
commit
fc3b29454b
@ -12,7 +12,10 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import contextlib
|
||||
|
||||
from oslo.config import cfg
|
||||
from six.moves import http_client
|
||||
from six.moves import urllib
|
||||
|
||||
from cloudbaseinit.metadata.services import base
|
||||
@ -28,6 +31,10 @@ OPTS = [
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(OPTS)
|
||||
|
||||
BAD_REQUEST = b"bad_request"
|
||||
SAVED_PASSWORD = b"saved_password"
|
||||
TIMEOUT = 10
|
||||
|
||||
|
||||
class CloudStack(base.BaseMetadataService):
|
||||
|
||||
@ -116,3 +123,108 @@ class CloudStack(base.BaseMetadataService):
|
||||
continue
|
||||
ssh_keys.append(ssh_key)
|
||||
return ssh_keys
|
||||
|
||||
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
|
||||
the request
|
||||
* `saved_password`: the password was already deleted from
|
||||
the Password Server
|
||||
* ``: the Password Server did not have any
|
||||
password for this instance
|
||||
* the password
|
||||
"""
|
||||
LOG.debug("Try to get password from the Password Server.")
|
||||
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
|
||||
|
||||
if response.status != 200:
|
||||
LOG.warning("Getting password failed: %(status)s "
|
||||
"%(reason)s - %(message)s",
|
||||
{"status": response.status,
|
||||
"reason": response.reason,
|
||||
"message": response.read()})
|
||||
continue
|
||||
|
||||
content = response.read()
|
||||
content = content.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 = content.decode()
|
||||
break
|
||||
|
||||
return password
|
||||
|
||||
def _delete_password(self):
|
||||
"""Delete the password from the Password Server.
|
||||
|
||||
After the password is used, it must be deleted from the Password
|
||||
Server for security reasons.
|
||||
"""
|
||||
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)s",
|
||||
{"status": response.status,
|
||||
"reason": response.reason,
|
||||
"message": response.read()})
|
||||
continue
|
||||
|
||||
content = response.read()
|
||||
if content.decode() != BAD_REQUEST:
|
||||
LOG.info("The password was removed from the Password Server.")
|
||||
break
|
||||
else:
|
||||
LOG.warning("Fail to remove the password from the "
|
||||
"Password Server.")
|
||||
|
||||
def get_admin_password(self):
|
||||
"""Get the admin pasword from the Password Server.
|
||||
|
||||
.. note:
|
||||
The password is deleted from the Password Server after the first
|
||||
call of this method.
|
||||
Another request for password will work only if the password was
|
||||
changed and sent to the Password Server.
|
||||
"""
|
||||
password = self._get_password()
|
||||
if password:
|
||||
self._delete_password()
|
||||
return password
|
||||
|
@ -33,6 +33,7 @@ class CloudStackTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
CONF.set_override('retry_count_interval', 0)
|
||||
CONF.set_override('retry_count', 1)
|
||||
self._service = self._get_service()
|
||||
self._service._metadata_uri = "http://10.1.1.1/latest/meta-data/"
|
||||
|
||||
@ -56,7 +57,7 @@ class CloudStackTest(unittest.TestCase):
|
||||
]
|
||||
|
||||
self.assertTrue(self._service._test_api(url))
|
||||
for _ in range(4):
|
||||
for _ in range(3):
|
||||
self.assertFalse(self._service._test_api(url))
|
||||
|
||||
@mock.patch('cloudbaseinit.osutils.factory.get_os_utils')
|
||||
@ -202,3 +203,98 @@ class CloudStackTest(unittest.TestCase):
|
||||
mock_urlopen = mock_urllib_request.urlopen.return_value
|
||||
mock_urlopen.read.assert_called_once_with()
|
||||
self.assertEqual(expected_logging, snatcher.output)
|
||||
|
||||
@mock.patch('six.moves.http_client.HTTPConnection')
|
||||
def test_get_password(self, mock_http_connection):
|
||||
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_output = [
|
||||
"Try to get password from the Password Server.",
|
||||
"The password server return a valid password "
|
||||
"for the current instance."
|
||||
]
|
||||
|
||||
with testutils.LogSnatcher('cloudbaseinit.metadata.services.'
|
||||
'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)
|
||||
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]
|
||||
expected_output = [
|
||||
["Try to get password from the Password Server.",
|
||||
"For this instance the password was already taken from "
|
||||
"the Password Server."],
|
||||
|
||||
["Try to get password from the Password Server.",
|
||||
"The Password Server did not recognise the request."],
|
||||
|
||||
["Try to get password from the Password Server.",
|
||||
"The Password Server did not have any password for the "
|
||||
"current instance."],
|
||||
]
|
||||
for _ in range(3):
|
||||
with testutils.LogSnatcher('cloudbaseinit.metadata.services.'
|
||||
'cloudstack') as snatcher:
|
||||
self.assertIsNone(self._service._get_password())
|
||||
self.assertEqual(expected_output.pop(), snatcher.output)
|
||||
|
||||
self.assertEqual(3, mock_request.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
|
||||
self.assertIsNone(self._service._delete_password())
|
||||
mock_response.status = 200
|
||||
self.assertIsNone(self._service._delete_password())
|
||||
self.assertEqual(2, mock_request.call_count)
|
||||
|
||||
@mock.patch('cloudbaseinit.metadata.services.cloudstack.CloudStack.'
|
||||
'_delete_password')
|
||||
@mock.patch('cloudbaseinit.metadata.services.cloudstack.CloudStack.'
|
||||
'_get_password')
|
||||
def test_get_admin_password(self, mock_get_password, mock_delete_password):
|
||||
mock_get_password.return_value = mock.sentinel.password
|
||||
password = self._service.get_admin_password()
|
||||
|
||||
self.assertEqual(mock.sentinel.password, password)
|
||||
self.assertEqual(1, mock_get_password.call_count)
|
||||
self.assertEqual(1, mock_delete_password.call_count)
|
||||
|
||||
@mock.patch('cloudbaseinit.metadata.services.cloudstack.CloudStack.'
|
||||
'_delete_password')
|
||||
@mock.patch('cloudbaseinit.metadata.services.cloudstack.CloudStack.'
|
||||
'_get_password')
|
||||
def test_get_admin_password_fail(self, mock_get_password,
|
||||
mock_delete_password):
|
||||
mock_get_password.return_value = None
|
||||
|
||||
self.assertIsNone(self._service.get_admin_password())
|
||||
self.assertEqual(1, mock_get_password.call_count)
|
||||
self.assertEqual(0, mock_delete_password.call_count)
|
||||
|
Loading…
x
Reference in New Issue
Block a user