Merge pull request #1 from openstack-charmers/glance_retrofitter
Implement charm
This commit is contained in:
commit
cbb7a4e06b
|
@ -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.
|
|
@ -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))
|
|
@ -0,0 +1 @@
|
|||
actions.py
|
|
@ -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.
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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'])
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
])
|
|
@ -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'])
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue