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:
Alexandru Coman 2015-03-26 15:29:16 +02:00
parent 40cc7b8fdb
commit fc3b29454b
2 changed files with 209 additions and 1 deletions

View File

@ -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

View File

@ -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)