Merge "packet: add phone_home and post_password support"
This commit is contained in:
commit
d53e765b26
@ -41,6 +41,13 @@ class MetadataNotFoundException(CloudbaseInitException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataEndpointException(CloudbaseInitException):
|
||||||
|
|
||||||
|
"""Exception thrown in case the metadata is unresponsive or errors out."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CertificateVerifyFailed(ServiceException):
|
class CertificateVerifyFailed(ServiceException):
|
||||||
|
|
||||||
"""The received certificate is not valid.
|
"""The received certificate is not valid.
|
||||||
|
@ -14,10 +14,13 @@
|
|||||||
"""Metadata Service for Packet."""
|
"""Metadata Service for Packet."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import requests
|
||||||
|
|
||||||
from cloudbaseinit import conf as cloudbaseinit_conf
|
from cloudbaseinit import conf as cloudbaseinit_conf
|
||||||
|
from cloudbaseinit import exception
|
||||||
from cloudbaseinit.metadata.services import base
|
from cloudbaseinit.metadata.services import base
|
||||||
from oslo_log import log as oslo_logging
|
from oslo_log import log as oslo_logging
|
||||||
|
from six.moves.urllib import error
|
||||||
|
|
||||||
CONF = cloudbaseinit_conf.CONF
|
CONF = cloudbaseinit_conf.CONF
|
||||||
LOG = oslo_logging.getLogger(__name__)
|
LOG = oslo_logging.getLogger(__name__)
|
||||||
@ -83,3 +86,49 @@ class PacketService(base.BaseHTTPMetadataService):
|
|||||||
def get_user_data(self):
|
def get_user_data(self):
|
||||||
"""Get the available user data for the current instance."""
|
"""Get the available user data for the current instance."""
|
||||||
return self._get_cache_data("userdata")
|
return self._get_cache_data("userdata")
|
||||||
|
|
||||||
|
def _get_phone_home_url(self):
|
||||||
|
return self._get_meta_data().get("phone_home_url")
|
||||||
|
|
||||||
|
def get_user_pwd_encryption_key(self):
|
||||||
|
phone_home_url = self._get_phone_home_url()
|
||||||
|
key_url = requests.compat.urljoin('%s/' % phone_home_url, "key")
|
||||||
|
return self._get_cache_data(key_url, decode=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_post_password(self):
|
||||||
|
"""The Packet metadata service supports posting the password."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def post_password(self, enc_password_b64):
|
||||||
|
phone_home_url = self._get_phone_home_url()
|
||||||
|
LOG.info("Posting password to: %s", phone_home_url)
|
||||||
|
try:
|
||||||
|
action = lambda: self._http_request(
|
||||||
|
url=phone_home_url,
|
||||||
|
data=json.dumps({'password': enc_password_b64.decode()}))
|
||||||
|
return self._exec_with_retry(action)
|
||||||
|
except error.HTTPError as exc:
|
||||||
|
LOG.exception(exc)
|
||||||
|
raise exception.MetadataEndpointException(
|
||||||
|
"Failed to post password to the metadata service")
|
||||||
|
|
||||||
|
def provisioning_completed(self):
|
||||||
|
"""Signal to Packet that the instance is ready.
|
||||||
|
|
||||||
|
To complete the provisioning, on the first boot after installation
|
||||||
|
make a GET request to CONF.packet.metadata_url, which will return a
|
||||||
|
JSON object which contains phone_home_url entry.
|
||||||
|
Make a POST request to phone_home_url with no body (important!)
|
||||||
|
and this will complete the installation process.
|
||||||
|
"""
|
||||||
|
phone_home_url = self._get_phone_home_url()
|
||||||
|
LOG.info("Calling home to: %s", phone_home_url)
|
||||||
|
try:
|
||||||
|
action = lambda: self._http_request(url=phone_home_url,
|
||||||
|
method="post")
|
||||||
|
return self._exec_with_retry(action)
|
||||||
|
except error.HTTPError as exc:
|
||||||
|
LOG.exception(exc)
|
||||||
|
raise exception.MetadataEndpointException(
|
||||||
|
"Failed to call home to the metadata service")
|
||||||
|
@ -20,7 +20,10 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
|
from six.moves.urllib import error
|
||||||
|
|
||||||
from cloudbaseinit import conf as cloudbaseinit_conf
|
from cloudbaseinit import conf as cloudbaseinit_conf
|
||||||
|
from cloudbaseinit import exception
|
||||||
from cloudbaseinit.tests import testutils
|
from cloudbaseinit.tests import testutils
|
||||||
|
|
||||||
|
|
||||||
@ -121,3 +124,106 @@ class PacketServiceTest(unittest.TestCase):
|
|||||||
response = self._packet_service.get_user_data()
|
response = self._packet_service.get_user_data()
|
||||||
mock_get_cache_data.assert_called_once_with("userdata")
|
mock_get_cache_data.assert_called_once_with("userdata")
|
||||||
self.assertEqual(mock_get_cache_data.return_value, response)
|
self.assertEqual(mock_get_cache_data.return_value, response)
|
||||||
|
|
||||||
|
@mock.patch(MODULE_PATH +
|
||||||
|
".PacketService._get_meta_data")
|
||||||
|
def test_get_phone_home_url(self, mock_get_meta_data):
|
||||||
|
fake_phone_url = 'fake_phone_url'
|
||||||
|
mock_get_meta_data.return_value = {
|
||||||
|
"phone_home_url": fake_phone_url
|
||||||
|
}
|
||||||
|
response = self._packet_service._get_phone_home_url()
|
||||||
|
|
||||||
|
self.assertEqual(response, fake_phone_url)
|
||||||
|
|
||||||
|
def test_can_post_password(self):
|
||||||
|
self.assertEqual(self._packet_service.can_post_password,
|
||||||
|
True)
|
||||||
|
|
||||||
|
@mock.patch(MODULE_PATH +
|
||||||
|
".PacketService._get_phone_home_url")
|
||||||
|
@mock.patch(MODULE_PATH +
|
||||||
|
".PacketService._get_cache_data")
|
||||||
|
def test_get_user_pwd_encryption_key(self, mock_get_cache_data,
|
||||||
|
mock_get_phone_url):
|
||||||
|
fake_phone_url = 'fake_phone_url'
|
||||||
|
user_pwd_encryption_key = 'fake_key'
|
||||||
|
|
||||||
|
mock_get_cache_data.return_value = user_pwd_encryption_key
|
||||||
|
mock_get_phone_url.return_value = fake_phone_url
|
||||||
|
|
||||||
|
response = self._packet_service.get_user_pwd_encryption_key()
|
||||||
|
mock_get_phone_url.assert_called_once()
|
||||||
|
mock_get_cache_data.assert_called_once_with(
|
||||||
|
"%s/%s" % (fake_phone_url, 'key'), decode=True)
|
||||||
|
|
||||||
|
self.assertEqual(response, user_pwd_encryption_key)
|
||||||
|
|
||||||
|
@mock.patch('time.sleep')
|
||||||
|
@mock.patch(MODULE_PATH +
|
||||||
|
".PacketService._get_phone_home_url")
|
||||||
|
@mock.patch(MODULE_PATH +
|
||||||
|
".PacketService._http_request")
|
||||||
|
def _test_post_password(self, mock_http_request,
|
||||||
|
mock_get_phone_url, mock_sleep, fail=False):
|
||||||
|
fake_phone_url = 'fake_phone_url'
|
||||||
|
fake_response = 'fake_response'
|
||||||
|
fake_encoded_password = b'fake_password'
|
||||||
|
|
||||||
|
if fail:
|
||||||
|
mock_http_request.side_effect = (
|
||||||
|
error.HTTPError(401, "invalid", {}, 0, 0))
|
||||||
|
with self.assertRaises(exception.MetadataEndpointException):
|
||||||
|
self._packet_service.post_password(fake_encoded_password)
|
||||||
|
else:
|
||||||
|
mock_http_request.return_value = fake_response
|
||||||
|
mock_get_phone_url.return_value = fake_phone_url
|
||||||
|
|
||||||
|
response = self._packet_service.post_password(
|
||||||
|
fake_encoded_password)
|
||||||
|
mock_get_phone_url.assert_called_once()
|
||||||
|
mock_http_request.assert_called_once_with(
|
||||||
|
data='{"password": "fake_password"}',
|
||||||
|
url=fake_phone_url)
|
||||||
|
|
||||||
|
self.assertEqual(response, fake_response)
|
||||||
|
|
||||||
|
def test_post_password(self):
|
||||||
|
self._test_post_password()
|
||||||
|
|
||||||
|
def test_post_password_with_failure(self):
|
||||||
|
self._test_post_password(fail=True)
|
||||||
|
|
||||||
|
@mock.patch('time.sleep')
|
||||||
|
@mock.patch(MODULE_PATH +
|
||||||
|
".PacketService._get_phone_home_url")
|
||||||
|
@mock.patch(MODULE_PATH +
|
||||||
|
".PacketService._http_request")
|
||||||
|
def _test_provisioning_completed(self, mock_http_request,
|
||||||
|
mock_get_phone_url, mock_sleep,
|
||||||
|
fail=False):
|
||||||
|
fake_phone_url = 'fake_phone_url'
|
||||||
|
fake_response = 'fake_response'
|
||||||
|
|
||||||
|
if fail:
|
||||||
|
mock_http_request.side_effect = (
|
||||||
|
error.HTTPError(401, "invalid", {}, 0, 0))
|
||||||
|
with self.assertRaises(exception.MetadataEndpointException):
|
||||||
|
self._packet_service.provisioning_completed()
|
||||||
|
else:
|
||||||
|
mock_http_request.return_value = fake_response
|
||||||
|
mock_get_phone_url.return_value = fake_phone_url
|
||||||
|
|
||||||
|
response = self._packet_service.provisioning_completed()
|
||||||
|
mock_get_phone_url.assert_called_once()
|
||||||
|
mock_http_request.assert_called_once_with(
|
||||||
|
url=fake_phone_url,
|
||||||
|
method="post")
|
||||||
|
|
||||||
|
self.assertEqual(response, fake_response)
|
||||||
|
|
||||||
|
def test_provisioning_completed(self):
|
||||||
|
self._test_provisioning_completed()
|
||||||
|
|
||||||
|
def test_provisioning_completed_with_failure(self):
|
||||||
|
self._test_provisioning_completed(fail=True)
|
||||||
|
@ -338,7 +338,9 @@ Capabilities:
|
|||||||
* instance id
|
* instance id
|
||||||
* hostname
|
* hostname
|
||||||
* public keys
|
* public keys
|
||||||
|
* post admin user password (only once)
|
||||||
* user data
|
* user data
|
||||||
|
* call home on successful provision
|
||||||
|
|
||||||
Config options for `packet` section:
|
Config options for `packet` section:
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user