Skip to content

Commit d53e765

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "packet: add phone_home and post_password support"
2 parents fd3309c + 107bdfa commit d53e765

File tree

4 files changed

+164
-0
lines changed

4 files changed

+164
-0
lines changed

cloudbaseinit/exception.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ class MetadataNotFoundException(CloudbaseInitException):
4141
pass
4242

4343

44+
class MetadataEndpointException(CloudbaseInitException):
45+
46+
"""Exception thrown in case the metadata is unresponsive or errors out."""
47+
48+
pass
49+
50+
4451
class CertificateVerifyFailed(ServiceException):
4552

4653
"""The received certificate is not valid.

cloudbaseinit/metadata/services/packet.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414
"""Metadata Service for Packet."""
1515

1616
import json
17+
import requests
1718

1819
from cloudbaseinit import conf as cloudbaseinit_conf
20+
from cloudbaseinit import exception
1921
from cloudbaseinit.metadata.services import base
2022
from oslo_log import log as oslo_logging
23+
from six.moves.urllib import error
2124

2225
CONF = cloudbaseinit_conf.CONF
2326
LOG = oslo_logging.getLogger(__name__)
@@ -83,3 +86,49 @@ def get_public_keys(self):
8386
def get_user_data(self):
8487
"""Get the available user data for the current instance."""
8588
return self._get_cache_data("userdata")
89+
90+
def _get_phone_home_url(self):
91+
return self._get_meta_data().get("phone_home_url")
92+
93+
def get_user_pwd_encryption_key(self):
94+
phone_home_url = self._get_phone_home_url()
95+
key_url = requests.compat.urljoin('%s/' % phone_home_url, "key")
96+
return self._get_cache_data(key_url, decode=True)
97+
98+
@property
99+
def can_post_password(self):
100+
"""The Packet metadata service supports posting the password."""
101+
return True
102+
103+
def post_password(self, enc_password_b64):
104+
phone_home_url = self._get_phone_home_url()
105+
LOG.info("Posting password to: %s", phone_home_url)
106+
try:
107+
action = lambda: self._http_request(
108+
url=phone_home_url,
109+
data=json.dumps({'password': enc_password_b64.decode()}))
110+
return self._exec_with_retry(action)
111+
except error.HTTPError as exc:
112+
LOG.exception(exc)
113+
raise exception.MetadataEndpointException(
114+
"Failed to post password to the metadata service")
115+
116+
def provisioning_completed(self):
117+
"""Signal to Packet that the instance is ready.
118+
119+
To complete the provisioning, on the first boot after installation
120+
make a GET request to CONF.packet.metadata_url, which will return a
121+
JSON object which contains phone_home_url entry.
122+
Make a POST request to phone_home_url with no body (important!)
123+
and this will complete the installation process.
124+
"""
125+
phone_home_url = self._get_phone_home_url()
126+
LOG.info("Calling home to: %s", phone_home_url)
127+
try:
128+
action = lambda: self._http_request(url=phone_home_url,
129+
method="post")
130+
return self._exec_with_retry(action)
131+
except error.HTTPError as exc:
132+
LOG.exception(exc)
133+
raise exception.MetadataEndpointException(
134+
"Failed to call home to the metadata service")

cloudbaseinit/tests/metadata/services/test_packet.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
except ImportError:
2121
import mock
2222

23+
from six.moves.urllib import error
24+
2325
from cloudbaseinit import conf as cloudbaseinit_conf
26+
from cloudbaseinit import exception
2427
from cloudbaseinit.tests import testutils
2528

2629

@@ -121,3 +124,106 @@ def test_get_user_data(self, mock_get_cache_data):
121124
response = self._packet_service.get_user_data()
122125
mock_get_cache_data.assert_called_once_with("userdata")
123126
self.assertEqual(mock_get_cache_data.return_value, response)
127+
128+
@mock.patch(MODULE_PATH +
129+
".PacketService._get_meta_data")
130+
def test_get_phone_home_url(self, mock_get_meta_data):
131+
fake_phone_url = 'fake_phone_url'
132+
mock_get_meta_data.return_value = {
133+
"phone_home_url": fake_phone_url
134+
}
135+
response = self._packet_service._get_phone_home_url()
136+
137+
self.assertEqual(response, fake_phone_url)
138+
139+
def test_can_post_password(self):
140+
self.assertEqual(self._packet_service.can_post_password,
141+
True)
142+
143+
@mock.patch(MODULE_PATH +
144+
".PacketService._get_phone_home_url")
145+
@mock.patch(MODULE_PATH +
146+
".PacketService._get_cache_data")
147+
def test_get_user_pwd_encryption_key(self, mock_get_cache_data,
148+
mock_get_phone_url):
149+
fake_phone_url = 'fake_phone_url'
150+
user_pwd_encryption_key = 'fake_key'
151+
152+
mock_get_cache_data.return_value = user_pwd_encryption_key
153+
mock_get_phone_url.return_value = fake_phone_url
154+
155+
response = self._packet_service.get_user_pwd_encryption_key()
156+
mock_get_phone_url.assert_called_once()
157+
mock_get_cache_data.assert_called_once_with(
158+
"%s/%s" % (fake_phone_url, 'key'), decode=True)
159+
160+
self.assertEqual(response, user_pwd_encryption_key)
161+
162+
@mock.patch('time.sleep')
163+
@mock.patch(MODULE_PATH +
164+
".PacketService._get_phone_home_url")
165+
@mock.patch(MODULE_PATH +
166+
".PacketService._http_request")
167+
def _test_post_password(self, mock_http_request,
168+
mock_get_phone_url, mock_sleep, fail=False):
169+
fake_phone_url = 'fake_phone_url'
170+
fake_response = 'fake_response'
171+
fake_encoded_password = b'fake_password'
172+
173+
if fail:
174+
mock_http_request.side_effect = (
175+
error.HTTPError(401, "invalid", {}, 0, 0))
176+
with self.assertRaises(exception.MetadataEndpointException):
177+
self._packet_service.post_password(fake_encoded_password)
178+
else:
179+
mock_http_request.return_value = fake_response
180+
mock_get_phone_url.return_value = fake_phone_url
181+
182+
response = self._packet_service.post_password(
183+
fake_encoded_password)
184+
mock_get_phone_url.assert_called_once()
185+
mock_http_request.assert_called_once_with(
186+
data='{"password": "fake_password"}',
187+
url=fake_phone_url)
188+
189+
self.assertEqual(response, fake_response)
190+
191+
def test_post_password(self):
192+
self._test_post_password()
193+
194+
def test_post_password_with_failure(self):
195+
self._test_post_password(fail=True)
196+
197+
@mock.patch('time.sleep')
198+
@mock.patch(MODULE_PATH +
199+
".PacketService._get_phone_home_url")
200+
@mock.patch(MODULE_PATH +
201+
".PacketService._http_request")
202+
def _test_provisioning_completed(self, mock_http_request,
203+
mock_get_phone_url, mock_sleep,
204+
fail=False):
205+
fake_phone_url = 'fake_phone_url'
206+
fake_response = 'fake_response'
207+
208+
if fail:
209+
mock_http_request.side_effect = (
210+
error.HTTPError(401, "invalid", {}, 0, 0))
211+
with self.assertRaises(exception.MetadataEndpointException):
212+
self._packet_service.provisioning_completed()
213+
else:
214+
mock_http_request.return_value = fake_response
215+
mock_get_phone_url.return_value = fake_phone_url
216+
217+
response = self._packet_service.provisioning_completed()
218+
mock_get_phone_url.assert_called_once()
219+
mock_http_request.assert_called_once_with(
220+
url=fake_phone_url,
221+
method="post")
222+
223+
self.assertEqual(response, fake_response)
224+
225+
def test_provisioning_completed(self):
226+
self._test_provisioning_completed()
227+
228+
def test_provisioning_completed_with_failure(self):
229+
self._test_provisioning_completed(fail=True)

doc/source/services.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,9 @@ Capabilities:
338338
* instance id
339339
* hostname
340340
* public keys
341+
* post admin user password (only once)
341342
* user data
343+
* call home on successful provision
342344

343345
Config options for `packet` section:
344346

0 commit comments

Comments
 (0)