Add OVS to OVN migration actions
Switch smoke to focal-ussuri. Unpin flake8. Change-Id: Ifa99988612eaaeb9d60a0d99db172f97e27cfc93
This commit is contained in:
parent
4751846159
commit
65f3911948
56
src/actions.yaml
Normal file
56
src/actions.yaml
Normal file
@ -0,0 +1,56 @@
|
||||
migrate-ovn-db:
|
||||
description: |
|
||||
Run the Neutron OVN DB Sync utility.
|
||||
params:
|
||||
i-really-mean-it:
|
||||
type: boolean
|
||||
default: false
|
||||
description: |
|
||||
The default of false will cause the action to perform a dry-run and log
|
||||
output. Set to true to perform the actual sync.
|
||||
.
|
||||
NOTE: The neutron-api units should be paused while running this action.
|
||||
required:
|
||||
- i-really-mean-it
|
||||
migrate-mtu:
|
||||
description: |
|
||||
Reduce MTU on overlay networks prior to migration to Geneve.
|
||||
params:
|
||||
i-really-mean-it:
|
||||
type: boolean
|
||||
default: false
|
||||
description: |
|
||||
The default of false will cause the action to verify that all overlay
|
||||
networks have been adjusted. Set to true to perform the actual
|
||||
migration.
|
||||
.
|
||||
NOTE: To avoid connectivity issues, running instances should already
|
||||
have been reconfigured with a lower MTU prior to running this action.
|
||||
.
|
||||
NOTE: The neutron-api units should NOT be paused while running this
|
||||
action.
|
||||
required:
|
||||
- i-really-mean-it
|
||||
offline-neutron-morph-db:
|
||||
description: |
|
||||
Perform optional offline morphing of tunnel networks in Neutron DB.
|
||||
params:
|
||||
i-really-mean-it:
|
||||
type: boolean
|
||||
default: false
|
||||
description: |
|
||||
The default of false will cause the action to not commit the database
|
||||
transaction, effectively performing a dry run. Set to true to perform
|
||||
the actual operation.
|
||||
.
|
||||
NOTE: Performing this action is optional and will allow migrated
|
||||
networks to show as type 'geneve' to the end user of the cloud which
|
||||
also allows other `openstack network set` operations to succeed
|
||||
post-migration.
|
||||
.
|
||||
NOTE: Before running this action you should make a backup of the
|
||||
Neutron database.
|
||||
.
|
||||
NOTE: The neutron-api units MUST be paused while running this action.
|
||||
required:
|
||||
- i-really-mean-it
|
276
src/actions/actions.py
Executable file
276
src/actions/actions.py
Executable file
@ -0,0 +1,276 @@
|
||||
#!/usr/local/sbin/charm-env python3
|
||||
#
|
||||
# Copyright 2020 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 contextlib
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
# Load modules from $CHARM_DIR/lib
|
||||
sys.path.append('lib')
|
||||
sys.path.append('reactive')
|
||||
|
||||
from charms.layer import basic
|
||||
basic.bootstrap_charm_deps()
|
||||
basic.init_config_states()
|
||||
|
||||
import charms_openstack.bus
|
||||
|
||||
import charmhelpers.core as ch_core
|
||||
|
||||
charms_openstack.bus.discover()
|
||||
|
||||
|
||||
NEUTRON_CONF = '/etc/neutron/neutron.conf'
|
||||
NEUTRON_OVN_DB_SYNC_CONF = '/etc/neutron/neutron-ovn-db-sync.conf'
|
||||
|
||||
|
||||
def get_neutron_credentials():
|
||||
"""Retrieve service credentials from Neutron's configuration file.
|
||||
|
||||
Since we are a subordinate of the neutron-api charm and have no direct
|
||||
relationship with Keystone ourselves we rely on gleaning Neutron's
|
||||
credentials from its config file.
|
||||
|
||||
:returns: Map of environment variable name and appropriate value for auth.
|
||||
:rtype: Dict[str,str]
|
||||
"""
|
||||
sections = {}
|
||||
parser = cfg.ConfigParser(NEUTRON_CONF, sections)
|
||||
parser.parse()
|
||||
auth_section = 'keystone_authtoken'
|
||||
return {
|
||||
'OS_USER_DOMAIN_NAME': sections[auth_section]['user_domain_name'][0],
|
||||
'OS_PROJECT_DOMAIN_NAME': sections[auth_section][
|
||||
'project_domain_name'][0],
|
||||
'OS_AUTH_URL': sections[auth_section]['auth_url'][0],
|
||||
'OS_PROJECT_NAME': sections[auth_section]['project_name'][0],
|
||||
'OS_USERNAME': sections[auth_section]['username'][0],
|
||||
'OS_PASSWORD': sections[auth_section]['password'][0],
|
||||
}
|
||||
|
||||
|
||||
def get_neutron_db_connection_string():
|
||||
"""Retrieve db connection string from Neutron's configuration file.
|
||||
|
||||
Since we are a subordinate of the neutron-api charm and have no direct
|
||||
relationship with the database ourselves we rely on gleaning Neutron's
|
||||
credentials from its config file.
|
||||
|
||||
:returns: SQLAlchemy consumable DB connection string.
|
||||
:rtype: str
|
||||
"""
|
||||
sections = {}
|
||||
parser = cfg.ConfigParser(NEUTRON_CONF, sections)
|
||||
parser.parse()
|
||||
return sections['database']['connection'][0]
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def write_filtered_neutron_config_for_sync_util():
|
||||
"""This helper exists to work around LP: #1894048.
|
||||
|
||||
Load neutron config and write out a copy with any sections or options
|
||||
offending the `neutron-ovn-db-sync-util` removed.
|
||||
|
||||
The helper should be used as a context manager to have the temporary config
|
||||
file removed when done. Example:
|
||||
|
||||
with write_filtered_neutron_config_for_sync_util():
|
||||
do_something()
|
||||
"""
|
||||
# Make sure the file we create has safe permissions
|
||||
stored_mask = os.umask(0o0027)
|
||||
try:
|
||||
with open(NEUTRON_CONF, 'r') as fin:
|
||||
with open(NEUTRON_OVN_DB_SYNC_CONF, 'w') as fout:
|
||||
for line in fin.readlines():
|
||||
# The ovn-db-sync-util chokes on this. LP: #1894048
|
||||
if line.startswith('auth_section'):
|
||||
continue
|
||||
fout.write(line)
|
||||
finally:
|
||||
# Restore umask for further execution regardless of any exception
|
||||
# occurring above.
|
||||
os.umask(stored_mask)
|
||||
|
||||
yield
|
||||
|
||||
# remove the temporary config file
|
||||
os.unlink(NEUTRON_OVN_DB_SYNC_CONF)
|
||||
|
||||
|
||||
def migrate_mtu(args):
|
||||
"""Reduce MTU on overlay networks prior to migration to Geneve.
|
||||
|
||||
:param args: Argument list
|
||||
:type args: List[str]
|
||||
"""
|
||||
action_name = os.path.basename(args[0])
|
||||
dry_run = not ch_core.hookenv.action_get('i-really-mean-it')
|
||||
mode = 'verify' if dry_run else 'update'
|
||||
cp = subprocess.run(
|
||||
(
|
||||
'neutron-ovn-migration-mtu',
|
||||
mode,
|
||||
'mtu',
|
||||
),
|
||||
capture_output=True,
|
||||
universal_newlines=True,
|
||||
env={
|
||||
'PATH': '/usr/bin',
|
||||
**get_neutron_credentials(),
|
||||
})
|
||||
if dry_run:
|
||||
banner_msg = '{}: OUTPUT FROM VERIFY'.format(action_name)
|
||||
else:
|
||||
banner_msg = '{}: OUTPUT FROM UPDATE'.format(action_name)
|
||||
|
||||
# we pass the output through and it will be captured both in log and
|
||||
# action output
|
||||
output_indicates_failure = False
|
||||
for output_name in ('stdout', 'stderr'):
|
||||
fh = getattr(sys, output_name)
|
||||
data = getattr(cp, output_name)
|
||||
print('{} ON {}:\n'.format(banner_msg, output_name.upper()) + data,
|
||||
file=fh)
|
||||
for fail_word in ('Exception', 'Traceback'):
|
||||
if fail_word in data:
|
||||
# the `neutron-ovn-migration-mtu` tool does not set an error
|
||||
# code on failure, look for errors in the output and set action
|
||||
# status accordingly.
|
||||
output_indicates_failure = True
|
||||
|
||||
if cp.returncode != 0 or output_indicates_failure:
|
||||
ch_core.hookenv.action_fail(
|
||||
'Execution failed, please investigate output.')
|
||||
|
||||
|
||||
def migrate_ovn_db(args):
|
||||
"""Migrate the Neutron DB into OVN with the `neutron-ovn-db-sync-util`.
|
||||
|
||||
:param args: Argument list
|
||||
:type args: List[str]
|
||||
"""
|
||||
action_name = os.path.basename(args[0])
|
||||
dry_run = not ch_core.hookenv.action_get('i-really-mean-it')
|
||||
sync_mode = 'log' if dry_run else 'repair'
|
||||
with write_filtered_neutron_config_for_sync_util():
|
||||
cp = subprocess.run(
|
||||
(
|
||||
'neutron-ovn-db-sync-util',
|
||||
'--config-file', NEUTRON_OVN_DB_SYNC_CONF,
|
||||
'--config-file', '/etc/neutron/plugins/ml2/ml2_conf.ini',
|
||||
'--ovn-neutron_sync_mode', sync_mode,
|
||||
),
|
||||
capture_output=True,
|
||||
universal_newlines=True,
|
||||
)
|
||||
if dry_run:
|
||||
banner_msg = '{}: OUTPUT FROM DRY-RUN'.format(action_name)
|
||||
else:
|
||||
banner_msg = '{}: OUTPUT FROM SYNC'.format(action_name)
|
||||
|
||||
# we pass the output through and it will be captured both in log and
|
||||
# action output
|
||||
output_indicates_failure = False
|
||||
for output_name in ('stdout', 'stderr'):
|
||||
fh = getattr(sys, output_name)
|
||||
data = getattr(cp, output_name)
|
||||
print('{} ON {}:\n'.format(banner_msg, output_name.upper()) + data,
|
||||
file=fh)
|
||||
if 'ERROR' in data:
|
||||
# the `neutron-ovn-db-sync-util` tool does not set an error code on
|
||||
# failure, look for errors in the output and set action status
|
||||
# accordingly.
|
||||
output_indicates_failure = True
|
||||
|
||||
if cp.returncode != 0 or output_indicates_failure:
|
||||
ch_core.hookenv.action_fail(
|
||||
'Execution failed, please investigate output.')
|
||||
|
||||
|
||||
def offline_neutron_morph_db(args):
|
||||
"""Perform offline moprhing of tunnel networks in the Neutron DB.
|
||||
|
||||
:param args: Argument list
|
||||
:type args: List[str]
|
||||
"""
|
||||
action_name = os.path.basename(args[0])
|
||||
dry_run = not ch_core.hookenv.action_get('i-really-mean-it')
|
||||
mode = 'dry' if dry_run else 'morph'
|
||||
cp = subprocess.run(
|
||||
(
|
||||
'{}'.format(
|
||||
os.path.join(
|
||||
ch_core.hookenv.charm_dir(),
|
||||
'files/scripts/neutron_offline_network_type_update.py')),
|
||||
get_neutron_db_connection_string(),
|
||||
mode,
|
||||
),
|
||||
capture_output=True,
|
||||
universal_newlines=True,
|
||||
# We want this tool to run outside of the charm venv to let it consume
|
||||
# system Python packages.
|
||||
env={'PATH': '/usr/bin'},
|
||||
)
|
||||
if dry_run:
|
||||
banner_msg = '{}: OUTPUT FROM DRY-RUN'.format(action_name)
|
||||
else:
|
||||
banner_msg = '{}: OUTPUT FROM MORPH'.format(action_name)
|
||||
|
||||
# we pass the output through and it will be captured both in log and
|
||||
# action output
|
||||
for output_name in ('stdout', 'stderr'):
|
||||
fh = getattr(sys, output_name)
|
||||
data = getattr(cp, output_name)
|
||||
print('{} ON {}:\n'.format(banner_msg, output_name.upper()) + data,
|
||||
file=fh)
|
||||
|
||||
if cp.returncode != 0:
|
||||
ch_core.hookenv.action_fail(
|
||||
'Execution failed, please investigate output.')
|
||||
|
||||
|
||||
ACTIONS = {
|
||||
'migrate-mtu': migrate_mtu,
|
||||
'migrate-ovn-db': migrate_ovn_db,
|
||||
'offline-neutron-morph-db': offline_neutron_morph_db,
|
||||
}
|
||||
|
||||
|
||||
def main(args):
|
||||
action_name = os.path.basename(args[0])
|
||||
try:
|
||||
action = ACTIONS[action_name]
|
||||
except KeyError:
|
||||
return 'Action {} undefined'.format(action_name)
|
||||
else:
|
||||
try:
|
||||
action(args)
|
||||
except Exception as e:
|
||||
ch_core.hookenv.log('action "{}" failed: "{}" "{}"'
|
||||
.format(action_name, str(e),
|
||||
traceback.format_exc()),
|
||||
level=ch_core.hookenv.ERROR)
|
||||
ch_core.hookenv.action_fail(str(e))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv))
|
1
src/actions/migrate-mtu
Symbolic link
1
src/actions/migrate-mtu
Symbolic link
@ -0,0 +1 @@
|
||||
actions.py
|
1
src/actions/migrate-ovn-db
Symbolic link
1
src/actions/migrate-ovn-db
Symbolic link
@ -0,0 +1 @@
|
||||
actions.py
|
1
src/actions/offline-neutron-morph-db
Symbolic link
1
src/actions/offline-neutron-morph-db
Symbolic link
@ -0,0 +1 @@
|
||||
actions.py
|
239
src/files/scripts/neutron_offline_network_type_update.py
Executable file
239
src/files/scripts/neutron_offline_network_type_update.py
Executable file
@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2020 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.
|
||||
|
||||
"""neutron_offline_network_type_update
|
||||
|
||||
The purpose of this module is to provide a tool that allow the user to perform
|
||||
Neutron database surgery to change the type of tunnel networks from 'gre' and
|
||||
'vxlan' to 'geneve'.
|
||||
|
||||
It is an optional part of a migration from a legacy Neutron ML2+OVS to ML2+OVN
|
||||
deployment.
|
||||
|
||||
At the time of this writing the Neutron OVN ML2 driver will assume that all
|
||||
chassis participating in a network to use the 'geneve' tunnel protocol and it
|
||||
will ignore the value of the `network_type` field in any non-physical network
|
||||
in the Neutron database. It will also ignore the `segmentation_id` field and
|
||||
let OVN assign the VNIs [0].
|
||||
|
||||
The Neutron API currently does not support changing the type of a network, so
|
||||
when doing a migration the above described behaviour is actually a welcomed
|
||||
one.
|
||||
|
||||
However, after the migration is done and all the primary functions are working,
|
||||
the end user of the cloud will be left with the false impression of their
|
||||
existing 'gre' or 'vxlan' typed networks still being operational on said tunnel
|
||||
protocols. In reality 'geneve' is used under the hood.
|
||||
|
||||
The end user will also run into issues with modifying any existing networks
|
||||
with `openstack network set` throwing error messages about networks of type
|
||||
'gre' or 'vxlan' not being supported.
|
||||
|
||||
After running this script said networks will have their `network_type` field
|
||||
changed to 'geneve' which will fix the above described problems.
|
||||
|
||||
NOTE: Use this script with caution, it is of absolute importance that the
|
||||
`neutron-server` process is stopped while the script is running.
|
||||
|
||||
NOTE: While we regularly exercise the script as part of our functional testing
|
||||
of the charmed migration path and the script is touching fundamental data
|
||||
structures that are not likely to have their definition changed much in
|
||||
the Neutron database, we would still advise you to take a fresh backup of
|
||||
the Neutron database and keep it for a while just in case.
|
||||
|
||||
0: https://github.com/ovn-org/ovn/blob/1e07781310d8155997672bdce01a2ff4f5a93e83/northd/ovn-northd.c#L1188-L1268
|
||||
""" # noqa
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from oslo_db.sqlalchemy import session
|
||||
|
||||
import sqlalchemy
|
||||
|
||||
|
||||
class NotFound(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def main(argv):
|
||||
"""Main function.
|
||||
|
||||
:param argv: Argument list
|
||||
:type argv: List[str]
|
||||
:returns: POSIX exit code
|
||||
:rtype: int
|
||||
"""
|
||||
program = os.path.basename(argv[0])
|
||||
if len(argv) < 2:
|
||||
usage(program)
|
||||
return os.EX_USAGE
|
||||
elif len(argv) < 3 or argv[2] != 'morph':
|
||||
print('DRY-RUN, WILL NOT COMMIT TRANSACTION')
|
||||
|
||||
db_engine = session.create_engine(argv[1])
|
||||
db_maker = session.get_maker(db_engine, autocommit=False)
|
||||
db_session = db_maker(bind=db_engine)
|
||||
|
||||
to_network_type = 'geneve'
|
||||
for network_type in ('gre', 'vxlan'):
|
||||
n_morphed = morph_networks(db_session, network_type, to_network_type)
|
||||
print('Morphed {} networks of type {} to {}.'
|
||||
.format(n_morphed, network_type, to_network_type))
|
||||
|
||||
if len(argv) < 3 or argv[2] != 'morph':
|
||||
print('DRY-RUN, WILL NOT COMMIT TRANSACTION')
|
||||
return os.EX_USAGE
|
||||
|
||||
db_session.commit()
|
||||
db_session.close()
|
||||
db_engine.dispose()
|
||||
return os.EX_OK
|
||||
|
||||
|
||||
def usage(program):
|
||||
"""Print information about how to use program.
|
||||
|
||||
:param program: Name of program
|
||||
:type program: str
|
||||
"""
|
||||
print('usage {} db-connection-string [morph]\n'
|
||||
'\n'
|
||||
'Morph non-physical networks of type "gre" and "vxlan" into '
|
||||
'geneve networks.\n'
|
||||
'\n'
|
||||
'The Neutron database must already have enough free "geneve" VNIs\n'
|
||||
'before running this tool. If the process stops because there are\n'
|
||||
'no more VNIs, increase the VNI range with the `vni_ranges`\n'
|
||||
'configuration option on the `ml2_type_geneve` section and then\n'
|
||||
'start and stop the neutron-server before trying again.\n'
|
||||
'\n'
|
||||
'The second argument must be the literal string "morph" for the\n'
|
||||
'tool to perform an action, otherwise it will not commit the\n'
|
||||
'transaction to the database, effectively performing a dry run.\n'
|
||||
''.format(program),
|
||||
file=sys.stderr)
|
||||
|
||||
|
||||
def allocate_segment(db_session, network_type):
|
||||
"""Allocate VNI for network_type.
|
||||
|
||||
:param db_session: SQLAlchemy DB Session object.
|
||||
:type db_session: SQLAlchemy DB Session object.
|
||||
:param network_type: Network type to allocate vni for.
|
||||
:type network_type: str
|
||||
:returns: Allocated VNI
|
||||
:rtype: int
|
||||
"""
|
||||
alloc_table = 'ml2_{}_allocations'.format(network_type)
|
||||
vni_row = '{}_vni'.format(network_type)
|
||||
|
||||
# Get next available VNI
|
||||
vni = None
|
||||
stmt = sqlalchemy.text(
|
||||
'SELECT MIN({}) FROM {} WHERE allocated=0'
|
||||
.format(vni_row, alloc_table))
|
||||
rs = db_session.execute(stmt)
|
||||
for row in rs:
|
||||
vni = next(row.itervalues())
|
||||
# A aggregated query will always provide a result, check for NULL
|
||||
if vni is None:
|
||||
raise NotFound(
|
||||
'unable to allocate "{}" segment.'.format(network_type))
|
||||
break
|
||||
|
||||
# Allocate VNI
|
||||
stmt = sqlalchemy.text(
|
||||
'UPDATE {} SET allocated=1 WHERE {}=:vni'.format(alloc_table, vni_row))
|
||||
db_session.execute(stmt, {'vni': vni})
|
||||
return vni
|
||||
|
||||
|
||||
def deallocate_segment(db_session, network_type, vni):
|
||||
"""Deallocate VNI for network_type.
|
||||
|
||||
:param db_session: SQLAlchemy DB Session object.
|
||||
:type db_session: SQLAlchemy DB Session object.
|
||||
:param network_type: Network type to de-allocate vni for.
|
||||
:type network_type: str
|
||||
:param vni: VNI
|
||||
:type vni: int
|
||||
"""
|
||||
alloc_table = 'ml2_{}_allocations'.format(network_type)
|
||||
vni_row = '{}_vni'.format(network_type)
|
||||
|
||||
# De-allocate VNI
|
||||
stmt = sqlalchemy.text(
|
||||
'UPDATE {} SET allocated=0 WHERE {}=:vni'.format(alloc_table, vni_row))
|
||||
db_session.execute(stmt, {'vni': vni})
|
||||
|
||||
|
||||
def get_network_segments(db_session, network_type):
|
||||
"""Get tunnel networks of certain type.
|
||||
|
||||
:param db_session: SQLAlchemy DB Session object.
|
||||
:type db_session: SQLAlchemy DB Session object.
|
||||
:param network_type: Network type to iterate over.
|
||||
:type network_type: str
|
||||
:returns: Iterator for data
|
||||
:rtype: Iterator[str,str,str,int]
|
||||
"""
|
||||
# Get networks
|
||||
stmt = sqlalchemy.text(
|
||||
'SELECT id,network_id,network_type,segmentation_id '
|
||||
'FROM networksegments '
|
||||
'WHERE physical_network IS NULL AND '
|
||||
' network_type=:network_type')
|
||||
rs = db_session.execute(stmt, {'network_type': network_type})
|
||||
for row in rs:
|
||||
yield row.values()
|
||||
|
||||
|
||||
def morph_networks(db_session, from_network_type, to_network_type):
|
||||
"""Morph all networks of one network type to another.
|
||||
|
||||
:param db_session: SQLAlchemy DB Session object.
|
||||
:type db_session: SQLAlchemy DB Session object.
|
||||
:param from_network_type: Network type to morph from.
|
||||
:type from_network_type: str
|
||||
:param to_network_type: Network type to morph to.
|
||||
:type to_network_type: str
|
||||
:returns: Number of networks morphed
|
||||
:rtype: int
|
||||
"""
|
||||
stmt = sqlalchemy.text(
|
||||
'UPDATE networksegments '
|
||||
'SET network_type=:new_network_type,segmentation_id=:new_vni '
|
||||
'WHERE id=:id')
|
||||
n_morphed = 0
|
||||
for segment_id, network_id, network_type, vni in get_network_segments(
|
||||
db_session, from_network_type):
|
||||
new_vni = allocate_segment(db_session, to_network_type)
|
||||
db_session.execute(stmt, {
|
||||
'new_network_type': to_network_type,
|
||||
'new_vni': new_vni,
|
||||
'id': segment_id,
|
||||
})
|
||||
print('segment {} for network {} changed from {}:{} to {}:{}'
|
||||
.format(segment_id, network_id, network_type, vni,
|
||||
to_network_type, new_vni))
|
||||
deallocate_segment(db_session, from_network_type, vni)
|
||||
n_morphed += 1
|
||||
return n_morphed
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv))
|
@ -1,6 +1,6 @@
|
||||
charm_name: neutron-api-plugin-ovn
|
||||
smoke_bundles:
|
||||
- bionic-ussuri
|
||||
- focal-ussuri
|
||||
gate_bundles:
|
||||
- bionic-train
|
||||
- bionic-ussuri
|
||||
|
@ -1 +1,2 @@
|
||||
zipp<2.0.0
|
||||
oslo_config
|
||||
|
@ -4,7 +4,7 @@
|
||||
# https://github.com/openstack-charmers/release-tools
|
||||
#
|
||||
# Lint and unit test requirements
|
||||
flake8>=2.2.4,<=2.4.1
|
||||
flake8>=2.2.4
|
||||
stestr>=2.2.0
|
||||
requests>=2.18.4
|
||||
charms.reactive
|
||||
|
@ -35,6 +35,8 @@ class _fake_decorator(object):
|
||||
|
||||
charms = mock.MagicMock()
|
||||
sys.modules['charms'] = charms
|
||||
charms.layer = mock.MagicMock()
|
||||
sys.modules['charms.layer'] = charms.layer
|
||||
charms.leadership = mock.MagicMock()
|
||||
sys.modules['charms.leadership'] = charms.leadership
|
||||
charms.reactive = mock.MagicMock()
|
||||
@ -60,3 +62,5 @@ sys.modules['charms.reactive.flags'] = charms.reactive.flags
|
||||
sys.modules['charms.reactive.relations'] = charms.reactive.relations
|
||||
netaddr = mock.MagicMock()
|
||||
sys.modules['netaddr'] = netaddr
|
||||
oslo_config = mock.MagicMock()
|
||||
sys.modules['oslo_config'] = oslo_config
|
||||
|
289
unit_tests/test_actions.py
Normal file
289
unit_tests/test_actions.py
Normal file
@ -0,0 +1,289 @@
|
||||
# Copyright 2020 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
|
||||
import unittest.mock as mock
|
||||
|
||||
sys.path.append('src')
|
||||
|
||||
import charms_openstack.test_utils as test_utils
|
||||
|
||||
import actions.actions as actions
|
||||
|
||||
|
||||
class FakeCalledProcess(object):
|
||||
|
||||
returncode = 0
|
||||
stdout = 'fake-output-on-stdout'
|
||||
stderr = 'fake-output-on-stderr'
|
||||
|
||||
|
||||
class TestActions(test_utils.PatchHelper):
|
||||
|
||||
def test_neutron_credentials(self):
|
||||
self.patch_object(actions.cfg, 'ConfigParser')
|
||||
parser = mock.MagicMock()
|
||||
self.maxDiff = None
|
||||
|
||||
expect = {
|
||||
'OS_USER_DOMAIN_NAME': 'fake-user-domain-name',
|
||||
'OS_PROJECT_DOMAIN_NAME': 'fake-project-domain-name',
|
||||
'OS_AUTH_URL': 'fake-auth-url',
|
||||
'OS_PROJECT_NAME': 'fake-project-name',
|
||||
'OS_USERNAME': 'fake-username',
|
||||
'OS_PASSWORD': 'fake-password',
|
||||
}
|
||||
|
||||
def _fakeparser(x, y):
|
||||
y.update(
|
||||
{
|
||||
'keystone_authtoken': {
|
||||
'user_domain_name': ['fake-user-domain-name'],
|
||||
'project_domain_name': ['fake-project-domain-name'],
|
||||
'auth_url': ['fake-auth-url'],
|
||||
'project_name': ['fake-project-name'],
|
||||
'username': ['fake-username'],
|
||||
'password': ['fake-password'],
|
||||
},
|
||||
})
|
||||
return parser
|
||||
|
||||
self.ConfigParser.side_effect = _fakeparser
|
||||
self.assertDictEqual(actions.get_neutron_credentials(), expect)
|
||||
self.ConfigParser.assert_called_once_with(
|
||||
'/etc/neutron/neutron.conf', mock.ANY)
|
||||
|
||||
def test_migrate_mtu(self):
|
||||
self.patch_object(actions.ch_core.hookenv, 'action_get')
|
||||
self.action_get.return_value = False
|
||||
self.patch_object(actions.subprocess, 'run')
|
||||
fcp = FakeCalledProcess()
|
||||
self.run.return_value = fcp
|
||||
self.patch_object(actions, 'get_neutron_credentials')
|
||||
self.get_neutron_credentials.return_value = {
|
||||
'fake-creds': 'from-neutron'}
|
||||
self.patch('builtins.print', name='builtin_print')
|
||||
self.patch_object(actions.ch_core.hookenv, 'action_fail')
|
||||
|
||||
actions.migrate_mtu(['/some/path/migrate-mtu'])
|
||||
self.run.assert_called_once_with(
|
||||
(
|
||||
'neutron-ovn-migration-mtu',
|
||||
'verify',
|
||||
'mtu',
|
||||
),
|
||||
capture_output=True,
|
||||
universal_newlines=True,
|
||||
env={
|
||||
'PATH': '/usr/bin',
|
||||
'fake-creds': 'from-neutron',
|
||||
})
|
||||
self.builtin_print.assert_has_calls([
|
||||
mock.call('migrate-mtu: OUTPUT FROM VERIFY ON STDOUT:\n'
|
||||
'fake-output-on-stdout',
|
||||
file=mock.ANY),
|
||||
mock.call('migrate-mtu: OUTPUT FROM VERIFY ON STDERR:\n'
|
||||
'fake-output-on-stderr',
|
||||
file=mock.ANY),
|
||||
])
|
||||
self.run.reset_mock()
|
||||
self.builtin_print.reset_mock()
|
||||
self.action_get.return_value = True
|
||||
actions.migrate_mtu(['/some/path/migrate-mtu'])
|
||||
self.run.assert_called_once_with(
|
||||
(
|
||||
'neutron-ovn-migration-mtu',
|
||||
'update',
|
||||
'mtu',
|
||||
),
|
||||
capture_output=True,
|
||||
universal_newlines=True,
|
||||
env={
|
||||
'PATH': '/usr/bin',
|
||||
'fake-creds': 'from-neutron',
|
||||
})
|
||||
self.builtin_print.assert_has_calls([
|
||||
mock.call('migrate-mtu: OUTPUT FROM UPDATE ON STDOUT:\n'
|
||||
'fake-output-on-stdout',
|
||||
file=mock.ANY),
|
||||
mock.call('migrate-mtu: OUTPUT FROM UPDATE ON STDERR:\n'
|
||||
'fake-output-on-stderr',
|
||||
file=mock.ANY),
|
||||
])
|
||||
# check that errors are detected
|
||||
fcp.returncode = 1
|
||||
actions.migrate_mtu(['/some/path/migrate-mtu'])
|
||||
self.action_fail.assert_called_once()
|
||||
fcp.returncode = 0
|
||||
self.action_fail.reset_mock()
|
||||
fcp.stderr = 'Traceback'
|
||||
actions.migrate_mtu(['/some/path/migrate-mtu'])
|
||||
self.action_fail.assert_called_once()
|
||||
self.action_fail.reset_mock()
|
||||
fcp.stderr = 'Exception'
|
||||
actions.migrate_mtu(['/some/path/migrate-mtu'])
|
||||
self.action_fail.assert_called_once()
|
||||
|
||||
def test_migrate_ovn_db(self):
|
||||
self.patch_object(actions.ch_core.hookenv, 'action_get')
|
||||
self.action_get.return_value = False
|
||||
self.patch_object(actions.subprocess, 'run')
|
||||
|
||||
fcp = FakeCalledProcess()
|
||||
self.run.return_value = fcp
|
||||
self.patch('builtins.print', name='builtin_print')
|
||||
self.patch_object(actions.ch_core.hookenv, 'action_fail')
|
||||
# NOTE: strictly speaking these really belong to a unit test for the
|
||||
# write_filtered_neutron_config_for_sync_util helper but since it
|
||||
# exists only to work around a bug let's just mock them here for
|
||||
# simplicity and remove it again when the bug is fixed.
|
||||
self.patch_object(actions.os, 'umask')
|
||||
self.patch_object(actions.os, 'unlink')
|
||||
|
||||
with mock.patch('builtins.open', create=True):
|
||||
actions.migrate_ovn_db(['/some/path/migrate-ovn-db'])
|
||||
self.run.assert_called_once_with(
|
||||
(
|
||||
'neutron-ovn-db-sync-util',
|
||||
'--config-file', '/etc/neutron/neutron-ovn-db-sync.conf',
|
||||
'--config-file', '/etc/neutron/plugins/ml2/ml2_conf.ini',
|
||||
'--ovn-neutron_sync_mode', 'log',
|
||||
),
|
||||
capture_output=True,
|
||||
universal_newlines=True,
|
||||
)
|
||||
self.builtin_print.assert_has_calls([
|
||||
mock.call('migrate-ovn-db: OUTPUT FROM DRY-RUN ON STDOUT:\n'
|
||||
'fake-output-on-stdout',
|
||||
file=mock.ANY),
|
||||
mock.call('migrate-ovn-db: OUTPUT FROM DRY-RUN ON STDERR:\n'
|
||||
'fake-output-on-stderr',
|
||||
file=mock.ANY),
|
||||
])
|
||||
self.run.reset_mock()
|
||||
self.builtin_print.reset_mock()
|
||||
self.action_get.return_value = True
|
||||
actions.migrate_ovn_db(['/some/path/migrate-ovn-db'])
|
||||
self.run.assert_called_once_with(
|
||||
(
|
||||
'neutron-ovn-db-sync-util',
|
||||
'--config-file', '/etc/neutron/neutron-ovn-db-sync.conf',
|
||||
'--config-file', '/etc/neutron/plugins/ml2/ml2_conf.ini',
|
||||
'--ovn-neutron_sync_mode', 'repair',
|
||||
),
|
||||
capture_output=True,
|
||||
universal_newlines=True,
|
||||
)
|
||||
self.builtin_print.assert_has_calls([
|
||||
mock.call('migrate-ovn-db: OUTPUT FROM SYNC ON STDOUT:\n'
|
||||
'fake-output-on-stdout',
|
||||
file=mock.ANY),
|
||||
mock.call('migrate-ovn-db: OUTPUT FROM SYNC ON STDERR:\n'
|
||||
'fake-output-on-stderr',
|
||||
file=mock.ANY),
|
||||
])
|
||||
# check that errors are detected
|
||||
fcp.returncode = 1
|
||||
actions.migrate_ovn_db(['/some/path/migrate-ovn-db'])
|
||||
self.action_fail.assert_called_once()
|
||||
fcp.returncode = 0
|
||||
self.action_fail.reset_mock()
|
||||
fcp.stderr = 'ERROR'
|
||||
actions.migrate_ovn_db(['/some/path/migrate-ovn-db'])
|
||||
self.action_fail.assert_called_once()
|
||||
|
||||
def test_get_neutron_db_connection_string(self):
|
||||
self.patch_object(actions.cfg, 'ConfigParser')
|
||||
parser = mock.MagicMock()
|
||||
self.maxDiff = None
|
||||
|
||||
def _fakeparser(x, y):
|
||||
y.update(
|
||||
{
|
||||
'database': {
|
||||
'connection': ['fake-connection'],
|
||||
},
|
||||
})
|
||||
return parser
|
||||
|
||||
self.ConfigParser.side_effect = _fakeparser
|
||||
self.assertEquals(
|
||||
actions.get_neutron_db_connection_string(), 'fake-connection')
|
||||
|
||||
def test_offline_neutron_morph_db(self):
|
||||
self.patch_object(actions.ch_core.hookenv, 'action_get')
|
||||
self.action_get.return_value = False
|
||||
self.patch_object(actions.subprocess, 'run')
|
||||
self.patch_object(actions.ch_core.hookenv, 'charm_dir')
|
||||
self.charm_dir.return_value = '/path/to/charm'
|
||||
self.patch_object(actions, 'get_neutron_db_connection_string')
|
||||
self.get_neutron_db_connection_string.return_value = 'fake-connection'
|
||||
|
||||
fcp = FakeCalledProcess()
|
||||
self.run.return_value = fcp
|
||||
self.patch('builtins.print', name='builtin_print')
|
||||
self.patch_object(actions.ch_core.hookenv, 'action_fail')
|
||||
|
||||
actions.offline_neutron_morph_db(
|
||||
['/some/path/offline-neutron-morph-db'])
|
||||
self.run.assert_called_once_with(
|
||||
(
|
||||
os.path.join(
|
||||
'/path/to/charm/',
|
||||
'files/scripts/neutron_offline_network_type_update.py'),
|
||||
'fake-connection',
|
||||
'dry',
|
||||
),
|
||||
capture_output=True,
|
||||
universal_newlines=True,
|
||||
env={'PATH': '/usr/bin'},
|
||||
)
|
||||
self.builtin_print.assert_has_calls([
|
||||
mock.call('offline-neutron-morph-db: OUTPUT FROM DRY-RUN ON '
|
||||
'STDOUT:\nfake-output-on-stdout',
|
||||
file=mock.ANY),
|
||||
mock.call('offline-neutron-morph-db: OUTPUT FROM DRY-RUN ON '
|
||||
'STDERR:\nfake-output-on-stderr',
|
||||
file=mock.ANY),
|
||||
])
|
||||
self.run.reset_mock()
|
||||
self.action_get.return_value = True
|
||||
actions.offline_neutron_morph_db(
|
||||
['/some/path/offline-neutron-morph-db'])
|
||||
self.run.assert_called_once_with(
|
||||
(
|
||||
os.path.join(
|
||||
'/path/to/charm/',
|
||||
'files/scripts/neutron_offline_network_type_update.py'),
|
||||
'fake-connection',
|
||||
'morph',
|
||||
),
|
||||
capture_output=True,
|
||||
universal_newlines=True,
|
||||
env={'PATH': '/usr/bin'},
|
||||
)
|
||||
self.builtin_print.assert_has_calls([
|
||||
mock.call('offline-neutron-morph-db: OUTPUT FROM MORPH ON '
|
||||
'STDOUT:\nfake-output-on-stdout',
|
||||
file=mock.ANY),
|
||||
mock.call('offline-neutron-morph-db: OUTPUT FROM MORPH ON '
|
||||
'STDERR:\nfake-output-on-stderr',
|
||||
file=mock.ANY),
|
||||
])
|
||||
# check that errors are detected
|
||||
fcp.returncode = 1
|
||||
actions.offline_neutron_morph_db(
|
||||
['/some/path/offline-neutron-morph-db'])
|
||||
self.action_fail.assert_called_once()
|
Loading…
Reference in New Issue
Block a user