-
Notifications
You must be signed in to change notification settings - Fork 182
Expand file tree
/
Copy pathbase.py
More file actions
396 lines (307 loc) · 11.9 KB
/
Copy pathbase.py
File metadata and controls
396 lines (307 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# Copyright 2012 Cloudbase Solutions Srl
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import abc
import copy
import gzip
import io
import time
from oslo_log import log as oslo_logging
import requests
from cloudbaseinit import conf as cloudbaseinit_conf
from cloudbaseinit import exception
from cloudbaseinit.utils import encoding
CONF = cloudbaseinit_conf.CONF
LOG = oslo_logging.getLogger(__name__)
EXPERIMENTAL_NOTICE = ("EXPERIMENTAL: The structure and format of content "
"scoped under the 'ds' key may change in subsequent "
"releases of cloud-init.")
class NotExistingMetadataException(Exception):
pass
class BaseMetadataService(object, metaclass=abc.ABCMeta):
_GZIP_MAGIC_NUMBER = b'\x1f\x8b'
def __init__(self):
self._cache = {}
self._enable_retry = False
def get_name(self):
return self.__class__.__name__
def load(self):
self._cache = {}
@abc.abstractmethod
def _get_data(self, path):
pass
def _exec_with_retry(self, action):
i = 0
while True:
try:
return action()
except NotExistingMetadataException:
raise
except Exception:
if self._enable_retry and i < CONF.retry_count:
i += 1
time.sleep(CONF.retry_count_interval)
else:
raise
def _get_cache_data(self, path, decode=False):
"""Get meta data with caching and decoding support."""
key = (path, decode)
if key in self._cache:
LOG.debug("Using cached copy of metadata: '%s'" % path)
return self._cache[key]
else:
data = self._exec_with_retry(lambda: self._get_data(path))
if decode:
data = encoding.get_as_string(data)
self._cache[key] = data
return data
def get_instance_id(self):
pass
def get_content(self, name):
"""Get raw content within a service."""
def get_user_data(self):
pass
def get_decoded_user_data(self):
"""Get the decoded user data, if any
The user data can be gzip-encoded, which means
that every access to it should verify this fact,
leading to code duplication.
"""
user_data = self.get_user_data()
if user_data and user_data[:2] == self._GZIP_MAGIC_NUMBER:
bio = io.BytesIO(user_data)
with gzip.GzipFile(fileobj=bio, mode='rb') as out:
user_data = out.read()
return user_data
def get_host_name(self):
pass
def get_public_keys(self):
"""Get a list of space-stripped strings as public keys."""
pass
def get_user_pwd_encryption_key(self):
"""Get the user password encryption public key as a string.
The encryption public key, if existent, will be used to encrypt the
user password to be sent to the metadata service.
By default, the first public key set by the user
will be used to encrypt the user password.
"""
public_keys = self.get_public_keys()
if public_keys:
return list(public_keys)[0]
def get_network_details(self):
"""Return a list of `NetworkDetails` objects.
These objects provide details regarding static
network configuration, details which can be found
in the namedtuple defined above.
"""
def get_network_details_v2(self):
"""Return a `NetworkDetailsV2` object."""
def get_admin_username(self):
pass
def get_admin_password(self):
pass
def confirm_admin_password(self, password):
"""Mark the admin password as applied (no-op by default)."""
@property
def can_post_password(self):
return False
@property
def is_password_set(self):
return False
def post_password(self, enc_password_b64):
pass
def get_winrm_listeners_configuration(self):
pass
def get_server_certs(self):
pass
def get_vm_agent_package_provisioning_data(self):
pass
def get_client_auth_certs(self):
pass
def cleanup(self):
pass
@property
def can_update_password(self):
"""The ability to update password of the metadata provider.
If :meth:`~can_update_password` is True, plugins can check
periodically (e.g. at every boot) if the password changed.
:rtype: bool
.. notes:
The password will be updated only if the
:meth:`~is_password_changed` returns True.
"""
return False
def is_password_changed(self):
"""Check if the metadata provider has a new password for this instance
:rtype: bool
.. notes:
This method will be used only when :meth:`~can_update_password`
is True.
"""
return False
def provisioning_started(self):
pass
def provisioning_completed(self):
pass
def provisioning_failed(self):
pass
@property
def can_post_rdp_cert_thumbprint(self):
return False
def post_rdp_cert_thumbprint(self, thumbprint):
pass
def get_kms_host(self):
pass
def get_use_avma_licensing(self):
pass
def get_enable_automatic_updates(self):
"""Check if the metadata provider enforces automatic updates."""
pass
def get_ephemeral_disk_data_loss_warning(self):
raise NotExistingMetadataException()
def get_instance_data(self):
"""Returns a dictionary with instance data from the metadata source
The instance data structure is based on the cloud-init specifications:
https://cloudinit.readthedocs.io/en/latest/topics/instancedata.html
The v1 namespace contains a subset of the cloud-init standard
for the instance data. In the future, it should reach parity with the
cloud-init standard.
The ds.meta_data namespace contains all the values the v1 namespace
contains, in order to be compatible with cloud-init, plus a subset of
other instance data.
The ds namespace can change without prior notice and should not be
used in production.
"""
instance_id = self.get_instance_id()
hostname = self.get_host_name()
v1_data = {
"instance-id": instance_id,
"instance_id": instance_id,
"local-hostname": hostname,
"local_hostname": hostname,
}
# Copy the v1 data to the ds.meta_data and add more fields
ds_meta_data = self._get_datasource_instance_meta_data()
if not ds_meta_data:
ds_meta_data = copy.deepcopy(v1_data)
ds_meta_data.update({
"hostname": hostname
})
v1_data["public_ssh_keys"] = self.get_public_keys()
md = {
"v1": v1_data,
"ds": {
"_doc": EXPERIMENTAL_NOTICE,
"meta_data": ds_meta_data,
},
"instance-id": instance_id,
"instance_id": instance_id,
"local-hostname": hostname,
"local_hostname": hostname,
"public_ssh_keys": self.get_public_keys()
}
return md
def _get_datasource_instance_meta_data(self):
"""Returns a dictionary with datasource specific instance data
The instance data structure is based on the cloud-init specifications:
https://cloudinit.readthedocs.io/en/latest/explanation/instancedata.html
Datasource-specific metadata crawled for the specific cloud platform.
It should closely represent the structure of the cloud metadata
crawled. The structure of content and details provided are entirely
cloud-dependent.
"""
pass
class BaseHTTPMetadataService(BaseMetadataService):
"""Contract class for metadata services that are using HTTP(S)."""
def __init__(self, base_url, https_allow_insecure=False,
https_ca_bundle=None):
"""Setup a new metadata service.
:param https_allow_insecure:
Whether to disable the validation of HTTPS certificates
(default False).
:param base_url:
The base URL where the service looks for metadata.
:param https_ca_bundle:
The path to a CA_BUNDLE file or directory with certificates
of trusted CAs.
.. note ::
If `https_ca_bundle` is set to a path to a directory, the
directory must have been processed using the c_rehash utility
supplied with OpenSSL.
"""
super(BaseHTTPMetadataService, self).__init__()
self._https_allow_insecure = https_allow_insecure
self._https_ca_bundle = https_ca_bundle
self._base_url = base_url
def _verify_https_request(self):
"""Whether to disable the validation of HTTPS certificates.
When this option is `True` the SSL certificate validation for the
current metadata provider will be disabled (please don't use it if
you don't know the implications of this behaviour).
"""
if self._https_ca_bundle:
return self._https_ca_bundle
else:
return self._https_allow_insecure
def _http_request(self, url, data=None, headers=None, method=None):
"""Get content for received url."""
if not url.startswith("http"):
url = requests.compat.urljoin(self._base_url, url)
if not method:
if data:
method = "POST"
else:
method = "GET"
method = method.upper()
LOG.debug('Executing http request %s at %s', method, url)
response = requests.request(method=method, url=url, data=data,
headers=headers,
verify=self._verify_https_request())
response.raise_for_status()
return response.content
def _get_data(self, path):
"""Getting the required information using metadata service."""
try:
response = self._http_request(path)
except requests.HTTPError as exc:
if exc.response.status_code == 404:
raise NotExistingMetadataException(
getattr(exc, "message", str(exc)))
raise
except requests.exceptions.SSLError as exc:
LOG.exception(exc)
raise exception.CertificateVerifyFailed(
"HTTPS certificate validation failed.")
except (requests.ConnectionError, requests.Timeout) as exc:
LOG.exception(exc)
raise
return response
class EmptyMetadataService(BaseMetadataService):
"""Empty metadata service implementation.
The empty metadata service can be used to run plugins that do not
rely on metadata service information, like setting ntp, mtu, etc.
It can be used also as a fallback metadata service, in case no other
previous metadata service could be loaded.
"""
def __init__(self):
super(EmptyMetadataService, self).__init__()
def _get_data(self, path):
pass
def load(self):
return True
def get_admin_username(self):
raise NotExistingMetadataException()
def get_admin_password(self):
raise NotExistingMetadataException()
def is_password_changed(self):
raise NotExistingMetadataException()