Files
zaza-openstack-tests/zaza/openstack/utilities/swift.py
Liam Young 17234c9e15 Respect model in get_swift_storage_topology
Change 0af4c93d causes the target model to be ignored when a
model_name was specified. This in turn is causing the swift
cmr tests to fail as an ip of None is being recorded for
units in a different model.
2022-09-02 14:17:07 +00:00

370 lines
13 KiB
Python

# 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.
"""Swift utilities."""
import logging
import uuid
import zaza.model
import zaza.openstack.utilities.juju as juju_utils
class ObjectReplica:
"""A replica of an object.
The replica attributes show the location of an object replica.
server: IP address or hostname of machine hosting replica
port: Port of swift object server running on machine hosting replica
device: Path to device hosting replica
handoff_device: Whether this is a handoff devices. Handoff devices pass
the replica on to a remote storage node.
"""
def __init__(self, raw_line):
"""Extract storage info from text."""
rl = raw_line.split()
self.server, self.port = rl[2].split(':')
self.device = rl[3]
self.handoff_device = rl[-1] == '[Handoff]'
class ObjectReplicas:
"""Replicas of an object."""
def __init__(self, proxy_app, account, container_name, object_name,
storage_topology, model_name=None):
"""Find all replicas of given object.
:param proxy_app: Name of proxy application
:type proxy_app: str
:param account: Account that owns the container.
:type account: str
:param container_name: Name of container that contains the object.
:type container_name: str
:param object_name: Name of object.
:type object_name: str
:param storage_topology: Dictionary keyed on IP of storage node info.
:type storage_topology: {}
:param model_name: Model to point environment at
:type model_name: str
"""
self.replicas = []
self.replica_placements = {}
self.storage_topology = storage_topology
raw_output = self.run_get_nodes(
proxy_app,
account,
container_name,
object_name,
model_name=model_name)
for line in self.extract_storage_lines(raw_output):
self.add_replica(line)
def add_replica(self, storage_line):
"""Add a replica to the replica set."""
self.replicas.append(ObjectReplica(storage_line))
def extract_storage_lines(self, raw_output):
"""Extract replica list from output of swift-get-nodes.
:param storage_line: Output of swift-get-nodes
:type storage_line: str
:returns: List of lines relating to replicas.
:rtype: [str, ...]
"""
storage_lines = []
for line in raw_output.split('\n'):
if line.startswith('Server:Port '):
storage_lines.append(line)
return storage_lines
def run_get_nodes(self, proxy_app, account, container_name, object_name,
model_name=None):
"""Run swift-get-nodes for an object on a proxy unit.
:param proxy_app: Name of proxy application
:type proxy_app: str
:param account: Account that owns the container.
:type account: str
:param container_name: Name of container that contains the object.
:type container_name: str
:param object_name: Name of object.
:type object_name: str
:param model_name: Model to point environment at
:type model_name: str
:returns: Stdout of command
:rtype: str
"""
ring_file = '/etc/swift/object.ring.gz'
obj_cmd = "swift-get-nodes -a {} {} {} {}".format(
ring_file,
account,
container_name,
object_name)
cmd_result = zaza.model.run_on_leader(
proxy_app,
obj_cmd,
model_name=model_name)
return cmd_result['Stdout']
@property
def hand_off_ips(self):
"""Replicas which are marked as handoff devices.
These are not real replicas. They hand off the replica to other node.
:returns: List of IPS of handoff nodes for object.
:rtype: List[str]
"""
return [r.server for r in self.replicas if r.handoff_device]
@property
def storage_ips(self):
"""Ip addresses of nodes that are housing a replica.
:returns: List of IPS of storage nodes holding a replica of the object.
:rtype: [str, ...]
"""
return [r.server for r in self.replicas if not r.handoff_device]
@property
def placements(self):
"""Region an zone information for each replica.
Zone info is in the form:
[{
'app_name': str,
'unit': juju.Unit,
'region': int,
'zone': int}, ...]
:returns: List of dicts with region and zone information.
:rtype: List[Dict[str, Union[str,int]]]
"""
return [self.storage_topology[ip] for ip in self.storage_ips]
@property
def distinct_regions(self):
"""List of distinct regions that have a replica.
:returns: List of regions that have a replica
:rtype: [int, ...]
"""
return list(set([p['region'] for p in self.placements]))
@property
def all_zones(self):
"""List of all zones that have a replica.
:returns: List of tuples (region, zone) that have a replica.
:rtype: List[Tuple[str, str]]
"""
return [(p['region'], p['zone']) for p in self.placements]
@property
def distinct_zones(self):
"""List of distinct region + zones that have a replica.
:returns: List of tuples (region, zone) that have a replica.
:rtype: [(r1, z1), ...]
"""
return list(set(self.all_zones))
def get_swift_storage_topology(model_name=None):
"""Get details of storage nodes and which region and zones they belong in.
:param model_name: Model to point environment at
:type model_name: str
:returns: Dictionary of storage nodes and their region/zone information.
:rtype: {
'ip (str)': {
'app_name': str,
'unit': juju.Unit
'region': int,
'zone': int},
...}
"""
topology = {}
status = juju_utils.get_full_juju_status(model_name=model_name)
for app_name, app_dep_config in status.applications.items():
if 'swift-storage' in app_dep_config['charm']:
app_config = zaza.model.get_application_config(
app_name,
model_name=model_name)
region = app_config['storage-region']['value']
zone = app_config['zone']['value']
for unit in zaza.model.get_units(app_name, model_name=model_name):
unit_ip = zaza.model.get_unit_public_address(
unit,
model_name=model_name)
topology[unit_ip] = {
'app_name': app_name,
'unit': unit,
'region': region,
'zone': zone}
return topology
def setup_test_container(swift_client, resource_prefix):
"""Create a swift container for use be tests.
:param swift_client: Swift client to use for object creation
:type swift_client: swiftclient.Client
:returns: (container_name, account_name) Container name and account
name for new container
:rtype: Tuple[str, str]
"""
run_id = str(uuid.uuid1()).split('-')[0]
container_name = '{}-{}-container'.format(resource_prefix, run_id)
swift_client.put_container(container_name)
resp_headers, containers = swift_client.get_account()
account = resp_headers['x-account-project-domain-id']
return container_name, account
def apply_proxy_config(proxy_app, config, model_name=None):
"""Update the give proxy_app with new charm config.
:param proxy_app: Name of proxy application
:type proxy_app: str
:param config: Dictionary of configuration setting(s) to apply
:type config: dict
:param model_name: Name of model to query.
:type model_name: str
"""
current_config = zaza.model.get_application_config(
proxy_app,
model_name=model_name)
# Although there is no harm in applying config that is a noop it
# does affect the expected behaviour afterwards. So, only apply
# genuine changes so we can safely expect the charm to fire a hook.
for key, value in config.items():
if str(config[key]) != str(current_config[key]['value']):
break
else:
logging.info(
'Config update for {} not required.'.format(proxy_app))
return
logging.info('Updating {} charm settings'.format(proxy_app))
zaza.model.set_application_config(
proxy_app,
config,
model_name=model_name)
zaza.model.block_until_all_units_idle()
def create_object(swift_client, proxy_app, storage_topology, resource_prefix,
model_name=None):
"""Create a test object in a new container.
:param swift_client: Swift client to use for object creation
:type swift_client: swiftclient.Client
:param proxy_app: Name of proxy application
:type proxy_app: str
:param storage_topology: Dictionary keyed on IP of storage node info.
:type storage_topology: {}
:param resource_prefix: Prefix to use when naming new resources
:type resource_prefix: str
:param model_name: Model to point environment at
:type model_name: str
:returns: (container_name, object_name, object replicas)
:rtype: (str, str, ObjectReplicas)
"""
container_name, account = setup_test_container(
swift_client,
resource_prefix)
object_name = 'zaza_test_object.txt'
swift_client.put_object(
container_name,
object_name,
contents='File contents',
content_type='text/plain'
)
obj_replicas = ObjectReplicas(
proxy_app,
account,
container_name,
object_name,
storage_topology,
model_name=model_name)
return container_name, object_name, obj_replicas
def search_builder(proxy_app, ring, search_target, model_name=None):
"""Run a swift-ring-builder search.
:param proxy_app: Name of proxy application
:type proxy_app: str
:param ring: Name of ring (one of: object, account, container)
:type ring: str
:param search_target: device search string (see: man swift-ring-builder)
:type search_target: str
:param model_name: Model to point environment at
:type model_name: str
:returns: stdout - full stdout output from swift-ring-builder cmd
:rtype: str
"""
cmd = ('swift-ring-builder /etc/swift/{}.builder search {}'
''.format(ring, search_target))
result = zaza.model.run_on_leader(proxy_app, cmd,
model_name=model_name)
return result['Stdout']
def is_proxy_ring_up_to_date(proxy_app, ring, model_name=None):
"""Check if the ring file is up-to-date with changes of the builder.
:param proxy_app: Name of proxy application
:type proxy_app: str
:param ring: Name of ring (one of: object, account, container)
:type ring: str
:param model_name: Model to point environment at
:type model_name: str
:returns: True if swift-ring-builder denotes ring.gz file is up-to-date
:rtype: str
"""
logging.info('Checking ring file matches builder file')
cmd = ('swift-ring-builder /etc/swift/{}.builder | '
'grep "Ring file .* is"'.format(ring))
result = zaza.model.run_on_leader(proxy_app, cmd, model_name=model_name)
expected = ('Ring file /etc/swift/{}.ring.gz is up-to-date'
''.format(ring))
return bool(result['Stdout'].strip('\n') == expected)
def is_ring_synced(proxy_app, ring, expected_hosts, model_name=None):
"""Check if md5sums of rings on swift-storage are synced to this proxy.
:param proxy_app: Name of proxy application
:type proxy_app: str
:param ring: Name of ring (one of: object, account, container)
:type ring: str
:param expoected_hosts: Number of swift-storage hosts in test environment
:type search_target: int
:param model_name: Model to point environment at
:type model_name: str
:returns: True if all expected_hosts matched md5sum of proxy ring file
:rtype: bool
"""
logging.info('Checking ring md5sums on storage unit(s) against proxy')
zaza.model.block_until_all_units_idle()
cmd = ('swift-recon {} --md5 | '
'grep -A1 "ring md5" | tail -1'.format(ring))
result = zaza.model.run_on_leader(proxy_app, cmd, model_name=model_name)
expected = ('{num}/{num} hosts matched, 0 error[s] while checking hosts.'
''.format(num=expected_hosts))
return bool(result['Stdout'].strip('\n') == expected)