Commit 0d3e8749 authored by Łukasz Nowak's avatar Łukasz Nowak

software/kvm: Implement downloadable images

Downloadable images are enabled when key image-url-list (described in
instance-kvm-input-schema.json) is present.

Images are downloaded outside of partition processing, as this can take a lot
of time by template/image-download-controller.py

Configuration is checked and cleaned up by
template/image-download-config-creator.py

Promises are used for:

 * checking if the current configuration from the request has been processed
 * checking status of configuration generation, image download process and
   checksum validity

Details about errors are exposed by using monitor stack provided HTTP server,
so that user is able to take informed decision about how to fix the problem.
parent 413d8aef
......@@ -15,11 +15,11 @@
[template]
filename = instance.cfg.in
md5sum = b36b6b3ccb15758d99f3b6258b141db9
md5sum = e6d5c7bb627b4f1d3e7c99721b7c58fe
[template-kvm]
filename = instance-kvm.cfg.jinja2
md5sum = fb4d76a83ff7c2d0f205d52fb314a9ee
md5sum = aa734942801ac62eeccaba43926a0a27
[template-kvm-cluster]
filename = instance-kvm-cluster.cfg.jinja2.in
......@@ -55,7 +55,7 @@ md5sum = b7e87479a289f472b634a046b44b5257
[template-kvm-run]
filename = template/template-kvm-run.in
md5sum = c319ca536b6bac5425245fae1684ca49
md5sum = dd1f581f34cf5a0b627576771347c710
[template-kvm-controller]
filename = template/kvm-controller-run.in
......@@ -80,3 +80,11 @@ md5sum = 599dbbbd438fe7801e3f8642ae9e9a78
[template-httpd]
filename = instance-kvm-http.cfg.in
md5sum = d657884d02105deffddee0edae4b50a6
[image-download-controller]
_update_hash_filename_ = template/image-download-controller.py
md5sum = 7e4b54f8172c364bd12d28b07f8b1600
[image-download-config-creator]
_update_hash_filename_ = template/image-download-config-creator.py
md5sum = 7f6cd75096695922fddbba1c9292cef5
......@@ -367,6 +367,12 @@
"type": "string",
"format": "uri",
"default": "http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg"
},
"image-url-list": {
"title": "List of URLs images to download",
"description": "The list shall be list of direct URLs to images, followed by hash (#), then by image MD5SUM. Each image shall appear on newline, like: \"https://example.com/image.iso#06226c7fac5bacfa385872a19bb99684<newline>https://example.com/another-image.iso#31b40d58b18e038498ddb46caea1361c\". They will be provided in KVM image list according to the order on the list. After updating the list, the instance has to be restarted to refresh it. Amount of images is limited to 4, and one image can be maximum 5G. Image will be downloaded and checked against its MD5SUM 4 times, then it will be considered as impossible to download with given MD5SUM. Each image has to be downloaded in time shorter than 4 hours, so in case of very slow images to access, it can take up to 16 hours to download all of them. Note: The instance has to be restarted in order to update the list of available images in the VM.",
"type": "string",
"textarea": "true"
}
}
}
......@@ -9,6 +9,7 @@
{% set instance_type = slapparameter_dict.get('type', 'standalone') -%}
{% set nat_rule_list = slapparameter_dict.get('nat-rules', '22 80 443') -%}
{% set disk_device_path = slapparameter_dict.get('disk-device-path', None) -%}
{% set image_url_list_enabled = 'image-url-list' in slapparameter_dict %}
{% set extends_list = [] -%}
{% set part_list = [] -%}
......@@ -51,6 +52,11 @@ public = ${:srv}/public/
cron-entries = ${:etc}/cron.d
crontabs = ${:etc}/crontabs
cronstamps = ${:etc}/cronstamps
{%- if image_url_list_enabled %}
image-repository = ${:srv}/image-repository
image-url-list-var = ${:var}/image-url-list
image-url-list-expose = ${monitor-directory:private}/image-url-list
{%- endif %}
[create-mac]
recipe = slapos.cookbook:generate.mac
......@@ -65,6 +71,108 @@ recipe = slapos.cookbook:generate.password
storage-path = ${directory:srv}/passwd
bytes = 8
{% if image_url_list_enabled %}
## image-url-list support BEGIN
[empty-file-state-base-promise]
<= monitor-promise-base
module = check_file_state
name = ${:_buildout_section_name_}.py
config-state = empty
# It's very hard to put the username and password correctly, after schema://
# and before the host, as it's not the way how one can use monitor provided
# information, so just show the information in the URL
config-url = ${monitor-base:base-url}/private/image-url-list/${:filename} with username ${monitor-publish-parameters:monitor-user} and password ${monitor-publish-parameters:monitor-password}
[image-url-list-source-config]
# generates configuration of the image from the user parameter
# special "magic" is used, to properly support multiline image-url-list
# but in the same time correctly generate the configuration file
recipe = slapos.recipe.template:jinja2
template = inline:
{#- Do special trick to support image-url-list being None, if key is present with value "" #}
{%- raw %}
{{ slap_parameter.get('image-url-list') or '' }}
{% endraw -%}
context =
section slap_parameter slap-parameter
rendered = ${directory:etc}/image-url-list.conf
[image-url-list-processed-config]
# compares if the current configuration has been used by
# the image-url-list-download, if not, exposes it as not empty file with
# information
recipe = slapos.recipe.build
install =
import os
import hashlib
if not os.path.exists(location):
os.mkdir(location)
with open('${:state-file}', 'w') as state_handler:
try:
with open('${:config-file}', 'rb') as config_handler, open('${:processed-md5sum}') as processed_handler:
config_md5sum = hashlib.md5(config_handler.read()).hexdigest()
processed_md5sum = processed_handler.read()
if config_md5sum == processed_md5sum:
state_handler.write('')
else:
state_handler.write('config %s != processed %s' % (config_md5sum, processed_md5sum))
except Exception as e:
state_handler.write(str(e))
update = ${:install}
config-file = ${image-url-list-source-config:rendered}
state-filename = image-url-list-processed-config.state
state-file = ${directory:image-url-list-expose}/${:state-filename}
processed-md5sum = ${directory:image-url-list-var}/update-image-processed.md5sum
[image-url-list-processed-config-promise]
# promise to check if the configuration provided by the user has been already
# processed by the image-url-list-download script, which runs asynchronously
<= empty-file-state-base-promise
filename = ${image-url-list-processed-config:state-filename}
config-filename = ${image-url-list-processed-config:state-file}
[image-url-list-json-config]
# generates json configuration from user configuration
recipe = plone.recipe.command
command = {{ python_executable }} {{ image_download_config_creator }} ${image-url-list-source-config:rendered} ${:rendered} ${directory:image-repository} ${:error-state-file}
update-command = ${:command}
rendered = ${directory:image-url-list-var}/image-url-list.json
error-state-filename = image-url-list-json-config-error.txt
error-state-file = ${directory:image-url-list-expose}/${:error-state-filename}
[image-url-list-config-state-promise]
# promise to check if configuration has been parsed without errors
<= empty-file-state-base-promise
filename = ${image-url-list-json-config:error-state-filename}
config-filename = ${image-url-list-json-config:error-state-file}
[image-url-list-download-wrapper]
# wrapper to execute image-url-list-download on each run
recipe = slapos.cookbook:wrapper
wrapper-path = ${directory:scripts}/image-updater
command-line = {{ python_executable }} {{ image_download_controller }} ${image-url-list-json-config:rendered} {{ curl_executable_location }} ${:md5sum-state-file} ${:error-state-file} ${image-url-list-processed-config:processed-md5sum}
md5sum-state-filename = image-download-controller-md5sum-fail.json
md5sum-state-file = ${directory:image-url-list-expose}/${:md5sum-state-filename}
error-state-filename = image-download-controller-error.text
error-state-file = ${directory:image-url-list-expose}/${:error-state-filename}
hash-existing-files = ${buildout:directory}/software_release/buildout.cfg
[image-url-list-download-md5sum-promise]
# promise to report errors with problems with calculating md5sum of the
# downloaded images
<= empty-file-state-base-promise
filename = ${image-url-list-download-wrapper:md5sum-state-filename}
config-filename = ${image-url-list-download-wrapper:md5sum-state-file}
[image-url-list-download-state-promise]
# promise to report errors during download
<= empty-file-state-base-promise
filename = ${image-url-list-download-wrapper:error-state-filename}
config-filename = ${image-url-list-download-wrapper:error-state-file}
## image-url-list support END
{% endif %} {# if image_url_list_enabled #}
[kvm-controller-parameter-dict]
python-path = {{ python_eggs_executable }}
vnc-passwd = ${gen-passwd:passwd}
......@@ -85,6 +193,11 @@ vnc-ip = ${:ipv4}
vnc-port = 5901
default-cdrom-iso = {{ debian_amd64_netinst_location }}
{% if image_url_list_enabled %}
image-url-list-json-config = ${image-url-list-json-config:rendered}
{% else %}
image-url-list-json-config =
{% endif %}
nbd-host = ${slap-parameter:nbd-host}
nbd-port = ${slap-parameter:nbd-port}
nbd2-host = ${slap-parameter:nbd2-host}
......@@ -708,6 +821,7 @@ nbd-port = 1024
nbd-host =
nbd2-port = 1024
nbd2-host =
image-url-list =
enable-device-hotplug = False
ram-size = 1024
......@@ -759,6 +873,9 @@ keyboard-layout-language = fr
{% set key_list = v.split('\n') -%}
{{ k }} =
{{ key_list | join('\n ') }}
{% elif k == 'image-url-list' %}
{# needs to decorate possibly multiline or maybe unsafe value #}
{{ k }} = {{ dumps(v) }}
{% else -%}
{{ k }} = {{ v }}
{% endif -%}
......@@ -833,6 +950,13 @@ parts =
cron-service
cron-entry-logrotate
frontend-promise
{% if image_url_list_enabled %}
image-url-list-download-wrapper
image-url-list-config-state-promise
image-url-list-download-md5sum-promise
image-url-list-download-state-promise
image-url-list-processed-config-promise
{% endif %}
{% if additional_frontend %}
frontend-additional-promise
{% endif %}
......
......@@ -83,6 +83,8 @@ extra-context =
raw dcron_executable_location ${dcron:location}/sbin/crond
raw debian_amd64_netinst_location ${debian-amd64-netinst.iso:location}/${debian-amd64-netinst.iso:filename}
raw file_download_script ${file-download-script:location}/${file-download-script:filename}
raw image_download_controller ${image-download-controller:target}
raw image_download_config_creator ${image-download-config-creator:target}
raw logrotate_cfg ${template-logrotate-base:rendered}
raw novnc_location ${noVNC:location}
raw netcat_bin ${netcat:location}/bin/netcat
......
......@@ -190,6 +190,14 @@ context =
raw openssl_executable_location ${openssl:location}/bin/openssl
raw template_apache_conf ${template-apache-conf:location}/${template-apache-conf:filename}
[image-download-controller]
recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/${:_update_hash_filename_}
mode = 640
[image-download-config-creator]
<= image-download-controller
[versions]
websockify = 0.9.0
......
#!/usr/bin/env python
import hashlib
import json
import re
import sys
if __name__ == "__main__":
source_configuration, destination_configuration, \
destination_directory, error_state_file = sys.argv[1:]
md5sum_re = re.compile(r"^([a-fA-F\d]{32})$")
image_prefix = 'image_'
maximum_image_amount = 4
# build currently wanted list
configuration_dict = {
'destination-directory': destination_directory,
}
image_list = []
error_list = []
print('INF: Storing errors in %s' % (error_state_file,))
with open(source_configuration, 'rb') as fh:
image_number = 0
data = fh.read()
configuration_dict['config-md5sum'] = hashlib.md5(data).hexdigest()
for entry in data.decode('utf-8').split():
split_entry = entry.split('#')
if len(split_entry) != 2:
error_list.append('ERR: entry %r is incorrect' % (entry,))
continue
url, md5sum = split_entry
if not md5sum_re.match(md5sum):
error_list.append('ERR: checksum in entry %r is malformed' % (entry, ))
continue
if md5sum not in [q['md5sum'] for q in image_list]:
image_number += 1
image_list.append({
'md5sum': md5sum,
'url': url,
'destination': md5sum,
'destination-tmp': md5sum + '_tmp',
'link': 'image_%03i' % (image_number,),
})
else:
print('INF: checksum %s repeated, used url %s' % (url, ))
image_amount = len(image_list)
if image_amount > maximum_image_amount:
error_list.append(
'ERR: Amount of images is %s, which is bigger than maximum (%s)' % (
image_amount, maximum_image_amount))
else:
configuration_dict['image-list'] = image_list
error_amount = len(error_list)
configuration_dict['error-amount'] = error_amount
with open(destination_configuration, 'w') as fh:
json.dump(configuration_dict, fh, indent=2)
with open(error_state_file, 'w') as fh:
if error_amount == 0:
print('INF: Configuration generated without errors')
fh.write('')
else:
print('ERR: Configuration generated with %s errors' % (error_amount,))
fh.write('\n'.join(error_list))
sys.exit(error_amount)
#!/usr/bin/env python
import hashlib
import json
import os
import re
import subprocess
import sys
# stolen from download_file.in
def md5Checksum(file_path):
with open(file_path, 'rb') as fh:
m = hashlib.md5()
while True:
data = fh.read(8192)
if not data:
break
m.update(data)
return m.hexdigest()
# Note: Assuring only one running instance is not done, as this script is only
# run from supervisord, which does it already
if __name__ == "__main__":
configuration, curl, md5sum_fail_file, error_state_file, \
processed_md5sum = sys.argv[1:]
error_list = []
md5sum_re = re.compile(r"^([a-fA-F\d]{32})$")
image_prefix = 'image_'
# build currently wanted list
with open(configuration) as fh:
try:
config = json.load(fh)
except Exception as e:
print('ERR: Problem loading configuration: %s' % (e,))
sys.exit(1)
if config['error-amount'] != 0:
print('ERR: There are problems with configuration')
print('INF: Storing errors in %s' % (error_state_file,))
# clean the destination directory
file_to_keep_list = []
for image in config['image-list']:
file_to_keep_list.append(image['destination'])
file_to_keep_list.append(image['link'])
for fname in os.listdir(config['destination-directory']):
if fname not in file_to_keep_list:
print('INF: Removing obsolete %s' % (fname,))
os.remove(os.path.join(config['destination-directory'], fname))
# prepare state dicts
# current and new are used to remove not existing configurations
# and also to allow re-add some configuration
try:
with open(md5sum_fail_file) as fh:
md5sum_state_dict = json.load(fh)
except Exception:
md5sum_state_dict = {}
new_md5sum_state_dict = {}
# fetch the wanted list
for image in config['image-list']:
destination = os.path.join(
config['destination-directory'], image['destination'])
if os.path.exists(destination):
if md5Checksum(destination) == image['md5sum']:
print('INF: %s : already downloaded' % (image['url'],))
continue
else:
print('INF: %s : Removed, as expected checksum does not match %s' % (
image['url'], image['md5sum']))
os.remove(destination)
# key is str, as the dict is dumped to JSON which does not accept tuples
md5sum_state_key = '%s#%s' % (image['url'], image['md5sum'])
md5sum_state_amount = md5sum_state_dict.get(md5sum_state_key, 0)
if md5sum_state_amount >= 4:
new_md5sum_state_dict[md5sum_state_key] = md5sum_state_amount
error_list.append(
'ERR: %s : Checksum is incorrect after %s tries, will not retry' % (
image['url'], md5sum_state_amount))
continue
print('INF: %s : Downloading' % (image['url'],))
download_success = True
destination_tmp = os.path.join(
config['destination-directory'], image['destination-tmp'])
try:
subprocess.check_output([
curl,
'--insecure', # allow any download
'--location', # follow redirects
'--no-progress-meter', # do not tell too much
'--max-time', '14400', # maximum time for download is 4 hours
'--max-filesize', '5368709120', # maximum 5G for an image
'--output', destination_tmp, image['url']],
stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
error_list.append('ERR: %s : Problem while downloading: %r' % (
image['url'], e.output.strip()))
continue
if not(os.path.exists(destination_tmp)):
error_list.append('ERR: %s : Image disappeared, will retry later')
continue
computed_md5sum = md5Checksum(destination_tmp)
if computed_md5sum != image['md5sum']:
try:
os.remove(destination_tmp)
except Exception:
pass
error_list.append(
'ERR: %s : MD5 mismatch expected is %s but got instead %s' % (
image['url'], image['md5sum'], computed_md5sum))
# Store yet another failure while computing md5sum for this
new_md5sum_state_dict[md5sum_state_key] = md5sum_state_amount + 1
else:
os.rename(destination_tmp, destination)
print('INF: %s : Stored with checksum %s' % (
image['url'], image['md5sum']))
for image in config['image-list']:
destination = os.path.join(
config['destination-directory'], image['destination'])
link = os.path.join(config['destination-directory'], image['link'])
if os.path.exists(destination):
if os.path.lexists(link):
if not os.path.islink(link):
os.remove(link)
if os.path.islink(link) and os.readlink(link) != destination:
os.remove(link)
if not os.path.lexists(link):
print('INF: %s : Symlinking %s -> %s' % (
image['url'], link, destination))
os.symlink(destination, link)
with open(md5sum_fail_file, 'w') as fh:
if new_md5sum_state_dict != {}:
json.dump(new_md5sum_state_dict, fh, indent=2)
else:
# if no problems reported, just empty the file
fh.write('')
with open(error_state_file, 'w') as fh:
fh.write('\n'.join(error_list))
with open(processed_md5sum, 'w') as fh:
fh.write(config['config-md5sum'])
sys.exit(len(error_list))
......@@ -15,6 +15,7 @@ import shutil
from random import shuffle
import glob
import re
import json
import ssl
......@@ -98,6 +99,8 @@ enable_device_hotplug = '{{ parameter_dict.get("enable-device-hotplug") }}'.lowe
logfile = '{{ parameter_dict.get("log-file") }}'
image_url_list_json_config = '{{ parameter_dict.get("image-url-list-json-config") }}'
if hasattr(ssl, '_create_unverified_context') and url_check_certificate == 'false':
opener = FancyURLopener(context=ssl._create_unverified_context())
else:
......@@ -366,11 +369,27 @@ for nbd_ip, nbd_port in nbd_list:
kvm_argument_list.extend([
'-drive',
'file=nbd:[%s]:%s,media=cdrom' % (nbd_ip, nbd_port)])
# If no NBD is specified/available: use internal disk image
else:
kvm_argument_list.extend([
'-drive', 'file=%s,media=cdrom' % default_cdrom_iso
])
image_url_list_used = False
if image_url_list_json_config:
# Support image-url-list
with open(image_url_list_json_config) as fh:
image_config = json.load(fh)
if image_config['error-amount'] == 0:
for image in sorted(image_config['image-list'], key=lambda k: k['link']):
link = os.path.join(image_config['destination-directory'], image['link'])
if os.path.exists(link) and os.path.islink(link):
image_url_list_used = True
kvm_argument_list.extend([
'-drive',
'file=%s,media=cdrom' % (link,)
])
if not image_url_list_used:
# If no NBD is specified/available not downloadable image: use internal disk image
kvm_argument_list.extend([
'-drive', 'file=%s,media=cdrom' % default_cdrom_iso
])
print('Starting KVM: \n %s' % ' '.join(kvm_argument_list))
os.execv(qemu_path, kvm_argument_list)
......@@ -28,15 +28,19 @@
import six.moves.http_client as httplib
import json
import os
import hashlib
import psutil
import requests
import six
import slapos.util
import sqlite3
from six.moves.urllib.parse import parse_qs, urlparse
import unittest
import subprocess
from slapos.recipe.librecipe import generateHashFromFiles
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
from slapos.slap.standalone import SlapOSNodeCommandError
has_kvm = os.access('/dev/kvm', os.R_OK | os.W_OK)
skipUnlessKvm = unittest.skipUnless(has_kvm, 'kvm not loaded or not allowed')
......@@ -479,3 +483,192 @@ class TestInstanceNbdServer(InstanceTestCase):
)
self.assertIn('<title>Upload new File</title>', result.text)
self.assertIn("WARNING", connection_parameter_dict['status_message'])
@skipUnlessKvm
class TestImageUrlList(InstanceTestCase):
__partition_reference__ = 'iul'
@classmethod
def getInstanceSoftwareType(cls):
return 'default'
@classmethod
def getInstanceParameterDict(cls):
# start with empty, but working configuration
return {}
def tearDown(self):
# clean up the instance for other tests
# 1st remove all images...
self.rerequestInstance({'image-url-list': ''})
self.slap.waitForInstance(max_retry=10)
# 2nd ...move instance to "default" state
self.rerequestInstance({})
self.slap.waitForInstance(max_retry=10)
def rerequestInstance(self, parameter_dict, state='started'):
software_url = self.getSoftwareURL()
software_type = self.getInstanceSoftwareType()
return self.slap.request(
software_release=software_url,
software_type=software_type,
partition_reference=self.default_partition_reference,
partition_parameter_kw=parameter_dict,
state=state)
fake_image, = (
"https://shacache.nxdcdn.com/shacache/05105cd25d1ad798b71fd46a206c9b73d"
"a2c285a078af33d0e739525a595886785725a68811578bc21f75d0a97700a66d5e75bc"
"e5b2721ca4556a0734cb13e65",)
fake_image_md5sum = "c98825aa1b6c8087914d2bfcafec3058"
fake_image2, = (
"https://shacache.nxdcdn.com/shacache/54f8a83a32bbf52602d9d211d592ee705"
"99f0c6b6aafe99e44aeadb0c8d3036a0e673aa994ffdb28d9fb0de155720123f74d814"
"2a74b7675a8d8ca20476dba6e",)
fake_image2_md5sum = "d4316a4d05f527d987b9d6e43e4c2bc6"
fake_image_wrong_md5sum = "c98825aa1b6c8087914d2bfcafec3057"
def raising_waitForInstance(self, max_retry):
with self.assertRaises(SlapOSNodeCommandError):
self.slap.waitForInstance(max_retry=max_retry)
def test(self):
partition_parameter_kw = {
'image-url-list': "%s#%s\n%s#%s" % (
self.fake_image, self.fake_image_md5sum, self.fake_image2,
self.fake_image2_md5sum)
}
self.rerequestInstance(partition_parameter_kw)
self.slap.waitForInstance(max_retry=10)
# check that image is correctly downloaded and linked
image_repository = os.path.join(
self.computer_partition_root_path, 'srv', 'image-repository')
image = os.path.join(image_repository, self.fake_image_md5sum)
image_link = os.path.join(image_repository, 'image_001')
self.assertTrue(os.path.exists(image))
with open(image, 'rb') as fh:
image_md5sum = hashlib.md5(fh.read()).hexdigest()
self.assertEqual(image_md5sum, self.fake_image_md5sum)
self.assertTrue(os.path.islink(image_link))
self.assertEqual(os.readlink(image_link), image)
image2 = os.path.join(image_repository, self.fake_image2_md5sum)
image2_link = os.path.join(image_repository, 'image_002')
self.assertTrue(os.path.exists(image2))
with open(image2, 'rb') as fh:
image2_md5sum = hashlib.md5(fh.read()).hexdigest()
self.assertEqual(image2_md5sum, self.fake_image2_md5sum)
self.assertTrue(os.path.islink(image2_link))
self.assertEqual(os.readlink(image2_link), image2)
# check that the image is NOT YET available in kvm
with self.slap.instance_supervisor_rpc as instance_supervisor:
kvm_pid = [q for q in instance_supervisor.getAllProcessInfo()
if 'kvm-' in q['name']][0]['pid']
kvm_process = psutil.Process(kvm_pid)
cmd_line = ''.join(kvm_process.cmdline())
self.assertNotIn(
'srv/image-repository/image_001,media=cdrom',
cmd_line
)
self.assertNotIn(
'srv/image-repository/image_002,media=cdrom',
cmd_line
)
# mimic the requirement: restart the instance by requesting it stopped and
# then started started, like user have to do it
self.rerequestInstance(partition_parameter_kw, state='stopped')
self.slap.waitForInstance(max_retry=1)
self.rerequestInstance(partition_parameter_kw, state='started')
self.slap.waitForInstance(max_retry=1)
# now the image is available in the kvm
with self.slap.instance_supervisor_rpc as instance_supervisor:
kvm_pid = [q for q in instance_supervisor.getAllProcessInfo()
if 'kvm-' in q['name']][0]['pid']
kvm_process = psutil.Process(kvm_pid)
cmd_line = ''.join(kvm_process.cmdline())
self.assertIn(
'srv/image-repository/image_001,media=cdrom',
cmd_line
)
self.assertIn(
'srv/image-repository/image_002,media=cdrom',
cmd_line
)
# cleanup of images works, also asserts that configuration changes are
# reflected
self.rerequestInstance({'image-url-list': ''})
self.slap.waitForInstance(max_retry=2)
self.assertEqual(
os.listdir(image_repository),
[]
)
def assertPromiseFails(self, promise):
monitor_run_promise = os.path.join(
self.computer_partition_root_path, 'software_release', 'bin',
'monitor.runpromise'
)
monitor_configuration = os.path.join(
self.computer_partition_root_path, 'etc', 'monitor.conf')
self.assertNotEqual(
0,
subprocess.call([
monitor_run_promise, '-c', monitor_configuration, '-a', '-f',
'--run-only', promise])
)
def test_bad_parameter(self):
self.rerequestInstance({
'image-url-list': "jsutbad"
})
self.raising_waitForInstance(3)
self.assertPromiseFails('image-url-list-config-state-promise.py')
def test_incorrect_md5sum(self):
self.rerequestInstance({
'image-url-list': "%s#" % (self.fake_image,)
})
self.raising_waitForInstance(3)
self.assertPromiseFails('image-url-list-config-state-promise.py')
self.rerequestInstance({
'image-url-list': "url#asdasd"
})
self.raising_waitForInstance(3)
self.assertPromiseFails('image-url-list-config-state-promise.py')
def test_not_matching_md5sum(self):
self.rerequestInstance({
'image-url-list': "%s#%s" % (
self.fake_image, self.fake_image_wrong_md5sum)
})
self.raising_waitForInstance(3)
self.assertPromiseFails('image-url-list-download-md5sum-promise.py')
self.assertPromiseFails('image-url-list-download-state-promise.py')
def test_unreachable_host(self):
self.rerequestInstance({
'image-url-list': "evennotahost#%s" % (
self.fake_image_md5sum,)
})
self.raising_waitForInstance(3)
self.assertPromiseFails('image-url-list-download-state-promise.py')
def test_too_many_images(self):
self.rerequestInstance({
'image-url-list': """
image1#11111111111111111111111111111111
image2#22222222222222222222222222222222
image3#33333333333333333333333333333333
image4#44444444444444444444444444444444
image5#55555555555555555555555555555555
image6#66666666666666666666666666666666
"""
})
self.ra