tripleo-ansible/tripleo_ansible/ansible_plugins/modules/tripleo_nova_image_cache.py
Oliver Walsh 359ef43f94 Stop relying on future mtime in tripleo_nova_image_cache
The tripleo_nova_image_cache role was relying on a future mtime to convince
nova image cleanup logic to skip a specific image that we want to pre-cache.
This works well on nodes that are not currently hosting an instance of the
image, however if there is an instance using it then nova will reset the mtime
every 40 minutes (by default).

As a result it's not possible to provide fine grained control of the caching.
We can just ensure the image exists on the host with an mtime of now.
It should remain cache for at least remove_unused_original_minimum_age_seconds
from nova.conf (default 86400).

Change-Id: If836759020bcaeb0d3af47f2af7f98ff2150b606
2020-09-02 00:34:44 +01:00

222 lines
7.9 KiB
Python

#!/usr/bin/python
# Copyright 2019 Red Hat, Inc.
# All Rights Reserved.
#
# 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.
__metaclass__ = type
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.openstack import openstack_cloud_from_module
from ansible.module_utils.openstack import openstack_full_argument_spec
from ansible.module_utils.openstack import openstack_module_kwargs
import datetime
import hashlib
import os
import tempfile
import time
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: tripleo_nova_image_cache
short_description: Manage Nova image cache on TripleO OpenStack deployment
version_added: "2.0"
author: "Oliver Walsh (@owalsh)"
description:
- Manage Nova image cache on TripleO OpenStack deployment
options:
id:
description:
- ID of the image to cache
required: true
scp_source:
description:
- Attempt to scp the image from this nova-compute host
scp_continue_on_error:
description:
- Fallback to image download if scp fails
default: false
requirements: ["openstacksdk", "tripleo-common"]
'''
EXAMPLES = '''
- name: Cache image
tripleo_nova_image_cache:
id: ec151bd1-aab4-413c-b577-ced089e7d3f8
- name: Cache image, try to copy from existing host
tripleo_nova_image_cache:
id: ec151bd1-aab4-413c-b577-ced089e7d3f8
scp_source: nova-compute-0
scp_continue_on_error: true
'''
def main():
argument_spec = openstack_full_argument_spec(
id=dict(required=True),
_cache_dir=dict(required=True),
_cache_file=dict(required=True),
_chunk_size=dict(default=64 * 1024, type='int'),
_prefetched_path=dict(default=None),
scp_continue_on_error=dict(default=False, type='bool')
)
module_kwargs = openstack_module_kwargs()
module = AnsibleModule(argument_spec, **module_kwargs)
image_id = module.params['id']
cache_dir = module.params['_cache_dir']
cache_file = module.params['_cache_file']
chunk_size = module.params['_chunk_size']
prefetched_path = module.params['_prefetched_path']
scp_continue = module.params['scp_continue_on_error']
result = dict(
changed=False,
actions=[],
image=None,
cache_file='',
exists_in_cache=False,
mtime=0
)
sdk, cloud = openstack_cloud_from_module(module, min_version='0.11.3')
try:
result['exists_in_cache'] = exists_in_cache = os.path.exists(
cache_file)
if exists_in_cache:
result['cache_file'] = cache_file
image = cloud.image.find_image(name_or_id=image_id)
exists_in_glance = image is not None
if exists_in_glance:
result['image'] = image.to_dict()
if not exists_in_cache:
if not exists_in_glance:
module.fail_json(
msg="Image not found in glance: %s" % image_id)
md5 = hashlib.md5()
if prefetched_path:
result['actions'].append({
'name': 'Verify pre-fetched image checksum'
})
with open(prefetched_path, 'rb') as prefetched_image_file:
while True:
chunk = prefetched_image_file.read(chunk_size)
if not chunk:
break
md5.update(chunk)
prefetched_checksum = md5.hexdigest()
if prefetched_checksum == image.checksum:
result['actions'].append({
'name': 'Verify pre-fetched image',
'result': True,
'expected_md5': image.checksum,
'actual_md5': prefetched_checksum
})
# FIXME: chown to the container nova uid (42436)
# until we can run within the container
os.chown(prefetched_path, 42436, 42436)
os.rename(prefetched_path, cache_file)
result['changed'] = True
else:
result['actions'].append({
'name': 'Verify pre-fetched image',
'result': False,
'expected_md5': image.checksum,
'actual_md5': prefetched_checksum
})
if not scp_continue:
module.fail_json(
msg="Pre-fetched image checksum failed")
# Ignore it and download direct from glance.
# As we did not create it we should not remove it.
prefetched_path = ''
if not prefetched_path:
with tempfile.NamedTemporaryFile(
'wb',
dir=cache_dir,
delete=False) as temp_cache_file:
try:
md5 = hashlib.md5()
image_stream = cloud.image.download_image(
image,
stream=True
)
try:
for chunk in image_stream.iter_content(
chunk_size=chunk_size):
md5.update(chunk)
temp_cache_file.write(chunk)
finally:
image_stream.close()
temp_cache_file.close()
download_checksum = md5.hexdigest()
if download_checksum != image.checksum:
result['actions'].append({
'name': 'Verify downloaded image',
'result': False,
'expected_md5': image.checksum,
'actual_md5': download_checksum
})
module.fail_json(
msg="Image data does not match checksum")
result['actions'].append({
'name': 'Verify downloaded image',
'result': True,
'expected_md5': image.checksum,
'actual_md5': download_checksum
})
# FIXME: chown to the container nova uid (42436)
# until we can run within the container
os.chown(temp_cache_file.name, 42436, 42436)
os.rename(temp_cache_file.name, cache_file)
result['changed'] = True
finally:
try:
os.unlink(temp_cache_file.name)
except Exception:
pass
# Always set the mtime to now but don't report this as a change
# as this is constantly refreshed by nova (every 40mins by default)
# while an instance on the host is using the image
now = time.time()
os.utime(cache_file, (now, now))
result['mtime'] = now
module.exit_json(**result)
except sdk.exceptions.OpenStackCloudException as e:
module.fail_json(msg=str(e), extra_data=e.extra_data)
if __name__ == "__main__":
main()