Merge ~chad.smith/cloud-init:datasource-ibmcloud into cloud-init:master
- Git
- lp:~chad.smith/cloud-init
- datasource-ibmcloud
- Merge into master
Status: | Merged |
---|---|
Approved by: | Chad Smith |
Approved revision: | 332275089657698b78826e8adbdc5ed62069ced8 |
Merge reported by: | Chad Smith |
Merged at revision: | e0f644b7c8c76bd63d242558685722cc70d9c51d |
Proposed branch: | ~chad.smith/cloud-init:datasource-ibmcloud |
Merge into: | cloud-init:master |
Diff against target: |
1049 lines (+857/-12) 7 files modified
cloudinit/sources/DataSourceConfigDrive.py (+10/-0) cloudinit/sources/DataSourceIBMCloud.py (+325/-0) cloudinit/tests/test_util.py (+72/-0) cloudinit/util.py (+31/-0) tests/unittests/test_datasource/test_ibmcloud.py (+262/-0) tests/unittests/test_ds_identify.py (+101/-3) tools/ds-identify (+56/-9) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
Chad Smith | Approve | ||
Review via email: mp+342009@code.launchpad.net |
Commit message
IBMCloud: Initial IBM Cloud datasource.
This adds a specific IBM Cloud datasource.
IBM Cloud is identified by:
a.) running on xen
b.) one of a LABEL=METADATA disk or a LABEL=config-2 disk with
UUID=9796-932E
The datasource contains its own config-drive reader that reads
only the currently supported portion of config-drive needed for
ibm cloud.
During the provisioning boot, cloud-init is disabled.
See the docstring in DataSourceIBMCl
Description of the change
see commit message. Shepherd smoser's branch into tip
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:4641cef07c6
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
FAILED: Ubuntu LTS: Integration
Click here to trigger a rebuild:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:cca002cca78
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
SUCCESS: Ubuntu LTS: Integration
SUCCESS: MAAS Compatability Testing
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild:
https:/
Chad Smith (chad.smith) wrote : | # |
An upstream commit landed for this bug.
To view that commit see the following URL:
https:/
Preview Diff
1 | diff --git a/cloudinit/sources/DataSourceConfigDrive.py b/cloudinit/sources/DataSourceConfigDrive.py | |||
2 | index b8db626..c7b5fe5 100644 | |||
3 | --- a/cloudinit/sources/DataSourceConfigDrive.py | |||
4 | +++ b/cloudinit/sources/DataSourceConfigDrive.py | |||
5 | @@ -14,6 +14,7 @@ from cloudinit import util | |||
6 | 14 | 14 | ||
7 | 15 | from cloudinit.net import eni | 15 | from cloudinit.net import eni |
8 | 16 | 16 | ||
9 | 17 | from cloudinit.sources.DataSourceIBMCloud import get_ibm_platform | ||
10 | 17 | from cloudinit.sources.helpers import openstack | 18 | from cloudinit.sources.helpers import openstack |
11 | 18 | 19 | ||
12 | 19 | LOG = logging.getLogger(__name__) | 20 | LOG = logging.getLogger(__name__) |
13 | @@ -255,6 +256,15 @@ def find_candidate_devs(probe_optical=True): | |||
14 | 255 | # an unpartitioned block device (ex sda, not sda1) | 256 | # an unpartitioned block device (ex sda, not sda1) |
15 | 256 | devices = [d for d in candidates | 257 | devices = [d for d in candidates |
16 | 257 | if d in by_label or not util.is_partition(d)] | 258 | if d in by_label or not util.is_partition(d)] |
17 | 259 | |||
18 | 260 | if devices: | ||
19 | 261 | # IBMCloud uses config-2 label, but limited to a single UUID. | ||
20 | 262 | ibm_platform, ibm_path = get_ibm_platform() | ||
21 | 263 | if ibm_path in devices: | ||
22 | 264 | devices.remove(ibm_path) | ||
23 | 265 | LOG.debug("IBMCloud device '%s' (%s) removed from candidate list", | ||
24 | 266 | ibm_path, ibm_platform) | ||
25 | 267 | |||
26 | 258 | return devices | 268 | return devices |
27 | 259 | 269 | ||
28 | 260 | 270 | ||
29 | diff --git a/cloudinit/sources/DataSourceIBMCloud.py b/cloudinit/sources/DataSourceIBMCloud.py | |||
30 | 261 | new file mode 100644 | 271 | new file mode 100644 |
31 | index 0000000..02b3d56 | |||
32 | --- /dev/null | |||
33 | +++ b/cloudinit/sources/DataSourceIBMCloud.py | |||
34 | @@ -0,0 +1,325 @@ | |||
35 | 1 | # This file is part of cloud-init. See LICENSE file for license information. | ||
36 | 2 | """Datasource for IBMCloud. | ||
37 | 3 | |||
38 | 4 | IBMCloud is also know as SoftLayer or BlueMix. | ||
39 | 5 | IBMCloud hypervisor is xen (2018-03-10). | ||
40 | 6 | |||
41 | 7 | There are 2 different api exposed launch methods. | ||
42 | 8 | * template: This is the legacy method of launching instances. | ||
43 | 9 | When booting from an image template, the system boots first into | ||
44 | 10 | a "provisioning" mode. There, host <-> guest mechanisms are utilized | ||
45 | 11 | to execute code in the guest and provision it. | ||
46 | 12 | |||
47 | 13 | Cloud-init will disable itself when it detects that it is in the | ||
48 | 14 | provisioning mode. It detects this by the presence of | ||
49 | 15 | a file '/root/provisioningConfiguration.cfg'. | ||
50 | 16 | |||
51 | 17 | When provided with user-data, the "first boot" will contain a | ||
52 | 18 | ConfigDrive-like disk labeled with 'METADATA'. If there is no user-data | ||
53 | 19 | provided, then there is no data-source. | ||
54 | 20 | |||
55 | 21 | Cloud-init never does any network configuration in this mode. | ||
56 | 22 | |||
57 | 23 | * os_code: Essentially "launch by OS Code" (Operating System Code). | ||
58 | 24 | This is a more modern approach. There is no specific "provisioning" boot. | ||
59 | 25 | Instead, cloud-init does all the customization. With or without | ||
60 | 26 | user-data provided, an OpenStack ConfigDrive like disk is attached. | ||
61 | 27 | |||
62 | 28 | Only disks with label 'config-2' and UUID '9796-932E' are considered. | ||
63 | 29 | This is to avoid this datasource claiming ConfigDrive. This does | ||
64 | 30 | mean that 1 in 8^16 (~4 billion) Xen ConfigDrive systems will be | ||
65 | 31 | incorrectly identified as IBMCloud. | ||
66 | 32 | |||
67 | 33 | TODO: | ||
68 | 34 | * is uuid (/sys/hypervisor/uuid) stable for life of an instance? | ||
69 | 35 | it seems it is not the same as data's uuid in the os_code case | ||
70 | 36 | but is in the template case. | ||
71 | 37 | |||
72 | 38 | """ | ||
73 | 39 | import base64 | ||
74 | 40 | import json | ||
75 | 41 | import os | ||
76 | 42 | |||
77 | 43 | from cloudinit import log as logging | ||
78 | 44 | from cloudinit import sources | ||
79 | 45 | from cloudinit.sources.helpers import openstack | ||
80 | 46 | from cloudinit import util | ||
81 | 47 | |||
82 | 48 | LOG = logging.getLogger(__name__) | ||
83 | 49 | |||
84 | 50 | IBM_CONFIG_UUID = "9796-932E" | ||
85 | 51 | |||
86 | 52 | |||
87 | 53 | class Platforms(object): | ||
88 | 54 | TEMPLATE_LIVE_METADATA = "Template/Live/Metadata" | ||
89 | 55 | TEMPLATE_LIVE_NODATA = "UNABLE TO BE IDENTIFIED." | ||
90 | 56 | TEMPLATE_PROVISIONING_METADATA = "Template/Provisioning/Metadata" | ||
91 | 57 | TEMPLATE_PROVISIONING_NODATA = "Template/Provisioning/No-Metadata" | ||
92 | 58 | OS_CODE = "OS-Code/Live" | ||
93 | 59 | |||
94 | 60 | |||
95 | 61 | PROVISIONING = ( | ||
96 | 62 | Platforms.TEMPLATE_PROVISIONING_METADATA, | ||
97 | 63 | Platforms.TEMPLATE_PROVISIONING_NODATA) | ||
98 | 64 | |||
99 | 65 | |||
100 | 66 | class DataSourceIBMCloud(sources.DataSource): | ||
101 | 67 | |||
102 | 68 | dsname = 'IBMCloud' | ||
103 | 69 | system_uuid = None | ||
104 | 70 | |||
105 | 71 | def __init__(self, sys_cfg, distro, paths): | ||
106 | 72 | super(DataSourceIBMCloud, self).__init__(sys_cfg, distro, paths) | ||
107 | 73 | self.source = None | ||
108 | 74 | self._network_config = None | ||
109 | 75 | self.network_json = None | ||
110 | 76 | self.platform = None | ||
111 | 77 | |||
112 | 78 | def __str__(self): | ||
113 | 79 | root = super(DataSourceIBMCloud, self).__str__() | ||
114 | 80 | mstr = "%s [%s %s]" % (root, self.platform, self.source) | ||
115 | 81 | return mstr | ||
116 | 82 | |||
117 | 83 | def _get_data(self): | ||
118 | 84 | results = read_md() | ||
119 | 85 | if results is None: | ||
120 | 86 | return False | ||
121 | 87 | |||
122 | 88 | self.source = results['source'] | ||
123 | 89 | self.platform = results['platform'] | ||
124 | 90 | self.metadata = results['metadata'] | ||
125 | 91 | self.userdata_raw = results.get('userdata') | ||
126 | 92 | self.network_json = results.get('networkdata') | ||
127 | 93 | vd = results.get('vendordata') | ||
128 | 94 | self.vendordata_pure = vd | ||
129 | 95 | self.system_uuid = results['system-uuid'] | ||
130 | 96 | try: | ||
131 | 97 | self.vendordata_raw = sources.convert_vendordata(vd) | ||
132 | 98 | except ValueError as e: | ||
133 | 99 | LOG.warning("Invalid content in vendor-data: %s", e) | ||
134 | 100 | self.vendordata_raw = None | ||
135 | 101 | |||
136 | 102 | return True | ||
137 | 103 | |||
138 | 104 | def check_instance_id(self, sys_cfg): | ||
139 | 105 | """quickly (local check only) if self.instance_id is still valid | ||
140 | 106 | |||
141 | 107 | in Template mode, the system uuid (/sys/hypervisor/uuid) is the | ||
142 | 108 | same as found in the METADATA disk. But that is not true in OS_CODE | ||
143 | 109 | mode. So we read the system_uuid and keep that for later compare.""" | ||
144 | 110 | if self.system_uuid is None: | ||
145 | 111 | return False | ||
146 | 112 | return self.system_uuid == _read_system_uuid() | ||
147 | 113 | |||
148 | 114 | @property | ||
149 | 115 | def network_config(self): | ||
150 | 116 | if self.platform != Platforms.OS_CODE: | ||
151 | 117 | # If deployed from template, an agent in the provisioning | ||
152 | 118 | # environment handles networking configuration. Not cloud-init. | ||
153 | 119 | return {'config': 'disabled', 'version': 1} | ||
154 | 120 | if self._network_config is None: | ||
155 | 121 | if self.network_json is not None: | ||
156 | 122 | LOG.debug("network config provided via network_json") | ||
157 | 123 | self._network_config = openstack.convert_net_json( | ||
158 | 124 | self.network_json, known_macs=None) | ||
159 | 125 | else: | ||
160 | 126 | LOG.debug("no network configuration available.") | ||
161 | 127 | return self._network_config | ||
162 | 128 | |||
163 | 129 | |||
164 | 130 | def _read_system_uuid(): | ||
165 | 131 | uuid_path = "/sys/hypervisor/uuid" | ||
166 | 132 | if not os.path.isfile(uuid_path): | ||
167 | 133 | return None | ||
168 | 134 | return util.load_file(uuid_path).strip().lower() | ||
169 | 135 | |||
170 | 136 | |||
171 | 137 | def _is_xen(): | ||
172 | 138 | return os.path.exists("/proc/xen") | ||
173 | 139 | |||
174 | 140 | |||
175 | 141 | def _is_ibm_provisioning(): | ||
176 | 142 | return os.path.exists("/root/provisioningConfiguration.cfg") | ||
177 | 143 | |||
178 | 144 | |||
179 | 145 | def get_ibm_platform(): | ||
180 | 146 | """Return a tuple (Platform, path) | ||
181 | 147 | |||
182 | 148 | If this is Not IBM cloud, then the return value is (None, None). | ||
183 | 149 | An instance in provisioning mode is considered running on IBM cloud.""" | ||
184 | 150 | label_mdata = "METADATA" | ||
185 | 151 | label_cfg2 = "CONFIG-2" | ||
186 | 152 | not_found = (None, None) | ||
187 | 153 | |||
188 | 154 | if not _is_xen(): | ||
189 | 155 | return not_found | ||
190 | 156 | |||
191 | 157 | # fslabels contains only the first entry with a given label. | ||
192 | 158 | fslabels = {} | ||
193 | 159 | try: | ||
194 | 160 | devs = util.blkid() | ||
195 | 161 | except util.ProcessExecutionError as e: | ||
196 | 162 | LOG.warning("Failed to run blkid: %s", e) | ||
197 | 163 | return (None, None) | ||
198 | 164 | |||
199 | 165 | for dev in sorted(devs.keys()): | ||
200 | 166 | data = devs[dev] | ||
201 | 167 | label = data.get("LABEL", "").upper() | ||
202 | 168 | uuid = data.get("UUID", "").upper() | ||
203 | 169 | if label not in (label_mdata, label_cfg2): | ||
204 | 170 | continue | ||
205 | 171 | if label in fslabels: | ||
206 | 172 | LOG.warning("Duplicate fslabel '%s'. existing=%s current=%s", | ||
207 | 173 | label, fslabels[label], data) | ||
208 | 174 | continue | ||
209 | 175 | if label == label_cfg2 and uuid != IBM_CONFIG_UUID: | ||
210 | 176 | LOG.debug("Skipping %s with LABEL=%s due to uuid != %s: %s", | ||
211 | 177 | dev, label, uuid, data) | ||
212 | 178 | continue | ||
213 | 179 | fslabels[label] = data | ||
214 | 180 | |||
215 | 181 | metadata_path = fslabels.get(label_mdata, {}).get('DEVNAME') | ||
216 | 182 | cfg2_path = fslabels.get(label_cfg2, {}).get('DEVNAME') | ||
217 | 183 | |||
218 | 184 | if cfg2_path: | ||
219 | 185 | return (Platforms.OS_CODE, cfg2_path) | ||
220 | 186 | elif metadata_path: | ||
221 | 187 | if _is_ibm_provisioning(): | ||
222 | 188 | return (Platforms.TEMPLATE_PROVISIONING_METADATA, metadata_path) | ||
223 | 189 | else: | ||
224 | 190 | return (Platforms.TEMPLATE_LIVE_METADATA, metadata_path) | ||
225 | 191 | elif _is_ibm_provisioning(): | ||
226 | 192 | return (Platforms.TEMPLATE_PROVISIONING_NODATA, None) | ||
227 | 193 | return not_found | ||
228 | 194 | |||
229 | 195 | |||
230 | 196 | def read_md(): | ||
231 | 197 | """Read data from IBM Cloud. | ||
232 | 198 | |||
233 | 199 | @return: None if not running on IBM Cloud. | ||
234 | 200 | dictionary with guaranteed fields: metadata, version | ||
235 | 201 | and optional fields: userdata, vendordata, networkdata. | ||
236 | 202 | Also includes the system uuid from /sys/hypervisor/uuid.""" | ||
237 | 203 | platform, path = get_ibm_platform() | ||
238 | 204 | if platform is None: | ||
239 | 205 | LOG.debug("This is not an IBMCloud platform.") | ||
240 | 206 | return None | ||
241 | 207 | elif platform in PROVISIONING: | ||
242 | 208 | LOG.debug("Cloud-init is disabled during provisioning: %s.", | ||
243 | 209 | platform) | ||
244 | 210 | return None | ||
245 | 211 | |||
246 | 212 | ret = {'platform': platform, 'source': path, | ||
247 | 213 | 'system-uuid': _read_system_uuid()} | ||
248 | 214 | |||
249 | 215 | try: | ||
250 | 216 | if os.path.isdir(path): | ||
251 | 217 | results = metadata_from_dir(path) | ||
252 | 218 | else: | ||
253 | 219 | results = util.mount_cb(path, metadata_from_dir) | ||
254 | 220 | except BrokenMetadata as e: | ||
255 | 221 | raise RuntimeError( | ||
256 | 222 | "Failed reading IBM config disk (platform=%s path=%s): %s" % | ||
257 | 223 | (platform, path, e)) | ||
258 | 224 | |||
259 | 225 | ret.update(results) | ||
260 | 226 | return ret | ||
261 | 227 | |||
262 | 228 | |||
263 | 229 | class BrokenMetadata(IOError): | ||
264 | 230 | pass | ||
265 | 231 | |||
266 | 232 | |||
267 | 233 | def metadata_from_dir(source_dir): | ||
268 | 234 | """Walk source_dir extracting standardized metadata. | ||
269 | 235 | |||
270 | 236 | Certain metadata keys are renamed to present a standardized set of metadata | ||
271 | 237 | keys. | ||
272 | 238 | |||
273 | 239 | This function has a lot in common with ConfigDriveReader.read_v2 but | ||
274 | 240 | there are a number of inconsistencies, such key renames and as only | ||
275 | 241 | presenting a 'latest' version which make it an unlikely candidate to share | ||
276 | 242 | code. | ||
277 | 243 | |||
278 | 244 | @return: Dict containing translated metadata, userdata, vendordata, | ||
279 | 245 | networkdata as present. | ||
280 | 246 | """ | ||
281 | 247 | |||
282 | 248 | def opath(fname): | ||
283 | 249 | return os.path.join("openstack", "latest", fname) | ||
284 | 250 | |||
285 | 251 | def load_json_bytes(blob): | ||
286 | 252 | return json.loads(blob.decode('utf-8')) | ||
287 | 253 | |||
288 | 254 | files = [ | ||
289 | 255 | # tuples of (results_name, path, translator) | ||
290 | 256 | ('metadata_raw', opath('meta_data.json'), load_json_bytes), | ||
291 | 257 | ('userdata', opath('user_data'), None), | ||
292 | 258 | ('vendordata', opath('vendor_data.json'), load_json_bytes), | ||
293 | 259 | ('networkdata', opath('network_data.json'), load_json_bytes), | ||
294 | 260 | ] | ||
295 | 261 | |||
296 | 262 | results = {} | ||
297 | 263 | for (name, path, transl) in files: | ||
298 | 264 | fpath = os.path.join(source_dir, path) | ||
299 | 265 | raw = None | ||
300 | 266 | try: | ||
301 | 267 | raw = util.load_file(fpath, decode=False) | ||
302 | 268 | except IOError as e: | ||
303 | 269 | LOG.debug("Failed reading path '%s': %s", fpath, e) | ||
304 | 270 | |||
305 | 271 | if raw is None or transl is None: | ||
306 | 272 | data = raw | ||
307 | 273 | else: | ||
308 | 274 | try: | ||
309 | 275 | data = transl(raw) | ||
310 | 276 | except Exception as e: | ||
311 | 277 | raise BrokenMetadata("Failed decoding %s: %s" % (path, e)) | ||
312 | 278 | |||
313 | 279 | results[name] = data | ||
314 | 280 | |||
315 | 281 | if results.get('metadata_raw') is None: | ||
316 | 282 | raise BrokenMetadata( | ||
317 | 283 | "%s missing required file 'meta_data.json'" % source_dir) | ||
318 | 284 | |||
319 | 285 | results['metadata'] = {} | ||
320 | 286 | |||
321 | 287 | md_raw = results['metadata_raw'] | ||
322 | 288 | md = results['metadata'] | ||
323 | 289 | if 'random_seed' in md_raw: | ||
324 | 290 | try: | ||
325 | 291 | md['random_seed'] = base64.b64decode(md_raw['random_seed']) | ||
326 | 292 | except (ValueError, TypeError) as e: | ||
327 | 293 | raise BrokenMetadata( | ||
328 | 294 | "Badly formatted metadata random_seed entry: %s" % e) | ||
329 | 295 | |||
330 | 296 | renames = ( | ||
331 | 297 | ('public_keys', 'public-keys'), ('hostname', 'local-hostname'), | ||
332 | 298 | ('uuid', 'instance-id')) | ||
333 | 299 | for mdname, newname in renames: | ||
334 | 300 | if mdname in md_raw: | ||
335 | 301 | md[newname] = md_raw[mdname] | ||
336 | 302 | |||
337 | 303 | return results | ||
338 | 304 | |||
339 | 305 | |||
340 | 306 | # Used to match classes to dependencies | ||
341 | 307 | datasources = [ | ||
342 | 308 | (DataSourceIBMCloud, (sources.DEP_FILESYSTEM,)), | ||
343 | 309 | ] | ||
344 | 310 | |||
345 | 311 | |||
346 | 312 | # Return a list of data sources that match this set of dependencies | ||
347 | 313 | def get_datasource_list(depends): | ||
348 | 314 | return sources.list_from_depends(depends, datasources) | ||
349 | 315 | |||
350 | 316 | |||
351 | 317 | if __name__ == "__main__": | ||
352 | 318 | import argparse | ||
353 | 319 | |||
354 | 320 | parser = argparse.ArgumentParser(description='Query IBM Cloud Metadata') | ||
355 | 321 | args = parser.parse_args() | ||
356 | 322 | data = read_md() | ||
357 | 323 | print(util.json_dumps(data)) | ||
358 | 324 | |||
359 | 325 | # vi: ts=4 expandtab | ||
360 | diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py | |||
361 | index d30643d..3f37dbb 100644 | |||
362 | --- a/cloudinit/tests/test_util.py | |||
363 | +++ b/cloudinit/tests/test_util.py | |||
364 | @@ -3,6 +3,7 @@ | |||
365 | 3 | """Tests for cloudinit.util""" | 3 | """Tests for cloudinit.util""" |
366 | 4 | 4 | ||
367 | 5 | import logging | 5 | import logging |
368 | 6 | from textwrap import dedent | ||
369 | 6 | 7 | ||
370 | 7 | import cloudinit.util as util | 8 | import cloudinit.util as util |
371 | 8 | 9 | ||
372 | @@ -140,4 +141,75 @@ class TestGetHostnameFqdn(CiTestCase): | |||
373 | 140 | [{'fqdn': True, 'metadata_only': True}, | 141 | [{'fqdn': True, 'metadata_only': True}, |
374 | 141 | {'metadata_only': True}], mycloud.calls) | 142 | {'metadata_only': True}], mycloud.calls) |
375 | 142 | 143 | ||
376 | 144 | |||
377 | 145 | class TestBlkid(CiTestCase): | ||
378 | 146 | ids = { | ||
379 | 147 | "id01": "1111-1111", | ||
380 | 148 | "id02": "22222222-2222", | ||
381 | 149 | "id03": "33333333-3333", | ||
382 | 150 | "id04": "44444444-4444", | ||
383 | 151 | "id05": "55555555-5555-5555-5555-555555555555", | ||
384 | 152 | "id06": "66666666-6666-6666-6666-666666666666", | ||
385 | 153 | "id07": "52894610484658920398", | ||
386 | 154 | "id08": "86753098675309867530", | ||
387 | 155 | "id09": "99999999-9999-9999-9999-999999999999", | ||
388 | 156 | } | ||
389 | 157 | |||
390 | 158 | blkid_out = dedent("""\ | ||
391 | 159 | /dev/loop0: TYPE="squashfs" | ||
392 | 160 | /dev/loop1: TYPE="squashfs" | ||
393 | 161 | /dev/loop2: TYPE="squashfs" | ||
394 | 162 | /dev/loop3: TYPE="squashfs" | ||
395 | 163 | /dev/sda1: UUID="{id01}" TYPE="vfat" PARTUUID="{id02}" | ||
396 | 164 | /dev/sda2: UUID="{id03}" TYPE="ext4" PARTUUID="{id04}" | ||
397 | 165 | /dev/sda3: UUID="{id05}" TYPE="ext4" PARTUUID="{id06}" | ||
398 | 166 | /dev/sda4: LABEL="default" UUID="{id07}" UUID_SUB="{id08}" """ | ||
399 | 167 | """TYPE="zfs_member" PARTUUID="{id09}" | ||
400 | 168 | /dev/loop4: TYPE="squashfs" | ||
401 | 169 | """) | ||
402 | 170 | |||
403 | 171 | maxDiff = None | ||
404 | 172 | |||
405 | 173 | def _get_expected(self): | ||
406 | 174 | return ({ | ||
407 | 175 | "/dev/loop0": {"DEVNAME": "/dev/loop0", "TYPE": "squashfs"}, | ||
408 | 176 | "/dev/loop1": {"DEVNAME": "/dev/loop1", "TYPE": "squashfs"}, | ||
409 | 177 | "/dev/loop2": {"DEVNAME": "/dev/loop2", "TYPE": "squashfs"}, | ||
410 | 178 | "/dev/loop3": {"DEVNAME": "/dev/loop3", "TYPE": "squashfs"}, | ||
411 | 179 | "/dev/loop4": {"DEVNAME": "/dev/loop4", "TYPE": "squashfs"}, | ||
412 | 180 | "/dev/sda1": {"DEVNAME": "/dev/sda1", "TYPE": "vfat", | ||
413 | 181 | "UUID": self.ids["id01"], | ||
414 | 182 | "PARTUUID": self.ids["id02"]}, | ||
415 | 183 | "/dev/sda2": {"DEVNAME": "/dev/sda2", "TYPE": "ext4", | ||
416 | 184 | "UUID": self.ids["id03"], | ||
417 | 185 | "PARTUUID": self.ids["id04"]}, | ||
418 | 186 | "/dev/sda3": {"DEVNAME": "/dev/sda3", "TYPE": "ext4", | ||
419 | 187 | "UUID": self.ids["id05"], | ||
420 | 188 | "PARTUUID": self.ids["id06"]}, | ||
421 | 189 | "/dev/sda4": {"DEVNAME": "/dev/sda4", "TYPE": "zfs_member", | ||
422 | 190 | "LABEL": "default", | ||
423 | 191 | "UUID": self.ids["id07"], | ||
424 | 192 | "UUID_SUB": self.ids["id08"], | ||
425 | 193 | "PARTUUID": self.ids["id09"]}, | ||
426 | 194 | }) | ||
427 | 195 | |||
428 | 196 | @mock.patch("cloudinit.util.subp") | ||
429 | 197 | def test_functional_blkid(self, m_subp): | ||
430 | 198 | m_subp.return_value = ( | ||
431 | 199 | self.blkid_out.format(**self.ids), "") | ||
432 | 200 | self.assertEqual(self._get_expected(), util.blkid()) | ||
433 | 201 | m_subp.assert_called_with(["blkid", "-o", "full"], capture=True, | ||
434 | 202 | decode="replace") | ||
435 | 203 | |||
436 | 204 | @mock.patch("cloudinit.util.subp") | ||
437 | 205 | def test_blkid_no_cache_uses_no_cache(self, m_subp): | ||
438 | 206 | """blkid should turn off cache if disable_cache is true.""" | ||
439 | 207 | m_subp.return_value = ( | ||
440 | 208 | self.blkid_out.format(**self.ids), "") | ||
441 | 209 | self.assertEqual(self._get_expected(), | ||
442 | 210 | util.blkid(disable_cache=True)) | ||
443 | 211 | m_subp.assert_called_with(["blkid", "-o", "full", "-c", "/dev/null"], | ||
444 | 212 | capture=True, decode="replace") | ||
445 | 213 | |||
446 | 214 | |||
447 | 143 | # vi: ts=4 expandtab | 215 | # vi: ts=4 expandtab |
448 | diff --git a/cloudinit/util.py b/cloudinit/util.py | |||
449 | index cae8b19..fb4ee5f 100644 | |||
450 | --- a/cloudinit/util.py | |||
451 | +++ b/cloudinit/util.py | |||
452 | @@ -1237,6 +1237,37 @@ def find_devs_with(criteria=None, oformat='device', | |||
453 | 1237 | return entries | 1237 | return entries |
454 | 1238 | 1238 | ||
455 | 1239 | 1239 | ||
456 | 1240 | def blkid(devs=None, disable_cache=False): | ||
457 | 1241 | """Get all device tags details from blkid. | ||
458 | 1242 | |||
459 | 1243 | @param devs: Optional list of device paths you wish to query. | ||
460 | 1244 | @param disable_cache: Bool, set True to start with clean cache. | ||
461 | 1245 | |||
462 | 1246 | @return: Dict of key value pairs of info for the device. | ||
463 | 1247 | """ | ||
464 | 1248 | if devs is None: | ||
465 | 1249 | devs = [] | ||
466 | 1250 | else: | ||
467 | 1251 | devs = list(devs) | ||
468 | 1252 | |||
469 | 1253 | cmd = ['blkid', '-o', 'full'] | ||
470 | 1254 | if disable_cache: | ||
471 | 1255 | cmd.extend(['-c', '/dev/null']) | ||
472 | 1256 | cmd.extend(devs) | ||
473 | 1257 | |||
474 | 1258 | # we have to decode with 'replace' as shelx.split (called by | ||
475 | 1259 | # load_shell_content) can't take bytes. So this is potentially | ||
476 | 1260 | # lossy of non-utf-8 chars in blkid output. | ||
477 | 1261 | out, _ = subp(cmd, capture=True, decode="replace") | ||
478 | 1262 | ret = {} | ||
479 | 1263 | for line in out.splitlines(): | ||
480 | 1264 | dev, _, data = line.partition(":") | ||
481 | 1265 | ret[dev] = load_shell_content(data) | ||
482 | 1266 | ret[dev]["DEVNAME"] = dev | ||
483 | 1267 | |||
484 | 1268 | return ret | ||
485 | 1269 | |||
486 | 1270 | |||
487 | 1240 | def peek_file(fname, max_bytes): | 1271 | def peek_file(fname, max_bytes): |
488 | 1241 | LOG.debug("Peeking at %s (max_bytes=%s)", fname, max_bytes) | 1272 | LOG.debug("Peeking at %s (max_bytes=%s)", fname, max_bytes) |
489 | 1242 | with open(fname, 'rb') as ifh: | 1273 | with open(fname, 'rb') as ifh: |
490 | diff --git a/tests/unittests/test_datasource/test_ibmcloud.py b/tests/unittests/test_datasource/test_ibmcloud.py | |||
491 | 1243 | new file mode 100644 | 1274 | new file mode 100644 |
492 | index 0000000..621cfe4 | |||
493 | --- /dev/null | |||
494 | +++ b/tests/unittests/test_datasource/test_ibmcloud.py | |||
495 | @@ -0,0 +1,262 @@ | |||
496 | 1 | # This file is part of cloud-init. See LICENSE file for license information. | ||
497 | 2 | |||
498 | 3 | from cloudinit.sources import DataSourceIBMCloud as ibm | ||
499 | 4 | from cloudinit.tests import helpers as test_helpers | ||
500 | 5 | |||
501 | 6 | import base64 | ||
502 | 7 | import copy | ||
503 | 8 | import json | ||
504 | 9 | import mock | ||
505 | 10 | from textwrap import dedent | ||
506 | 11 | |||
507 | 12 | D_PATH = "cloudinit.sources.DataSourceIBMCloud." | ||
508 | 13 | |||
509 | 14 | |||
510 | 15 | class TestIBMCloud(test_helpers.CiTestCase): | ||
511 | 16 | """Test the datasource.""" | ||
512 | 17 | def setUp(self): | ||
513 | 18 | super(TestIBMCloud, self).setUp() | ||
514 | 19 | pass | ||
515 | 20 | |||
516 | 21 | |||
517 | 22 | @mock.patch(D_PATH + "_is_xen", return_value=True) | ||
518 | 23 | @mock.patch(D_PATH + "_is_ibm_provisioning") | ||
519 | 24 | @mock.patch(D_PATH + "util.blkid") | ||
520 | 25 | class TestGetIBMPlatform(test_helpers.CiTestCase): | ||
521 | 26 | """Test the get_ibm_platform helper.""" | ||
522 | 27 | |||
523 | 28 | blkid_base = { | ||
524 | 29 | "/dev/xvda1": { | ||
525 | 30 | "DEVNAME": "/dev/xvda1", "LABEL": "cloudimg-bootfs", | ||
526 | 31 | "TYPE": "ext3"}, | ||
527 | 32 | "/dev/xvda2": { | ||
528 | 33 | "DEVNAME": "/dev/xvda2", "LABEL": "cloudimg-rootfs", | ||
529 | 34 | "TYPE": "ext4"}, | ||
530 | 35 | } | ||
531 | 36 | |||
532 | 37 | blkid_metadata_disk = { | ||
533 | 38 | "/dev/xvdh1": { | ||
534 | 39 | "DEVNAME": "/dev/xvdh1", "LABEL": "METADATA", "TYPE": "vfat", | ||
535 | 40 | "SEC_TYPE": "msdos", "UUID": "681B-8C5D", | ||
536 | 41 | "PARTUUID": "3d631e09-01"}, | ||
537 | 42 | } | ||
538 | 43 | |||
539 | 44 | blkid_oscode_disk = { | ||
540 | 45 | "/dev/xvdh": { | ||
541 | 46 | "DEVNAME": "/dev/xvdh", "LABEL": "config-2", "TYPE": "vfat", | ||
542 | 47 | "SEC_TYPE": "msdos", "UUID": ibm.IBM_CONFIG_UUID} | ||
543 | 48 | } | ||
544 | 49 | |||
545 | 50 | def setUp(self): | ||
546 | 51 | self.blkid_metadata = copy.deepcopy(self.blkid_base) | ||
547 | 52 | self.blkid_metadata.update(copy.deepcopy(self.blkid_metadata_disk)) | ||
548 | 53 | |||
549 | 54 | self.blkid_oscode = copy.deepcopy(self.blkid_base) | ||
550 | 55 | self.blkid_oscode.update(copy.deepcopy(self.blkid_oscode_disk)) | ||
551 | 56 | |||
552 | 57 | def test_id_template_live_metadata(self, m_blkid, m_is_prov, _m_xen): | ||
553 | 58 | """identify TEMPLATE_LIVE_METADATA.""" | ||
554 | 59 | m_blkid.return_value = self.blkid_metadata | ||
555 | 60 | m_is_prov.return_value = False | ||
556 | 61 | self.assertEqual( | ||
557 | 62 | (ibm.Platforms.TEMPLATE_LIVE_METADATA, "/dev/xvdh1"), | ||
558 | 63 | ibm.get_ibm_platform()) | ||
559 | 64 | |||
560 | 65 | def test_id_template_prov_metadata(self, m_blkid, m_is_prov, _m_xen): | ||
561 | 66 | """identify TEMPLATE_PROVISIONING_METADATA.""" | ||
562 | 67 | m_blkid.return_value = self.blkid_metadata | ||
563 | 68 | m_is_prov.return_value = True | ||
564 | 69 | self.assertEqual( | ||
565 | 70 | (ibm.Platforms.TEMPLATE_PROVISIONING_METADATA, "/dev/xvdh1"), | ||
566 | 71 | ibm.get_ibm_platform()) | ||
567 | 72 | |||
568 | 73 | def test_id_template_prov_nodata(self, m_blkid, m_is_prov, _m_xen): | ||
569 | 74 | """identify TEMPLATE_PROVISIONING_NODATA.""" | ||
570 | 75 | m_blkid.return_value = self.blkid_base | ||
571 | 76 | m_is_prov.return_value = True | ||
572 | 77 | self.assertEqual( | ||
573 | 78 | (ibm.Platforms.TEMPLATE_PROVISIONING_NODATA, None), | ||
574 | 79 | ibm.get_ibm_platform()) | ||
575 | 80 | |||
576 | 81 | def test_id_os_code(self, m_blkid, m_is_prov, _m_xen): | ||
577 | 82 | """Identify OS_CODE.""" | ||
578 | 83 | m_blkid.return_value = self.blkid_oscode | ||
579 | 84 | m_is_prov.return_value = False | ||
580 | 85 | self.assertEqual((ibm.Platforms.OS_CODE, "/dev/xvdh"), | ||
581 | 86 | ibm.get_ibm_platform()) | ||
582 | 87 | |||
583 | 88 | def test_id_os_code_must_match_uuid(self, m_blkid, m_is_prov, _m_xen): | ||
584 | 89 | """Test against false positive on openstack with non-ibm UUID.""" | ||
585 | 90 | blkid = self.blkid_oscode | ||
586 | 91 | blkid["/dev/xvdh"]["UUID"] = "9999-9999" | ||
587 | 92 | m_blkid.return_value = blkid | ||
588 | 93 | m_is_prov.return_value = False | ||
589 | 94 | self.assertEqual((None, None), ibm.get_ibm_platform()) | ||
590 | 95 | |||
591 | 96 | |||
592 | 97 | @mock.patch(D_PATH + "_read_system_uuid", return_value=None) | ||
593 | 98 | @mock.patch(D_PATH + "get_ibm_platform") | ||
594 | 99 | class TestReadMD(test_helpers.CiTestCase): | ||
595 | 100 | """Test the read_datasource helper.""" | ||
596 | 101 | |||
597 | 102 | template_md = { | ||
598 | 103 | "files": [], | ||
599 | 104 | "network_config": {"content_path": "/content/interfaces"}, | ||
600 | 105 | "hostname": "ci-fond-ram", | ||
601 | 106 | "name": "ci-fond-ram", | ||
602 | 107 | "domain": "testing.ci.cloud-init.org", | ||
603 | 108 | "meta": {"dsmode": "net"}, | ||
604 | 109 | "uuid": "8e636730-9f5d-c4a5-327c-d7123c46e82f", | ||
605 | 110 | "public_keys": {"1091307": "ssh-rsa AAAAB3NzaC1...Hw== ci-pubkey"}, | ||
606 | 111 | } | ||
607 | 112 | |||
608 | 113 | oscode_md = { | ||
609 | 114 | "hostname": "ci-grand-gannet.testing.ci.cloud-init.org", | ||
610 | 115 | "name": "ci-grand-gannet", | ||
611 | 116 | "uuid": "2f266908-8e6c-4818-9b5c-42e9cc66a785", | ||
612 | 117 | "random_seed": "bm90LXJhbmRvbQo=", | ||
613 | 118 | "crypt_key": "ssh-rsa AAAAB3NzaC1yc2..n6z/", | ||
614 | 119 | "configuration_token": "eyJhbGciOi..M3ZA", | ||
615 | 120 | "public_keys": {"1091307": "ssh-rsa AAAAB3N..Hw== ci-pubkey"}, | ||
616 | 121 | } | ||
617 | 122 | |||
618 | 123 | content_interfaces = dedent("""\ | ||
619 | 124 | auto lo | ||
620 | 125 | iface lo inet loopback | ||
621 | 126 | |||
622 | 127 | auto eth0 | ||
623 | 128 | allow-hotplug eth0 | ||
624 | 129 | iface eth0 inet static | ||
625 | 130 | address 10.82.43.5 | ||
626 | 131 | netmask 255.255.255.192 | ||
627 | 132 | """) | ||
628 | 133 | |||
629 | 134 | userdata = b"#!/bin/sh\necho hi mom\n" | ||
630 | 135 | # meta.js file gets json encoded userdata as a list. | ||
631 | 136 | meta_js = '["#!/bin/sh\necho hi mom\n"]' | ||
632 | 137 | vendor_data = { | ||
633 | 138 | "cloud-init": "#!/bin/bash\necho 'root:$6$5ab01p1m1' | chpasswd -e"} | ||
634 | 139 | |||
635 | 140 | network_data = { | ||
636 | 141 | "links": [ | ||
637 | 142 | {"id": "interface_29402281", "name": "eth0", "mtu": None, | ||
638 | 143 | "type": "phy", "ethernet_mac_address": "06:00:f1:bd:da:25"}, | ||
639 | 144 | {"id": "interface_29402279", "name": "eth1", "mtu": None, | ||
640 | 145 | "type": "phy", "ethernet_mac_address": "06:98:5e:d0:7f:86"} | ||
641 | 146 | ], | ||
642 | 147 | "networks": [ | ||
643 | 148 | {"id": "network_109887563", "link": "interface_29402281", | ||
644 | 149 | "type": "ipv4", "ip_address": "10.82.43.2", | ||
645 | 150 | "netmask": "255.255.255.192", | ||
646 | 151 | "routes": [ | ||
647 | 152 | {"network": "10.0.0.0", "netmask": "255.0.0.0", | ||
648 | 153 | "gateway": "10.82.43.1"}, | ||
649 | 154 | {"network": "161.26.0.0", "netmask": "255.255.0.0", | ||
650 | 155 | "gateway": "10.82.43.1"}]}, | ||
651 | 156 | {"id": "network_109887551", "link": "interface_29402279", | ||
652 | 157 | "type": "ipv4", "ip_address": "108.168.194.252", | ||
653 | 158 | "netmask": "255.255.255.248", | ||
654 | 159 | "routes": [ | ||
655 | 160 | {"network": "0.0.0.0", "netmask": "0.0.0.0", | ||
656 | 161 | "gateway": "108.168.194.249"}]} | ||
657 | 162 | ], | ||
658 | 163 | "services": [ | ||
659 | 164 | {"type": "dns", "address": "10.0.80.11"}, | ||
660 | 165 | {"type": "dns", "address": "10.0.80.12"} | ||
661 | 166 | ], | ||
662 | 167 | } | ||
663 | 168 | |||
664 | 169 | sysuuid = '7f79ebf5-d791-43c3-a723-854e8389d59f' | ||
665 | 170 | |||
666 | 171 | def _get_expected_metadata(self, os_md): | ||
667 | 172 | """return expected 'metadata' for data loaded from meta_data.json.""" | ||
668 | 173 | os_md = copy.deepcopy(os_md) | ||
669 | 174 | renames = ( | ||
670 | 175 | ('hostname', 'local-hostname'), | ||
671 | 176 | ('uuid', 'instance-id'), | ||
672 | 177 | ('public_keys', 'public-keys')) | ||
673 | 178 | ret = {} | ||
674 | 179 | for osname, mdname in renames: | ||
675 | 180 | if osname in os_md: | ||
676 | 181 | ret[mdname] = os_md[osname] | ||
677 | 182 | if 'random_seed' in os_md: | ||
678 | 183 | ret['random_seed'] = base64.b64decode(os_md['random_seed']) | ||
679 | 184 | |||
680 | 185 | return ret | ||
681 | 186 | |||
682 | 187 | def test_provisioning_md(self, m_platform, m_sysuuid): | ||
683 | 188 | """Provisioning env with a metadata disk should return None.""" | ||
684 | 189 | m_platform.return_value = ( | ||
685 | 190 | ibm.Platforms.TEMPLATE_PROVISIONING_METADATA, "/dev/xvdh") | ||
686 | 191 | self.assertIsNone(ibm.read_md()) | ||
687 | 192 | |||
688 | 193 | def test_provisioning_no_metadata(self, m_platform, m_sysuuid): | ||
689 | 194 | """Provisioning env with no metadata disk should return None.""" | ||
690 | 195 | m_platform.return_value = ( | ||
691 | 196 | ibm.Platforms.TEMPLATE_PROVISIONING_NODATA, None) | ||
692 | 197 | self.assertIsNone(ibm.read_md()) | ||
693 | 198 | |||
694 | 199 | def test_provisioning_not_ibm(self, m_platform, m_sysuuid): | ||
695 | 200 | """Provisioning env but not identified as IBM should return None.""" | ||
696 | 201 | m_platform.return_value = (None, None) | ||
697 | 202 | self.assertIsNone(ibm.read_md()) | ||
698 | 203 | |||
699 | 204 | def test_template_live(self, m_platform, m_sysuuid): | ||
700 | 205 | """Template live environment should be identified.""" | ||
701 | 206 | tmpdir = self.tmp_dir() | ||
702 | 207 | m_platform.return_value = ( | ||
703 | 208 | ibm.Platforms.TEMPLATE_LIVE_METADATA, tmpdir) | ||
704 | 209 | m_sysuuid.return_value = self.sysuuid | ||
705 | 210 | |||
706 | 211 | test_helpers.populate_dir(tmpdir, { | ||
707 | 212 | 'openstack/latest/meta_data.json': json.dumps(self.template_md), | ||
708 | 213 | 'openstack/latest/user_data': self.userdata, | ||
709 | 214 | 'openstack/content/interfaces': self.content_interfaces, | ||
710 | 215 | 'meta.js': self.meta_js}) | ||
711 | 216 | |||
712 | 217 | ret = ibm.read_md() | ||
713 | 218 | self.assertEqual(ibm.Platforms.TEMPLATE_LIVE_METADATA, | ||
714 | 219 | ret['platform']) | ||
715 | 220 | self.assertEqual(tmpdir, ret['source']) | ||
716 | 221 | self.assertEqual(self.userdata, ret['userdata']) | ||
717 | 222 | self.assertEqual(self._get_expected_metadata(self.template_md), | ||
718 | 223 | ret['metadata']) | ||
719 | 224 | self.assertEqual(self.sysuuid, ret['system-uuid']) | ||
720 | 225 | |||
721 | 226 | def test_os_code_live(self, m_platform, m_sysuuid): | ||
722 | 227 | """Verify an os_code metadata path.""" | ||
723 | 228 | tmpdir = self.tmp_dir() | ||
724 | 229 | m_platform.return_value = (ibm.Platforms.OS_CODE, tmpdir) | ||
725 | 230 | netdata = json.dumps(self.network_data) | ||
726 | 231 | test_helpers.populate_dir(tmpdir, { | ||
727 | 232 | 'openstack/latest/meta_data.json': json.dumps(self.oscode_md), | ||
728 | 233 | 'openstack/latest/user_data': self.userdata, | ||
729 | 234 | 'openstack/latest/vendor_data.json': json.dumps(self.vendor_data), | ||
730 | 235 | 'openstack/latest/network_data.json': netdata, | ||
731 | 236 | }) | ||
732 | 237 | |||
733 | 238 | ret = ibm.read_md() | ||
734 | 239 | self.assertEqual(ibm.Platforms.OS_CODE, ret['platform']) | ||
735 | 240 | self.assertEqual(tmpdir, ret['source']) | ||
736 | 241 | self.assertEqual(self.userdata, ret['userdata']) | ||
737 | 242 | self.assertEqual(self._get_expected_metadata(self.oscode_md), | ||
738 | 243 | ret['metadata']) | ||
739 | 244 | |||
740 | 245 | def test_os_code_live_no_userdata(self, m_platform, m_sysuuid): | ||
741 | 246 | """Verify os_code without user-data.""" | ||
742 | 247 | tmpdir = self.tmp_dir() | ||
743 | 248 | m_platform.return_value = (ibm.Platforms.OS_CODE, tmpdir) | ||
744 | 249 | test_helpers.populate_dir(tmpdir, { | ||
745 | 250 | 'openstack/latest/meta_data.json': json.dumps(self.oscode_md), | ||
746 | 251 | 'openstack/latest/vendor_data.json': json.dumps(self.vendor_data), | ||
747 | 252 | }) | ||
748 | 253 | |||
749 | 254 | ret = ibm.read_md() | ||
750 | 255 | self.assertEqual(ibm.Platforms.OS_CODE, ret['platform']) | ||
751 | 256 | self.assertEqual(tmpdir, ret['source']) | ||
752 | 257 | self.assertIsNone(ret['userdata']) | ||
753 | 258 | self.assertEqual(self._get_expected_metadata(self.oscode_md), | ||
754 | 259 | ret['metadata']) | ||
755 | 260 | |||
756 | 261 | |||
757 | 262 | # vi: ts=4 expandtab | ||
758 | diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py | |||
759 | index 85999b7..5364398 100644 | |||
760 | --- a/tests/unittests/test_ds_identify.py | |||
761 | +++ b/tests/unittests/test_ds_identify.py | |||
762 | @@ -9,6 +9,8 @@ from cloudinit import util | |||
763 | 9 | from cloudinit.tests.helpers import ( | 9 | from cloudinit.tests.helpers import ( |
764 | 10 | CiTestCase, dir2dict, populate_dir) | 10 | CiTestCase, dir2dict, populate_dir) |
765 | 11 | 11 | ||
766 | 12 | from cloudinit.sources import DataSourceIBMCloud as dsibm | ||
767 | 13 | |||
768 | 12 | UNAME_MYSYS = ("Linux bart 4.4.0-62-generic #83-Ubuntu " | 14 | UNAME_MYSYS = ("Linux bart 4.4.0-62-generic #83-Ubuntu " |
769 | 13 | "SMP Wed Jan 18 14:10:15 UTC 2017 x86_64 GNU/Linux") | 15 | "SMP Wed Jan 18 14:10:15 UTC 2017 x86_64 GNU/Linux") |
770 | 14 | UNAME_PPC64EL = ("Linux diamond 4.4.0-83-generic #106-Ubuntu SMP " | 16 | UNAME_PPC64EL = ("Linux diamond 4.4.0-83-generic #106-Ubuntu SMP " |
771 | @@ -37,8 +39,8 @@ BLKID_UEFI_UBUNTU = [ | |||
772 | 37 | 39 | ||
773 | 38 | POLICY_FOUND_ONLY = "search,found=all,maybe=none,notfound=disabled" | 40 | POLICY_FOUND_ONLY = "search,found=all,maybe=none,notfound=disabled" |
774 | 39 | POLICY_FOUND_OR_MAYBE = "search,found=all,maybe=all,notfound=disabled" | 41 | POLICY_FOUND_OR_MAYBE = "search,found=all,maybe=all,notfound=disabled" |
777 | 40 | DI_DEFAULT_POLICY = "search,found=all,maybe=all,notfound=enabled" | 42 | DI_DEFAULT_POLICY = "search,found=all,maybe=all,notfound=disabled" |
778 | 41 | DI_DEFAULT_POLICY_NO_DMI = "search,found=all,maybe=all,notfound=disabled" | 43 | DI_DEFAULT_POLICY_NO_DMI = "search,found=all,maybe=all,notfound=enabled" |
779 | 42 | DI_EC2_STRICT_ID_DEFAULT = "true" | 44 | DI_EC2_STRICT_ID_DEFAULT = "true" |
780 | 43 | OVF_MATCH_STRING = 'http://schemas.dmtf.org/ovf/environment/1' | 45 | OVF_MATCH_STRING = 'http://schemas.dmtf.org/ovf/environment/1' |
781 | 44 | 46 | ||
782 | @@ -64,6 +66,9 @@ P_SYS_VENDOR = "sys/class/dmi/id/sys_vendor" | |||
783 | 64 | P_SEED_DIR = "var/lib/cloud/seed" | 66 | P_SEED_DIR = "var/lib/cloud/seed" |
784 | 65 | P_DSID_CFG = "etc/cloud/ds-identify.cfg" | 67 | P_DSID_CFG = "etc/cloud/ds-identify.cfg" |
785 | 66 | 68 | ||
786 | 69 | IBM_PROVISIONING_CHECK_PATH = "/root/provisioningConfiguration.cfg" | ||
787 | 70 | IBM_CONFIG_UUID = "9796-932E" | ||
788 | 71 | |||
789 | 67 | MOCK_VIRT_IS_KVM = {'name': 'detect_virt', 'RET': 'kvm', 'ret': 0} | 72 | MOCK_VIRT_IS_KVM = {'name': 'detect_virt', 'RET': 'kvm', 'ret': 0} |
790 | 68 | MOCK_VIRT_IS_VMWARE = {'name': 'detect_virt', 'RET': 'vmware', 'ret': 0} | 73 | MOCK_VIRT_IS_VMWARE = {'name': 'detect_virt', 'RET': 'vmware', 'ret': 0} |
791 | 69 | MOCK_VIRT_IS_XEN = {'name': 'detect_virt', 'RET': 'xen', 'ret': 0} | 74 | MOCK_VIRT_IS_XEN = {'name': 'detect_virt', 'RET': 'xen', 'ret': 0} |
792 | @@ -239,6 +244,57 @@ class TestDsIdentify(CiTestCase): | |||
793 | 239 | self._test_ds_found('ConfigDriveUpper') | 244 | self._test_ds_found('ConfigDriveUpper') |
794 | 240 | return | 245 | return |
795 | 241 | 246 | ||
796 | 247 | def test_ibmcloud_template_userdata_in_provisioning(self): | ||
797 | 248 | """Template provisioned with user-data during provisioning stage. | ||
798 | 249 | |||
799 | 250 | Template provisioning with user-data has METADATA disk, | ||
800 | 251 | datasource should return not found.""" | ||
801 | 252 | data = copy.deepcopy(VALID_CFG['IBMCloud-metadata']) | ||
802 | 253 | data['files'] = {IBM_PROVISIONING_CHECK_PATH: 'xxx'} | ||
803 | 254 | return self._check_via_dict(data, RC_NOT_FOUND) | ||
804 | 255 | |||
805 | 256 | def test_ibmcloud_template_userdata(self): | ||
806 | 257 | """Template provisioned with user-data first boot. | ||
807 | 258 | |||
808 | 259 | Template provisioning with user-data has METADATA disk. | ||
809 | 260 | datasource should return found.""" | ||
810 | 261 | self._test_ds_found('IBMCloud-metadata') | ||
811 | 262 | |||
812 | 263 | def test_ibmcloud_template_no_userdata_in_provisioning(self): | ||
813 | 264 | """Template provisioned with no user-data during provisioning. | ||
814 | 265 | |||
815 | 266 | no disks attached. Datasource should return not found.""" | ||
816 | 267 | data = copy.deepcopy(VALID_CFG['IBMCloud-nodisks']) | ||
817 | 268 | data['files'] = {IBM_PROVISIONING_CHECK_PATH: 'xxx'} | ||
818 | 269 | return self._check_via_dict(data, RC_NOT_FOUND) | ||
819 | 270 | |||
820 | 271 | def test_ibmcloud_template_no_userdata(self): | ||
821 | 272 | """Template provisioned with no user-data first boot. | ||
822 | 273 | |||
823 | 274 | no disks attached. Datasource should return found.""" | ||
824 | 275 | self._check_via_dict(VALID_CFG['IBMCloud-nodisks'], RC_NOT_FOUND) | ||
825 | 276 | |||
826 | 277 | def test_ibmcloud_os_code(self): | ||
827 | 278 | """Launched by os code always has config-2 disk.""" | ||
828 | 279 | self._test_ds_found('IBMCloud-config-2') | ||
829 | 280 | |||
830 | 281 | def test_ibmcloud_os_code_different_uuid(self): | ||
831 | 282 | """IBM cloud config-2 disks must be explicit match on UUID. | ||
832 | 283 | |||
833 | 284 | If the UUID is not 9796-932E then we actually expect ConfigDrive.""" | ||
834 | 285 | data = copy.deepcopy(VALID_CFG['IBMCloud-config-2']) | ||
835 | 286 | offset = None | ||
836 | 287 | for m, d in enumerate(data['mocks']): | ||
837 | 288 | if d.get('name') == "blkid": | ||
838 | 289 | offset = m | ||
839 | 290 | break | ||
840 | 291 | if not offset: | ||
841 | 292 | raise ValueError("Expected to find 'blkid' mock, but did not.") | ||
842 | 293 | data['mocks'][offset]['out'] = d['out'].replace(dsibm.IBM_CONFIG_UUID, | ||
843 | 294 | "DEAD-BEEF") | ||
844 | 295 | self._check_via_dict( | ||
845 | 296 | data, rc=RC_FOUND, dslist=['ConfigDrive', DS_NONE]) | ||
846 | 297 | |||
847 | 242 | def test_policy_disabled(self): | 298 | def test_policy_disabled(self): |
848 | 243 | """A Builtin policy of 'disabled' should return not found. | 299 | """A Builtin policy of 'disabled' should return not found. |
849 | 244 | 300 | ||
850 | @@ -452,7 +508,7 @@ VALID_CFG = { | |||
851 | 452 | }, | 508 | }, |
852 | 453 | 'Ec2-xen': { | 509 | 'Ec2-xen': { |
853 | 454 | 'ds': 'Ec2', | 510 | 'ds': 'Ec2', |
855 | 455 | 'mocks': [{'name': 'detect_virt', 'RET': 'xen', 'ret': 0}], | 511 | 'mocks': [MOCK_VIRT_IS_XEN], |
856 | 456 | 'files': { | 512 | 'files': { |
857 | 457 | 'sys/hypervisor/uuid': 'ec2c6e2f-5fac-4fc7-9c82-74127ec14bbb\n' | 513 | 'sys/hypervisor/uuid': 'ec2c6e2f-5fac-4fc7-9c82-74127ec14bbb\n' |
858 | 458 | }, | 514 | }, |
859 | @@ -579,6 +635,48 @@ VALID_CFG = { | |||
860 | 579 | 'ds': 'Hetzner', | 635 | 'ds': 'Hetzner', |
861 | 580 | 'files': {P_SYS_VENDOR: 'Hetzner\n'}, | 636 | 'files': {P_SYS_VENDOR: 'Hetzner\n'}, |
862 | 581 | }, | 637 | }, |
863 | 638 | 'IBMCloud-metadata': { | ||
864 | 639 | 'ds': 'IBMCloud', | ||
865 | 640 | 'mocks': [ | ||
866 | 641 | MOCK_VIRT_IS_XEN, | ||
867 | 642 | {'name': 'blkid', 'ret': 0, | ||
868 | 643 | 'out': blkid_out( | ||
869 | 644 | [{'DEVNAME': 'xvda1', 'TYPE': 'vfat', 'PARTUUID': uuid4()}, | ||
870 | 645 | {'DEVNAME': 'xvda2', 'TYPE': 'ext4', | ||
871 | 646 | 'LABEL': 'cloudimg-rootfs', 'PARTUUID': uuid4()}, | ||
872 | 647 | {'DEVNAME': 'xvdb', 'TYPE': 'vfat', 'LABEL': 'METADATA'}]), | ||
873 | 648 | }, | ||
874 | 649 | ], | ||
875 | 650 | }, | ||
876 | 651 | 'IBMCloud-config-2': { | ||
877 | 652 | 'ds': 'IBMCloud', | ||
878 | 653 | 'mocks': [ | ||
879 | 654 | MOCK_VIRT_IS_XEN, | ||
880 | 655 | {'name': 'blkid', 'ret': 0, | ||
881 | 656 | 'out': blkid_out( | ||
882 | 657 | [{'DEVNAME': 'xvda1', 'TYPE': 'ext3', 'PARTUUID': uuid4(), | ||
883 | 658 | 'UUID': uuid4(), 'LABEL': 'cloudimg-bootfs'}, | ||
884 | 659 | {'DEVNAME': 'xvdb', 'TYPE': 'vfat', 'LABEL': 'config-2', | ||
885 | 660 | 'UUID': dsibm.IBM_CONFIG_UUID}, | ||
886 | 661 | {'DEVNAME': 'xvda2', 'TYPE': 'ext4', | ||
887 | 662 | 'LABEL': 'cloudimg-rootfs', 'PARTUUID': uuid4(), | ||
888 | 663 | 'UUID': uuid4()}, | ||
889 | 664 | ]), | ||
890 | 665 | }, | ||
891 | 666 | ], | ||
892 | 667 | }, | ||
893 | 668 | 'IBMCloud-nodisks': { | ||
894 | 669 | 'ds': 'IBMCloud', | ||
895 | 670 | 'mocks': [ | ||
896 | 671 | MOCK_VIRT_IS_XEN, | ||
897 | 672 | {'name': 'blkid', 'ret': 0, | ||
898 | 673 | 'out': blkid_out( | ||
899 | 674 | [{'DEVNAME': 'xvda1', 'TYPE': 'vfat', 'PARTUUID': uuid4()}, | ||
900 | 675 | {'DEVNAME': 'xvda2', 'TYPE': 'ext4', | ||
901 | 676 | 'LABEL': 'cloudimg-rootfs', 'PARTUUID': uuid4()}]), | ||
902 | 677 | }, | ||
903 | 678 | ], | ||
904 | 679 | }, | ||
905 | 582 | } | 680 | } |
906 | 583 | 681 | ||
907 | 584 | # vi: ts=4 expandtab | 682 | # vi: ts=4 expandtab |
908 | diff --git a/tools/ds-identify b/tools/ds-identify | |||
909 | index e2552c8..9a2db5c 100755 | |||
910 | --- a/tools/ds-identify | |||
911 | +++ b/tools/ds-identify | |||
912 | @@ -92,6 +92,7 @@ DI_DMI_SYS_VENDOR="" | |||
913 | 92 | DI_DMI_PRODUCT_SERIAL="" | 92 | DI_DMI_PRODUCT_SERIAL="" |
914 | 93 | DI_DMI_PRODUCT_UUID="" | 93 | DI_DMI_PRODUCT_UUID="" |
915 | 94 | DI_FS_LABELS="" | 94 | DI_FS_LABELS="" |
916 | 95 | DI_FS_UUIDS="" | ||
917 | 95 | DI_ISO9660_DEVS="" | 96 | DI_ISO9660_DEVS="" |
918 | 96 | DI_KERNEL_CMDLINE="" | 97 | DI_KERNEL_CMDLINE="" |
919 | 97 | DI_VIRT="" | 98 | DI_VIRT="" |
920 | @@ -114,7 +115,7 @@ DI_DSNAME="" | |||
921 | 114 | # be searched if there is no setting found in config. | 115 | # be searched if there is no setting found in config. |
922 | 115 | DI_DSLIST_DEFAULT="MAAS ConfigDrive NoCloud AltCloud Azure Bigstep \ | 116 | DI_DSLIST_DEFAULT="MAAS ConfigDrive NoCloud AltCloud Azure Bigstep \ |
923 | 116 | CloudSigma CloudStack DigitalOcean AliYun Ec2 GCE OpenNebula OpenStack \ | 117 | CloudSigma CloudStack DigitalOcean AliYun Ec2 GCE OpenNebula OpenStack \ |
925 | 117 | OVF SmartOS Scaleway Hetzner" | 118 | OVF SmartOS Scaleway Hetzner IBMCloud" |
926 | 118 | DI_DSLIST="" | 119 | DI_DSLIST="" |
927 | 119 | DI_MODE="" | 120 | DI_MODE="" |
928 | 120 | DI_ON_FOUND="" | 121 | DI_ON_FOUND="" |
929 | @@ -123,6 +124,8 @@ DI_ON_NOTFOUND="" | |||
930 | 123 | 124 | ||
931 | 124 | DI_EC2_STRICT_ID_DEFAULT="true" | 125 | DI_EC2_STRICT_ID_DEFAULT="true" |
932 | 125 | 126 | ||
933 | 127 | _IS_IBM_CLOUD="" | ||
934 | 128 | |||
935 | 126 | error() { | 129 | error() { |
936 | 127 | set -- "ERROR:" "$@"; | 130 | set -- "ERROR:" "$@"; |
937 | 128 | debug 0 "$@" | 131 | debug 0 "$@" |
938 | @@ -196,7 +199,7 @@ read_fs_info() { | |||
939 | 196 | return | 199 | return |
940 | 197 | fi | 200 | fi |
941 | 198 | local oifs="$IFS" line="" delim="," | 201 | local oifs="$IFS" line="" delim="," |
943 | 199 | local ret=0 out="" labels="" dev="" label="" ftype="" isodevs="" | 202 | local ret=0 out="" labels="" dev="" label="" ftype="" isodevs="" uuids="" |
944 | 200 | out=$(blkid -c /dev/null -o export) || { | 203 | out=$(blkid -c /dev/null -o export) || { |
945 | 201 | ret=$? | 204 | ret=$? |
946 | 202 | error "failed running [$ret]: blkid -c /dev/null -o export" | 205 | error "failed running [$ret]: blkid -c /dev/null -o export" |
947 | @@ -219,12 +222,14 @@ read_fs_info() { | |||
948 | 219 | LABEL=*) label="${line#LABEL=}"; | 222 | LABEL=*) label="${line#LABEL=}"; |
949 | 220 | labels="${labels}${line#LABEL=}${delim}";; | 223 | labels="${labels}${line#LABEL=}${delim}";; |
950 | 221 | TYPE=*) ftype=${line#TYPE=};; | 224 | TYPE=*) ftype=${line#TYPE=};; |
951 | 225 | UUID=*) uuids="${uuids}${line#UUID=}$delim";; | ||
952 | 222 | esac | 226 | esac |
953 | 223 | done | 227 | done |
954 | 224 | [ -n "$dev" -a "$ftype" = "iso9660" ] && | 228 | [ -n "$dev" -a "$ftype" = "iso9660" ] && |
955 | 225 | isodevs="${isodevs} ${dev}=$label" | 229 | isodevs="${isodevs} ${dev}=$label" |
956 | 226 | 230 | ||
957 | 227 | DI_FS_LABELS="${labels%${delim}}" | 231 | DI_FS_LABELS="${labels%${delim}}" |
958 | 232 | DI_FS_UUIDS="${uuids%${delim}}" | ||
959 | 228 | DI_ISO9660_DEVS="${isodevs# }" | 233 | DI_ISO9660_DEVS="${isodevs# }" |
960 | 229 | } | 234 | } |
961 | 230 | 235 | ||
962 | @@ -437,14 +442,25 @@ dmi_sys_vendor_is() { | |||
963 | 437 | [ "${DI_DMI_SYS_VENDOR}" = "$1" ] | 442 | [ "${DI_DMI_SYS_VENDOR}" = "$1" ] |
964 | 438 | } | 443 | } |
965 | 439 | 444 | ||
970 | 440 | has_fs_with_label() { | 445 | has_fs_with_uuid() { |
971 | 441 | local label="$1" | 446 | case ",${DI_FS_UUIDS}," in |
972 | 442 | case ",${DI_FS_LABELS}," in | 447 | *,$1,*) return 0;; |
969 | 443 | *,$label,*) return 0;; | ||
973 | 444 | esac | 448 | esac |
974 | 445 | return 1 | 449 | return 1 |
975 | 446 | } | 450 | } |
976 | 447 | 451 | ||
977 | 452 | has_fs_with_label() { | ||
978 | 453 | # has_fs_with_label(label1[ ,label2 ..]) | ||
979 | 454 | # return 0 if a there is a filesystem that matches any of the labels. | ||
980 | 455 | local label="" | ||
981 | 456 | for label in "$@"; do | ||
982 | 457 | case ",${DI_FS_LABELS}," in | ||
983 | 458 | *,$label,*) return 0;; | ||
984 | 459 | esac | ||
985 | 460 | done | ||
986 | 461 | return 1 | ||
987 | 462 | } | ||
988 | 463 | |||
989 | 448 | nocase_equal() { | 464 | nocase_equal() { |
990 | 449 | # nocase_equal(a, b) | 465 | # nocase_equal(a, b) |
991 | 450 | # return 0 if case insenstive comparision a.lower() == b.lower() | 466 | # return 0 if case insenstive comparision a.lower() == b.lower() |
992 | @@ -583,6 +599,8 @@ dscheck_NoCloud() { | |||
993 | 583 | case " ${DI_DMI_PRODUCT_SERIAL} " in | 599 | case " ${DI_DMI_PRODUCT_SERIAL} " in |
994 | 584 | *\ ds=nocloud*) return ${DS_FOUND};; | 600 | *\ ds=nocloud*) return ${DS_FOUND};; |
995 | 585 | esac | 601 | esac |
996 | 602 | |||
997 | 603 | is_ibm_cloud && return ${DS_NOT_FOUND} | ||
998 | 586 | for d in nocloud nocloud-net; do | 604 | for d in nocloud nocloud-net; do |
999 | 587 | check_seed_dir "$d" meta-data user-data && return ${DS_FOUND} | 605 | check_seed_dir "$d" meta-data user-data && return ${DS_FOUND} |
1000 | 588 | check_writable_seed_dir "$d" meta-data user-data && return ${DS_FOUND} | 606 | check_writable_seed_dir "$d" meta-data user-data && return ${DS_FOUND} |
1001 | @@ -594,9 +612,8 @@ dscheck_NoCloud() { | |||
1002 | 594 | } | 612 | } |
1003 | 595 | 613 | ||
1004 | 596 | check_configdrive_v2() { | 614 | check_configdrive_v2() { |
1008 | 597 | if has_fs_with_label "config-2"; then | 615 | is_ibm_cloud && return ${DS_NOT_FOUND} |
1009 | 598 | return ${DS_FOUND} | 616 | if has_fs_with_label CONFIG-2 config-2; then |
1007 | 599 | elif has_fs_with_label "CONFIG-2"; then | ||
1010 | 600 | return ${DS_FOUND} | 617 | return ${DS_FOUND} |
1011 | 601 | fi | 618 | fi |
1012 | 602 | # look in /config-drive <vlc>/seed/config_drive for a directory | 619 | # look in /config-drive <vlc>/seed/config_drive for a directory |
1013 | @@ -988,6 +1005,36 @@ dscheck_Hetzner() { | |||
1014 | 988 | return ${DS_NOT_FOUND} | 1005 | return ${DS_NOT_FOUND} |
1015 | 989 | } | 1006 | } |
1016 | 990 | 1007 | ||
1017 | 1008 | is_ibm_provisioning() { | ||
1018 | 1009 | [ -f "${PATH_ROOT}/root/provisioningConfiguration.cfg" ] | ||
1019 | 1010 | } | ||
1020 | 1011 | |||
1021 | 1012 | is_ibm_cloud() { | ||
1022 | 1013 | cached "${_IS_IBM_CLOUD}" && return ${_IS_IBM_CLOUD} | ||
1023 | 1014 | local ret=1 | ||
1024 | 1015 | if [ "$DI_VIRT" = "xen" ]; then | ||
1025 | 1016 | if is_ibm_provisioning; then | ||
1026 | 1017 | ret=0 | ||
1027 | 1018 | elif has_fs_with_label METADATA metadata; then | ||
1028 | 1019 | ret=0 | ||
1029 | 1020 | elif has_fs_with_uuid 9796-932E && | ||
1030 | 1021 | has_fs_with_label CONFIG-2 config-2; then | ||
1031 | 1022 | ret=0 | ||
1032 | 1023 | fi | ||
1033 | 1024 | fi | ||
1034 | 1025 | _IS_IBM_CLOUD=$ret | ||
1035 | 1026 | return $ret | ||
1036 | 1027 | } | ||
1037 | 1028 | |||
1038 | 1029 | dscheck_IBMCloud() { | ||
1039 | 1030 | if is_ibm_provisioning; then | ||
1040 | 1031 | debug 1 "cloud-init disabled during provisioning on IBMCloud" | ||
1041 | 1032 | return ${DS_NOT_FOUND} | ||
1042 | 1033 | fi | ||
1043 | 1034 | is_ibm_cloud && return ${DS_FOUND} | ||
1044 | 1035 | return ${DS_NOT_FOUND} | ||
1045 | 1036 | } | ||
1046 | 1037 | |||
1047 | 991 | collect_info() { | 1038 | collect_info() { |
1048 | 992 | read_virt | 1039 | read_virt |
1049 | 993 | read_pid1_product_name | 1040 | read_pid1_product_name |
Addressed minor review comments to land this branch.