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:
|
includes:
|
||||||
- layer:openstack
|
- layer:openstack
|
||||||
- layer:snap
|
- layer:snap
|
||||||
|
- layer:tls-client
|
||||||
- interface:juju-info
|
- interface:juju-info
|
||||||
- interface:keystone-credentials
|
- interface:keystone-credentials
|
||||||
|
- interface:tls-certificates
|
||||||
options:
|
options:
|
||||||
basic:
|
basic:
|
||||||
use_venv: True
|
use_venv: True
|
||||||
|
@ -10,11 +12,11 @@ options:
|
||||||
packages: [ 'libffi-dev', 'libssl-dev' ]
|
packages: [ 'libffi-dev', 'libssl-dev' ]
|
||||||
snap:
|
snap:
|
||||||
octavia-diskimage-retrofit:
|
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
|
channel: edge
|
||||||
devmode: True
|
devmode: True
|
||||||
|
comment: |
|
||||||
|
Using devmode pending resolution of snapd fuse-support issue
|
||||||
|
https://github.com/openstack-charmers/octavia-diskimage-retrofit/issues/6
|
||||||
resources:
|
resources:
|
||||||
octavia-diskimage-retrofit:
|
octavia-diskimage-retrofit:
|
||||||
type: file
|
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
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
|
||||||
import charms_openstack.adapters
|
import charms_openstack.adapters
|
||||||
import charms_openstack.charm
|
import charms_openstack.charm
|
||||||
import charms_openstack.charm.core
|
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):
|
class OctaviaDiskimageRetrofitCharm(charms_openstack.charm.OpenStackCharm):
|
||||||
release = 'rocky'
|
release = 'rocky'
|
||||||
name = 'octavia-diskimage-retrofit'
|
name = 'octavia-diskimage-retrofit'
|
||||||
python_version = 3
|
python_version = 3
|
||||||
adapters_class = charms_openstack.adapters.OpenStackRelationAdapters
|
adapters_class = charms_openstack.adapters.OpenStackRelationAdapters
|
||||||
required_relations = ['identity-credentials']
|
required_relations = ['juju-info', 'identity-credentials']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def application_version(self):
|
def application_version(self):
|
||||||
|
@ -33,3 +51,87 @@ class OctaviaDiskimageRetrofitCharm(charms_openstack.charm.OpenStackCharm):
|
||||||
self.name,
|
self.name,
|
||||||
project='services',
|
project='services',
|
||||||
domain='service_domain')
|
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')
|
@reactive.when_not('charm.installed')
|
||||||
def check_snap_installed():
|
def check_snap_installed():
|
||||||
|
# Installation is handled by the ``snap`` layer, just update our status.
|
||||||
with charm.provide_charm_instance() as instance:
|
with charm.provide_charm_instance() as instance:
|
||||||
instance.assess_status()
|
instance.assess_status()
|
||||||
reactive.set_flag('charm.installed')
|
reactive.set_flag('charm.installed')
|
||||||
|
@ -38,3 +39,10 @@ def request_credentials():
|
||||||
'identity-credentials.connected')
|
'identity-credentials.connected')
|
||||||
with charm.provide_charm_instance() as instance:
|
with charm.provide_charm_instance() as instance:
|
||||||
instance.request_credentials(keystone_endpoint)
|
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
|
stestr>=2.2.0
|
||||||
requests>=2.18.4
|
requests>=2.18.4
|
||||||
git+https://github.com/openstack-charmers/zaza.git#egg=zaza
|
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
|
charm_name: octavia-diskimage-retrofit
|
||||||
smoke_bundles:
|
smoke_bundles:
|
||||||
- bionic-rocky
|
- bionic-rocky
|
||||||
|
gate_bundles:
|
||||||
|
- bionic-rocky
|
||||||
|
- bionic-stein
|
||||||
|
- disco
|
||||||
|
dev_bundles:
|
||||||
|
- eoan
|
||||||
target_deploy_status:
|
target_deploy_status:
|
||||||
glance-simplestreams-sync:
|
glance-simplestreams-sync:
|
||||||
workload-status: active
|
workload-status: active
|
||||||
workload-status-message: Sync completed
|
workload-status-message: Sync completed
|
||||||
configure:
|
|
||||||
- zaza.charm_tests.noop.setup.basic_setup
|
|
||||||
tests:
|
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
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import mock
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path.append('src')
|
sys.path.append('src')
|
||||||
|
@ -20,3 +21,12 @@ sys.path.append('src/lib')
|
||||||
# Mock out charmhelpers so that we can test without it.
|
# Mock out charmhelpers so that we can test without it.
|
||||||
import charms_openstack.test_mocks # noqa
|
import charms_openstack.test_mocks # noqa
|
||||||
charms_openstack.test_mocks.mock_charmhelpers()
|
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.
|
# limitations under the License.
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
import charms_openstack.test_utils as test_utils
|
import charms_openstack.test_utils as test_utils
|
||||||
|
|
||||||
|
@ -39,3 +41,75 @@ class TestOctaviaDiskimageRetrofitCharm(test_utils.PatchHelper):
|
||||||
c.name,
|
c.name,
|
||||||
project='services',
|
project='services',
|
||||||
domain='service_domain')
|
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': {
|
'when': {
|
||||||
'request_credentials': (
|
'request_credentials': (
|
||||||
'identity-credentials.connected',),
|
'identity-credentials.connected',),
|
||||||
|
'credentials_available': (
|
||||||
|
'identity-credentials.available',),
|
||||||
|
'build': (
|
||||||
|
'octavia-diskimage-retrofit.build',),
|
||||||
},
|
},
|
||||||
'when_not': {
|
'when_not': {
|
||||||
'check_snap_installed': (
|
'check_snap_installed': (
|
||||||
|
@ -67,3 +71,8 @@ class TestOctaviaDiskimageRetrofitHandlers(test_utils.PatchHelper):
|
||||||
'identity-credentials.connected')
|
'identity-credentials.connected')
|
||||||
self.charm_instance.request_credentials.assert_called_once_with(
|
self.charm_instance.request_credentials.assert_called_once_with(
|
||||||
'endpoint')
|
'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