Merge pull request #1 from openstack-charmers/glance_retrofitter

Implement charm
This commit is contained in:
Alex Kavanagh 2019-06-20 13:49:05 +01:00 committed by GitHub
commit cbb7a4e06b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 790 additions and 7 deletions

17
src/actions.yaml Normal file
View File

@ -0,0 +1,17 @@
retrofit-image:
description: |
Trigger image retrofitting process
params:
source-image:
type: string
default: ''
description: |
Optionally specify ID of image in Glance to use as source for the
retrofitting. The default is to automatically select the most
recent Ubuntu Server or Minimal daily Cloud image.
force:
type: boolean
default: False
description: |
Force re-retrofitting image despite presence of apparently up to
date target image.

70
src/actions/actions.py Executable file
View File

@ -0,0 +1,70 @@
#!/usr/bin/env python3
# Copyright 2019 Canonical Ltd
#
# 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 os
import sys
# Load basic layer module from $CHARM_DIR/lib
sys.path.append('lib')
from charms.layer import basic
# setup module loading from charm venv
basic.bootstrap_charm_deps()
import charms.reactive as reactive
import charmhelpers.core as ch_core
import charms_openstack.bus
import charms_openstack.charm as charm
# load reactive interfaces
reactive.bus.discover()
# load Endpoint based interface data
ch_core.hookenv._run_atstart()
# load charm class
charms_openstack.bus.discover()
def retrofit_image(*args):
"""Trigger image retrofitting process."""
keystone_endpoint = reactive.endpoint_from_flag(
'identity-credentials.available')
with charm.provide_charm_instance() as instance:
instance.retrofit(
keystone_endpoint,
ch_core.hookenv.action_get('force'),
ch_core.hookenv.action_get('source-image'))
ACTIONS = {
'retrofit-image': retrofit_image,
}
def main(args):
action_name = os.path.basename(args[0])
try:
action = ACTIONS[action_name]
except KeyError:
return 'Action {} is undefined'.format(action_name)
try:
action(args)
except Exception as e:
ch_core.hookenv.action_fail(str(e))
if __name__ == '__main__':
sys.exit(main(sys.argv))

1
src/actions/retrofit-image Symbolic link
View File

@ -0,0 +1 @@
actions.py

7
src/config.yaml Normal file
View File

@ -0,0 +1,7 @@
options:
retrofit-uca-pocket:
type: string
default: 'rocky'
description: |
Name of Ubuntu Cloud Archive pocket to add to the image being
retrofitted.

View File

@ -1,8 +1,10 @@
includes:
- layer:openstack
- layer:snap
- layer:tls-client
- interface:juju-info
- interface:keystone-credentials
- interface:tls-certificates
options:
basic:
use_venv: True
@ -10,11 +12,11 @@ options:
packages: [ 'libffi-dev', 'libssl-dev' ]
snap:
octavia-diskimage-retrofit:
comment: |
Using devmode pending resolution of snapd fuse-support issue
https://github.com/openstack-charmers/octavia-diskimage-retrofit/issues/6
channel: edge
devmode: True
comment: |
Using devmode pending resolution of snapd fuse-support issue
https://github.com/openstack-charmers/octavia-diskimage-retrofit/issues/6
resources:
octavia-diskimage-retrofit:
type: file

View File

@ -0,0 +1,159 @@
# Copyright 2019 Canonical Ltd
#
# 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 subprocess
import glanceclient
import keystoneauth1.loading
import keystoneauth1.session
SYSTEM_CA_BUNDLE = '/etc/ssl/certs/ca-certificates.crt'
def session_from_identity_credentials(identity_credentials):
"""Get Keystone Session from ``identity-credentials`` relation.
:param identity_credentials: reactive Endpoint
:type identity_credentials: RelationBase
:returns: Keystone session
:rtype: keystoneauth1.session.Session
"""
loader = keystoneauth1.loading.get_plugin_loader('password')
auth = loader.load_from_options(
auth_url='{}://{}:{}/'
.format(identity_credentials.auth_protocol(),
identity_credentials.auth_host(),
identity_credentials.auth_port()),
user_domain_name=identity_credentials.credentials_user_domain_name(),
project_domain_name=(
identity_credentials.credentials_project_domain_name()),
project_name=identity_credentials.credentials_project(),
username=identity_credentials.credentials_username(),
password=identity_credentials.credentials_password())
session = keystoneauth1.session.Session(
auth=auth,
verify=SYSTEM_CA_BUNDLE)
return session
def get_glance_client(session):
"""Get Glance Client from Keystone Session.
:param session: Keystone Session object
:type session: keystoneauth1.session.Session
:returns: Glance Client
:rtype: glanceclient.Client
"""
return glanceclient.Client('2', session=session)
def get_product_name(stream='daily', variant='server', release='18.04',
arch=''):
"""Build Simple Streams ``product_name`` string.
:param stream: Stream type. ('daily'|'released')
:type stream: str
:param variant: Image variant. ('server'|'minimal')
:type variant: str
:param release: Release verssion. (e.g. '18.04')
:type release: str
:param arch: Architecture string as Debian would expect it
(Optional: default behaviour is to query dpkg)
:type arch: str
:returns: Simple Streams ``product_name``
:rtype: str
"""
if not arch:
arch = subprocess.check_output(
['dpkg', '--print-architecture'],
universal_newlines=True).rstrip()
if stream and stream != 'released':
return ('com.ubuntu.cloud.{}:{}:{}:{}'
.format(stream, variant, release, arch))
else:
return ('com.ubuntu.cloud:{}:{}:{}'
.format(variant, release, arch))
def find_image(glance, filters):
"""Find most recent image based on filters and ``version_name``.
:param filters: Dictionary with Glance image properties to filter on
:type filters: dict
:returns: Glance image object
"""
candidate = None
for image in glance.images.list(filters=filters,
sort_key='created_at',
sort_dir='desc'):
# glance does not offer ``version_name`` as a sort key.
# iterate over result to make sure we get the most recent image.
if not candidate or candidate.version_name < image.version_name:
candidate = image
return candidate
def find_destination_image(glance, product_name, version_name):
"""Find previously retrofitted image.
:param product_name: SimpleStreams ``product_name``
:type product_name: str
:param version_name: SimpleStreams ``version_name``
:type version_name: str
:returns: Glance image object
:rtype: generator
"""
return glance.images.list(filters={'source_product_name': product_name,
'source_version_name': version_name})
def find_source_image(glance):
"""Find source image in Glance.
Attempts to find a image from the ``daily`` stream first and reverts to
the ``released`` stream if none is found there.
Image variant ``server`` is selected over ``minimal`` as source at the
moment. This is due to it taking a shorter amount of time to retrofit
the standard image and its presence being more commonplace in deployed
clouds.
:returns: Glance image object or None
:rtype: Option[..., None]
"""
for stream in 'daily', 'released':
for variant in 'server', 'minimal':
product = get_product_name(stream=stream, variant=variant)
image = find_image(glance, filters={'product_name': product})
if image:
break
else:
continue
break
return image
def download_image(glance, image, file_object):
"""Download image from glance.
:param glance: Glance client
:type glance: glanceclient.Client
:param image: Glance image object
:type image: Glance image object
:param file_object: Open file object to write data to
:type file_object: Python file object
"""
with file_object as out:
for chunk in glance.images.data(image.id):
out.write(chunk)

View File

@ -12,17 +12,35 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import subprocess
import tempfile
import charms_openstack.adapters
import charms_openstack.charm
import charms_openstack.charm.core
import charmhelpers.core as ch_core
import charm.openstack.glance_retrofitter as glance_retrofitter
TMPDIR = '/var/snap/octavia-diskimage-retrofit/common/tmp'
class SourceImageNotFound(Exception):
pass
class DestinationImageExists(Exception):
pass
class OctaviaDiskimageRetrofitCharm(charms_openstack.charm.OpenStackCharm):
release = 'rocky'
name = 'octavia-diskimage-retrofit'
python_version = 3
adapters_class = charms_openstack.adapters.OpenStackRelationAdapters
required_relations = ['identity-credentials']
required_relations = ['juju-info', 'identity-credentials']
@property
def application_version(self):
@ -33,3 +51,87 @@ class OctaviaDiskimageRetrofitCharm(charms_openstack.charm.OpenStackCharm):
self.name,
project='services',
domain='service_domain')
def retrofit(self, keystone_endpoint, force=False, image_id=''):
"""Use ``octavia-diskimage-retrofit`` tool to retrofit an image.
:param keystone_endpoint: Keystone Credentials endpoint
:type keystone_endpoint: keystone-credentials RelationBase
:param force: Force retrofitting of image despite presence of
apparently up to date target image
:type force: bool
:param image_id: Use specific source image for retrofitting
:type image_id: str
:raises:SourceImageNotFound,DestinationImageExists
"""
session = glance_retrofitter.session_from_identity_credentials(
keystone_endpoint)
glance = glance_retrofitter.get_glance_client(session)
if image_id:
source_image = next(glance.images.list(filters={'id': image_id}))
else:
source_image = glance_retrofitter.find_source_image(glance)
if not source_image:
raise SourceImageNotFound('unable to find suitable source image')
if not image_id:
for image in glance_retrofitter.find_destination_image(
glance,
source_image.product_name,
source_image.version_name):
if not force:
raise DestinationImageExists(
'image with product_name "{}" and '
'version_name "{}" already exists: "{}"'
.format(source_image.product_name,
source_image.version_name, image.id))
input_file = tempfile.NamedTemporaryFile(delete=False, dir=TMPDIR)
ch_core.hookenv.atexit(os.unlink, input_file.name)
ch_core.hookenv.status_set('maintenance',
'Downloading {}'
.format(source_image.name))
glance_retrofitter.download_image(glance, source_image, input_file)
output_file = tempfile.NamedTemporaryFile(delete=False, dir=TMPDIR)
ch_core.hookenv.atexit(os.unlink, output_file.name)
output_file.close()
ch_core.hookenv.status_set('maintenance',
'Retrofitting {}'
.format(source_image.name))
subprocess.check_output(
['octavia-diskimage-retrofit', '-d',
'-u', ch_core.hookenv.config('retrofit-uca-pocket'),
input_file.name, output_file.name],
stderr=subprocess.STDOUT, universal_newlines=True)
# NOTE(fnordahl) the manifest is stored within the image itself in
# ``/etc/dib-manifests``. A copy of the manifest is saved on the host
# by the ``octavia-diskimage-retrofit`` tool. With the lack of a place
# to store the copy, remove it. (it does not fit in a Glance image
# property)
manifest_file = output_file.name + '.manifest'
ch_core.hookenv.atexit(os.unlink, manifest_file)
dest_name = 'amphora-haproxy'
for image_property in (source_image.architecture,
source_image.os_distro,
source_image.os_version,
source_image.version_name):
# build a informative image name
dest_name += '-' + str(image_property)
dest_image = glance.images.create(container_format='bare',
disk_format='qcow2',
name=dest_name)
ch_core.hookenv.status_set('maintenance',
'Uploading {}'
.format(dest_image.name))
with open(output_file.name, 'rb') as fin:
glance.images.upload(dest_image.id, fin)
glance.images.update(
dest_image.id,
source_product_name=source_image.product_name or 'custom',
source_version_name=source_image.version_name or 'custom',
tags=[self.name, 'octavia-amphora'])

View File

@ -26,6 +26,7 @@ charm.use_defaults(
@reactive.when_not('charm.installed')
def check_snap_installed():
# Installation is handled by the ``snap`` layer, just update our status.
with charm.provide_charm_instance() as instance:
instance.assess_status()
reactive.set_flag('charm.installed')
@ -38,3 +39,10 @@ def request_credentials():
'identity-credentials.connected')
with charm.provide_charm_instance() as instance:
instance.request_credentials(keystone_endpoint)
instance.assess_status()
@reactive.when('identity-credentials.available')
def credentials_available():
with charm.provide_charm_instance() as instance:
instance.assess_status()

View File

@ -8,3 +8,4 @@ flake8>=2.2.4,<=2.4.1
stestr>=2.2.0
requests>=2.18.4
git+https://github.com/openstack-charmers/zaza.git#egg=zaza
git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack

View File

@ -0,0 +1,67 @@
series: bionic
relations:
- - glance-simplestreams-sync:juju-info
- octavia-diskimage-retrofit:juju-info
- - mysql:shared-db
- keystone:shared-db
- - mysql:shared-db
- glance:shared-db
- - keystone:identity-service
- glance:identity-service
- - keystone:identity-service
- glance-simplestreams-sync:identity-service
- - glance:amqp
- rabbitmq-server:amqp
- - rabbitmq-server:amqp
- glance-simplestreams-sync:amqp
- - keystone:identity-credentials
- octavia-diskimage-retrofit:identity-credentials
applications:
keystone:
charm: cs:~openstack-charmers-next/keystone
num_units: 1
options:
openstack-origin: cloud:bionic-stein
mysql:
constraints: mem=3072M
charm: cs:~openstack-charmers-next/percona-cluster
num_units: 1
rabbitmq-server:
charm: cs:~openstack-charmers-next/rabbitmq-server
num_units: 1
glance:
charm: cs:~openstack-charmers-next/glance
num_units: 1
options:
openstack-origin: cloud:bionic-stein
glance-simplestreams-sync:
charm: cs:~openstack-charmers-next/glance-simplestreams-sync
num_units: 1
options:
source: ppa:simplestreams-dev/trunk
use_swift: False
mirror_list: "[{url: 'http://cloud-images.ubuntu.com/daily/',
name_prefix: 'ubuntu:released',
path: 'streams/v1/index.sjson',
max: 1,
item_filters: [
'release~(xenial|bionic|eoan)',
'arch~(x86_64|amd64)',
'ftype~(disk1.img|disk.img)'
]
},
{url: 'http://cloud-images.ubuntu.com/minimal/daily/',
name_prefix: 'ubuntu:released',
path: 'streams/v1/index.sjson',
max: 1,
item_filters: [
'release~(xenial|bionic|eoan)',
'arch~(x86_64|amd64)',
'ftype~(disk1.img|disk.img)'
]
}]"
octavia-diskimage-retrofit:
series: bionic
charm: ../../../octavia-diskimage-retrofit
options:
retrofit-uca-pocket: stein

View File

@ -0,0 +1,65 @@
series: disco
relations:
- - glance-simplestreams-sync:juju-info
- octavia-diskimage-retrofit:juju-info
- - mysql:shared-db
- keystone:shared-db
- - mysql:shared-db
- glance:shared-db
- - keystone:identity-service
- glance:identity-service
- - keystone:identity-service
- glance-simplestreams-sync:identity-service
- - glance:amqp
- rabbitmq-server:amqp
- - rabbitmq-server:amqp
- glance-simplestreams-sync:amqp
- - keystone:identity-credentials
- octavia-diskimage-retrofit:identity-credentials
applications:
keystone:
charm: cs:~openstack-charmers-next/keystone
num_units: 1
options:
openstack-origin: distro
mysql:
constraints: mem=3072M
charm: cs:~openstack-charmers-next/percona-cluster
num_units: 1
rabbitmq-server:
charm: cs:~openstack-charmers-next/rabbitmq-server
num_units: 1
glance:
charm: cs:~openstack-charmers-next/glance
num_units: 1
options:
openstack-origin: distro
glance-simplestreams-sync:
charm: cs:~openstack-charmers-next/glance-simplestreams-sync
num_units: 1
options:
source: ppa:simplestreams-dev/trunk
use_swift: False
mirror_list: "[{url: 'http://cloud-images.ubuntu.com/daily/',
name_prefix: 'ubuntu:released',
path: 'streams/v1/index.sjson',
max: 1,
item_filters: [
'release~(xenial|bionic|eoan)',
'arch~(x86_64|amd64)',
'ftype~(disk1.img|disk.img)'
]
},
{url: 'http://cloud-images.ubuntu.com/minimal/daily/',
name_prefix: 'ubuntu:released',
path: 'streams/v1/index.sjson',
max: 1,
item_filters: [
'release~(xenial|bionic|eoan)',
'arch~(x86_64|amd64)',
'ftype~(disk1.img|disk.img)'
]
}]"
octavia-diskimage-retrofit:
series: disco
charm: ../../../octavia-diskimage-retrofit

View File

@ -0,0 +1,65 @@
series: eoan
relations:
- - glance-simplestreams-sync:juju-info
- octavia-diskimage-retrofit:juju-info
- - mysql:shared-db
- keystone:shared-db
- - mysql:shared-db
- glance:shared-db
- - keystone:identity-service
- glance:identity-service
- - keystone:identity-service
- glance-simplestreams-sync:identity-service
- - glance:amqp
- rabbitmq-server:amqp
- - rabbitmq-server:amqp
- glance-simplestreams-sync:amqp
- - keystone:identity-credentials
- octavia-diskimage-retrofit:identity-credentials
applications:
keystone:
charm: cs:~openstack-charmers-next/keystone
num_units: 1
options:
openstack-origin: distro
mysql:
constraints: mem=3072M
charm: cs:~openstack-charmers-next/percona-cluster
num_units: 1
rabbitmq-server:
charm: cs:~openstack-charmers-next/rabbitmq-server
num_units: 1
glance:
charm: cs:~openstack-charmers-next/glance
num_units: 1
options:
openstack-origin: distro
glance-simplestreams-sync:
charm: cs:~openstack-charmers-next/glance-simplestreams-sync
num_units: 1
options:
source: ppa:simplestreams-dev/trunk
use_swift: False
mirror_list: "[{url: 'http://cloud-images.ubuntu.com/daily/',
name_prefix: 'ubuntu:released',
path: 'streams/v1/index.sjson',
max: 1,
item_filters: [
'release~(xenial|bionic|eoan)',
'arch~(x86_64|amd64)',
'ftype~(disk1.img|disk.img)'
]
},
{url: 'http://cloud-images.ubuntu.com/minimal/daily/',
name_prefix: 'ubuntu:released',
path: 'streams/v1/index.sjson',
max: 1,
item_filters: [
'release~(xenial|bionic|eoan)',
'arch~(x86_64|amd64)',
'ftype~(disk1.img|disk.img)'
]
}]"
octavia-diskimage-retrofit:
series: eoan
charm: ../../../octavia-diskimage-retrofit

View File

@ -1,11 +1,15 @@
charm_name: octavia-diskimage-retrofit
smoke_bundles:
- bionic-rocky
gate_bundles:
- bionic-rocky
- bionic-stein
- disco
dev_bundles:
- eoan
target_deploy_status:
glance-simplestreams-sync:
workload-status: active
workload-status-message: Sync completed
configure:
- zaza.charm_tests.noop.setup.basic_setup
tests:
- zaza.charm_tests.noop.tests.NoopTest
- zaza.openstack.charm_tests.octavia.diskimage_retrofit.tests.OctaviaDiskimageRetrofitTest

View File

@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import mock
import sys
sys.path.append('src')
@ -20,3 +21,12 @@ sys.path.append('src/lib')
# Mock out charmhelpers so that we can test without it.
import charms_openstack.test_mocks # noqa
charms_openstack.test_mocks.mock_charmhelpers()
global glanceclient
global keystoneauth1
glanceclient = mock.MagicMock()
keystoneauth1 = mock.MagicMock()
sys.modules['glanceclient'] = glanceclient
sys.modules['keystoneauth1'] = keystoneauth1
sys.modules['keystoneauth1.loading'] = keystoneauth1.loading
sys.modules['keystoneauth1.session'] = keystoneauth1.session

View File

@ -0,0 +1,122 @@
# Copyright 2019 Canonical Ltd
#
# 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 mock
import charms_openstack.test_utils as test_utils
import charm.openstack.glance_retrofitter as glance_retrofitter
class TestGlanceRetrofitter(test_utils.PatchHelper):
def test_session_from_identity_credentials(self):
self.patch_object(
glance_retrofitter.keystoneauth1.loading, 'get_plugin_loader')
self.patch_object(
glance_retrofitter.keystoneauth1.session, 'Session')
loader = mock.MagicMock()
self.get_plugin_loader.return_value = loader
endpoint = mock.MagicMock()
result = glance_retrofitter.session_from_identity_credentials(endpoint)
self.get_plugin_loader.assert_called_once_with('password')
loader.load_from_options.assert_called_once_with(
auth_url='{}://{}:{}/'
.format(endpoint.auth_protocol(),
endpoint.auth_host(),
endpoint.auth_port()),
user_domain_name=endpoint.credentials_user_domain_name(),
project_domain_name=endpoint.credentials_project_domain_name(),
project_name=endpoint.credentials_project(),
username=endpoint.credentials_username(),
password=endpoint.credentials_password())
self.Session.assert_called_once_with(
auth=loader.load_from_options(),
verify=glance_retrofitter.SYSTEM_CA_BUNDLE)
self.assertEquals(result, self.Session())
def test_get_glance_client(self):
self.patch_object(glance_retrofitter.glanceclient, 'Client')
result = glance_retrofitter.get_glance_client('aSession')
self.Client.assert_called_once_with('2', session='aSession')
self.assertEquals(result, self.Client())
def test_get_product_name(self):
self.patch_object(glance_retrofitter.subprocess, 'check_output')
self.check_output.return_value = 'aArchitecture'
self.assertEquals(glance_retrofitter.get_product_name(),
'com.ubuntu.cloud.daily:server:18.04:aArchitecture')
self.check_output.assert_called_once_with(
['dpkg', '--print-architecture'],
universal_newlines=True)
def test_find_image(self):
glance = mock.MagicMock()
class FakeImage1(object):
version_name = '20194242'
fake_image1 = FakeImage1()
class FakeImage2(object):
version_name = '20195151'
fake_image2 = FakeImage2()
glance.images.list.return_value = [fake_image1, fake_image2]
self.assertEquals(
glance_retrofitter.find_image(glance, {'fake_property': 'real'}),
fake_image2)
glance.images.list.assert_called_once_with(
filters={'fake_property': 'real'},
sort_key='created_at',
sort_dir='desc')
def test_find_destination_image(self):
glance = mock.MagicMock()
result = glance_retrofitter.find_destination_image(
glance, 'aProduct', 'aVersion')
glance.images.list.assert_called_once_with(
filters={'source_product_name': 'aProduct',
'source_version_name': 'aVersion'})
self.assertEquals(result, glance.images.list())
def test_find_source_image(self):
self.patch_object(glance_retrofitter, 'get_product_name')
self.patch_object(glance_retrofitter, 'find_image')
self.get_product_name.return_value = 'aProduct'
self.find_image.side_effect = [None, None, None, 'aImage']
self.assertEquals(
glance_retrofitter.find_source_image('aGlance'),
'aImage')
self.get_product_name.assert_has_calls([
mock.call(stream='daily', variant='server'),
mock.call(stream='daily', variant='minimal'),
mock.call(stream='released', variant='server'),
mock.call(stream='released', variant='minimal'),
])
self.find_image.assert_called_with(
'aGlance', filters={'product_name': 'aProduct'})
def test_download_image(self):
glance = mock.MagicMock()
glance.images.data.return_value = ['two', 'Chunks']
image = mock.MagicMock()
file_object = mock.MagicMock()
file_handle = mock.MagicMock()
file_object.__enter__.return_value = file_handle
glance_retrofitter.download_image(glance, image, file_object)
glance.images.data.assert_called_once_with(image.id)
file_handle.write.assert_has_calls([
mock.call('two'),
mock.call('Chunks'),
])

View File

@ -13,6 +13,8 @@
# limitations under the License.
import mock
import os
import subprocess
import charms_openstack.test_utils as test_utils
@ -39,3 +41,75 @@ class TestOctaviaDiskimageRetrofitCharm(test_utils.PatchHelper):
c.name,
project='services',
domain='service_domain')
def test_retrofit(self):
self.patch_object(octavia_diskimage_retrofit, 'glance_retrofitter')
glance = mock.MagicMock()
self.glance_retrofitter.get_glance_client.return_value = glance
class FakeImage(object):
id = 'aId'
name = 'aName'
architecture = 'aArchitecture'
os_distro = 'aOSDistro'
os_version = 'aOSVersion'
version_name = 'aVersionName'
product_name = 'aProductName'
fake_image = FakeImage()
glance.images.create.return_value = fake_image
self.glance_retrofitter.find_source_image.return_value = fake_image
self.patch_object(
octavia_diskimage_retrofit.tempfile,
'NamedTemporaryFile')
self.patch_object(octavia_diskimage_retrofit.ch_core, 'hookenv')
self.patch_object(octavia_diskimage_retrofit.subprocess,
'check_output')
c = octavia_diskimage_retrofit.OctaviaDiskimageRetrofitCharm()
with mock.patch('charm.openstack.octavia_diskimage_retrofit.open',
create=True) as mocked_open:
self.glance_retrofitter.find_destination_image.return_value = \
[fake_image]
with self.assertRaises(Exception):
c.retrofit('aKeystone')
self.glance_retrofitter.session_from_identity_credentials.\
assert_called_once_with('aKeystone')
self.glance_retrofitter.get_glance_client.assert_called_once_with(
self.glance_retrofitter.session_from_identity_credentials())
self.glance_retrofitter.find_destination_image.return_value = \
[]
c.retrofit('aKeystone')
self.NamedTemporaryFile.assert_has_calls([
mock.call(delete=False,
dir=octavia_diskimage_retrofit.TMPDIR),
mock.call(delete=False,
dir=octavia_diskimage_retrofit.TMPDIR),
])
self.hookenv.atexit.assert_called_with(os.unlink, mock.ANY)
self.hookenv.status_set.assert_has_calls([
mock.call('maintenance', 'Downloading aName'),
mock.call('maintenance', 'Retrofitting aName'),
mock.call('maintenance', 'Uploading aName'),
])
self.glance_retrofitter.download_image.assert_called_once_with(
glance, fake_image, self.NamedTemporaryFile())
self.hookenv.config.assert_called_once_with('retrofit-uca-pocket')
self.check_output.assert_called_once_with(
['octavia-diskimage-retrofit', '-d', '-u',
self.hookenv.config(), self.NamedTemporaryFile().name,
self.NamedTemporaryFile().name],
stderr=subprocess.STDOUT, universal_newlines=True)
glance.images.create.assert_called_once_with(
container_format='bare',
disk_format='qcow2',
name='amphora-haproxy-aArchitecture-aOSDistro-aOSVersion-'
'aVersionName')
glance.images.upload.assert_called_once_with('aId', mock.ANY)
mocked_open.assert_called_once_with(
self.NamedTemporaryFile().name, 'rb')
glance.images.update.assert_called_once_with(
'aId',
source_product_name='aProductName',
source_version_name='aVersionName',
tags=['octavia-diskimage-retrofit', 'octavia-amphora'])

View File

@ -29,6 +29,10 @@ class TestRegisteredHooks(test_utils.TestRegisteredHooks):
'when': {
'request_credentials': (
'identity-credentials.connected',),
'credentials_available': (
'identity-credentials.available',),
'build': (
'octavia-diskimage-retrofit.build',),
},
'when_not': {
'check_snap_installed': (
@ -67,3 +71,8 @@ class TestOctaviaDiskimageRetrofitHandlers(test_utils.PatchHelper):
'identity-credentials.connected')
self.charm_instance.request_credentials.assert_called_once_with(
'endpoint')
self.charm_instance.assess_status.assert_called_once_with()
def test_credentials_available(self):
handlers.credentials_available()
self.charm_instance.assess_status.assert_called_once_with()