docs: explain traffic rules migration path (from OpenFlow Protocol to XAPI Plugin) (#9999)

* fix(configuration.md): remove trailing spaces

* feat(sdn-controller.md): explain traffic rules migration path: from OpenFlow Protocol to XAPI Plugin

- add sdn-migration.py python script
- in configuration.md, refer to sdn_controller.md for changing `useDirectChannel`
- in sdn_controller.md, provide documentation on migration path
This commit is contained in:
Sebastien Marie
2026-06-24 08:53:34 +02:00
committed by GitHub
parent 8f31ff7481
commit a6f2a25aec
3 changed files with 260 additions and 3 deletions

View File

@@ -43,11 +43,11 @@ If changing a user-specific configuration, make sure the file belongs to the use
Place your `config.toml` (or any `config.*.toml` override) in one of the following:
- **System-wide configuration**:
`/etc/xo-server/config.toml`
- **System-wide configuration**:
`/etc/xo-server/config.toml`
(or any file matching `config.<name>.toml`)
- **User-specific configuration**:
- **User-specific configuration**:
`~/.config/xo-server/config.toml`
The order of the loading and overriding values in multiple config files is the following:
@@ -151,6 +151,10 @@ For this kind of setting, we recommend using something like `/etc/xo-server/conf
useDirectChannel = false
```
Changing the mode is only supported forward : from direct OpenFlow Protocol channel to XAPI Plugin communication.
A migration script is provided to automatically convert the traffic rules.
See [XO 5 > Management > SDN Controller > OpenFlow rules](sdn_controller#migration-path) for details.
## Custom certificate authority
If you use certificates signed by an in-house CA for your XCP-ng or XenServer hosts, and want to have Xen Orchestra connect to them without rejection, you can use the [`NODE_EXTRA_CA_CERTS`](https://nodejs.org/api/cli.html#cli_node_extra_ca_certs_file) environment variable.

View File

@@ -162,6 +162,40 @@ xo-cli sdnController.addNetworkRule networkId=9334aa83-6960-62e5-a463-5acc05295a
xo-cli sdnController.deleteNetworkRule networkId=9334aa83-6960-62e5-a463-5acc05295af4 direction=from ipRange=1.1.1.2 protocol=tcp port=json:4242
```
### Migration path
OpenFlowProtocol and XAPIPlugin modes implement traffic rules in slightly different ways.
Because of this mismatch, the two modes are not directly interchangeable and you must either
1. Recreate the rules from scratch, or
2. Use the provided migration script to translate an existing set of rules.
:::note
The migration is forward only.
You can go from OpenFlowProtocol to XAPIPlugin in a semiautomatic manner, but to revert existing rules will be deleted first.
:::
#### What the migration script does
- Back up the current rules in the OpenFlowProtocol format in `rules-channel.json` file.
- Translate those rules into the XAPIPlugin format.
If you ever need to go back to the original configuration, the same script can restore the rules from the backup.
Existing rules will first be removed before restoring the saved ones.
#### How to use the script
The migration script is available as [sdn-migration.py (download script)](../../static/sh/sdn-migration.py).
```sh
# Export current rules to rules-channel.json and convert to XAPI format
python3 sdn-migration.py migrate
# (Optional) Restore original rules (from rules-channel.json file)
python3 sdn-migration.py restore
```
## OpenSSL 3 + SDN upgrade path
With OpenSSL 3 arriving in XCP-ng 8.3, the certificates previously generated by the Xen Orchestra SDN Controller plugin will no longer work. They will need to be regenerated. We recommend you take the time to handle this as soon as possible, so youre ready for the XCP-ngs March 2026 update.

219
docs/static/sh/sdn-migration.py vendored Normal file
View File

@@ -0,0 +1,219 @@
#!/usr/bin/env python3
from __future__ import print_function
import json
from datetime import datetime
import XenAPI
OpaqueRef = str
# Tool informations
TOOL_NAME = 'traffic-rules-migration-tool'
TOOL_VERSION = '1.0'
# rules backup filename
RULES_BACKUP_FILENAME = 'rules-channel.json'
# pool.other_config keys
XO_SDN_CONTROLLER_OF_METHOD = 'xo:sdn-controller:of-method'
XO_SDN_CONTROLLER_OF_FORMAT = 'xo:sdn-controller:of-format'
XO_SDN_CONTROLLER_OF_RULES = 'xo:sdn-controller:of-rules'
# --------------------------------------------------------------------
def xapi_login():
"""
Return XenAPI Session, connected from local endpoint.
"""
import atexit
# get local xapi session
session = XenAPI.xapi_local()
# register logout
def logout():
import contextlib
with contextlib.suppress(Exception):
session.xenapi.session.logout()
atexit.register(logout)
# login to XAPI
session.xenapi.login_with_password('', '', TOOL_VERSION, TOOL_NAME)
return session
# --------------------------------------------------------------------
def get_pool(session):
"""
Return the current pool.
"""
pools = session.xenapi.pool.get_all()
assert len(pools) == 1, 'Too many pools returned: {}'.format(pools)
return pools[0]
def get_pool_name(session, pool):
"""
Return the pool name, or the master name if empty (like XO).
"""
pool_name = session.xenapi.pool.get_name_label(pool)
if pool_name == '':
master = session.xenapi.pool.get_master(pool)
return session.xenapi.host.get_name_label(master)
else:
return pool_name
# --------------------------------------------------------------------
def get_of_method(other_config):
"""
Get the pool.other_config['xo:sdn-controller:of-method'] property.
If it doesn't exist, default to "channel".
"""
return other_config.get(XO_SDN_CONTROLLER_OF_METHOD, 'channel')
def get_of_format(other_config):
"""
Get the pool.other_config['xo:sdn-controller:of-format'] property.
If it doesn't exist, default to of_method() value.
"""
return other_config.get(XO_SDN_CONTROLLER_OF_FORMAT) or get_of_method(other_config)
# --------------------------------------------------------------------
def deserialize_of_rules(data):
"""
Deserialize of-rules (convert it from string to python).
"""
return [json.loads(r) for r in json.loads(data)]
def serialize_of_rules(data):
"""
Serialize of-rules (convert it from python to string).
"""
return json.dumps([json.dumps(rule) for rule in data])
# --------------------------------------------------------------------
def usage(error):
from sys import exit
print('migration.py migrate\n do \'channel\' -> \'xapi-plugin\' migration, keeping local file "{}" with original rules.\n\nmigration.py restore\n do \'xapi-plugin\' -> \'channel\' restoration using the "{}" file.\n (rules changes will be lost and restored from the backup files).\n'.format(RULES_BACKUP_FILENAME, RULES_BACKUP_FILENAME))
exit(0 if not error else 1)
# --------------------------------------------------------------------
def channel_to_xapiplugin(channel_rules):
"""
Convert 'channel' semantic to 'xapi-channel' semantic.
"""
xapiplugin_rules = []
try:
for crule in channel_rules:
if crule['protocol'] in ['IP', 'ARP']:
if crule['direction'] in ['from', 'from/to']:
xapiplugin_rules.append({'direction': 'to', 'protocol': crule['protocol'], 'ipRange': crule['ipRange'], 'allow': crule['allow']})
if crule['direction'] in ['to', 'from/to']:
xapiplugin_rules.append({'direction': 'from', 'protocol': crule['protocol'], 'ipRange': crule['ipRange'], 'allow': crule['allow']})
elif crule['protocol'] in ['ICMP']:
xapiplugin_rules.append({'direction': 'from', 'protocol': crule['protocol'], 'ipRange': crule['ipRange'], 'allow': crule['allow']})
xapiplugin_rules.append({'direction': 'to', 'protocol': crule['protocol'], 'ipRange': crule['ipRange'], 'allow': crule['allow']})
elif crule['protocol'] in ['TCP', 'UDP']:
# not bug-to-bug compliant: 'channel' is mixing ip source and port destination here
xapiplugin_rules.append({'direction': 'from', 'protocol': crule['protocol'], 'ipRange': crule['ipRange'], 'port': crule['port'], 'allow': crule['allow']})
xapiplugin_rules.append({'direction': 'to', 'protocol': crule['protocol'], 'ipRange': crule['ipRange'], 'port': crule['port'], 'allow': crule['allow']})
else:
assert False, 'unrecognized protocol: {}'.format(crule)
except KeyError as e:
print('KeyError while using {}'.format(crule))
raise e
# deduplicate xapiplugin_rules elements
return [dict(t) for t in {tuple(d.items()) for d in xapiplugin_rules}]
# --------------------------------------------------------------------
def do_migrate(session):
# check the of-format of the pool
pool = get_pool(session)
pool_other_config = session.xenapi.pool.get_other_config(pool)
of_format = get_of_format(pool_other_config)
if of_format == 'xapi-plugin':
print("warn: pool '{}' is already migrated to 'xapi-plugin', skipping".format(get_pool_name(session, pool)))
return
# get of-rules in the pool
channel_rules = [{'tool': TOOL_NAME, 'version': TOOL_VERSION, 'date': datetime.now().strftime('%a, %d %b %Y %T %z')}]
xapiplugin_rules = []
# (we only process VIF (and not Network) as 'channel' can only do VIF rules)
for vif in session.xenapi.VIF.get_all():
vif_other_config = session.xenapi.VIF.get_other_config(vif)
if XO_SDN_CONTROLLER_OF_RULES in vif_other_config:
# keep the rules to backup
channel_rules.append({'uuid': session.xenapi.VIF.get_uuid(vif), 'of-rules': vif_other_config.get(XO_SDN_CONTROLLER_OF_RULES)})
# channel->xapiplugin
# update other_config dict, but do not write it now.
vif_other_config[XO_SDN_CONTROLLER_OF_RULES] = serialize_of_rules(channel_to_xapiplugin(deserialize_of_rules(vif_other_config.get(XO_SDN_CONTROLLER_OF_RULES))))
# keep it for later
xapiplugin_rules.append((vif, vif_other_config))
# backup: open the file for writing and error out if it already exists
with open(RULES_BACKUP_FILENAME, 'x') as backup_fp:
json.dump(channel_rules, backup_fp, indent=2)
# To avoid inconsistencies in xapi database, any exception after this point
# will lead to reverting to the backup.
try:
# write updated rules to xapi
for vif, vif_other_config in xapiplugin_rules:
session.xenapi.VIF.set_other_config(vif, vif_other_config)
# update of-format to 'xapi-plugin'
pool_other_config[XO_SDN_CONTROLLER_OF_FORMAT] = 'xapi-plugin'
session.xenapi.pool.set_other_config(pool, pool_other_config)
print("pool '{}' successfully migrated to 'xapi-plugin' format".format(get_pool_name(session, pool)))
except Exception as e:
print('error: an unexpected exception occured, reverting any changes...')
do_restore(session, force=True)
print('restore done, raising the original exception:')
raise e
# --------------------------------------------------------------------
def do_restore(session, force=False):
# check the of-format of the pool
pool = get_pool(session)
pool_other_config = session.xenapi.pool.get_other_config(pool)
of_format = get_of_format(pool_other_config)
if of_format == 'channel' and (not force):
print("warn: pool '{}' is already using 'channel', skipping".format(get_pool_name(session, pool)))
return
# read the backup file
with open(RULES_BACKUP_FILENAME, 'r') as backup_fp:
channel_rules = json.load(backup_fp)
# restore the rules
restored = set()
for crule in channel_rules:
if 'tool' in crule:
print('restoring data from {} {} at {}'.format(crule['tool'], crule['version'], crule['date']))
continue
# check the VIF still exists
try:
vif = session.xenapi.VIF.get_by_uuid(crule['uuid'])
except XenAPI.Failure as e:
if e.details[0] == 'UUID_INVALID':
vif = None
else:
raise e
if vif is None:
print("warn: no more VIF '{}', skipping it.".format(crule['uuid']))
continue
# set of-rules
vif_other_config = session.xenapi.VIF.get_other_config(vif)
vif_other_config[XO_SDN_CONTROLLER_OF_RULES] = crule['of-rules']
session.xenapi.VIF.set_other_config(vif, vif_other_config)
restored.add(vif)
# remove all others of-rules (which was created after the backup)
for vif in session.xenapi.VIF.get_all():
if vif in restored:
continue
vif_other_config = session.xenapi.VIF.get_other_config(vif)
if XO_SDN_CONTROLLER_OF_RULES in vif_other_config:
del vif_other_config[XO_SDN_CONTROLLER_OF_RULES]
session.xenapi.VIF.set_other_config(vif, vif_other_config)
# set of-format to 'channel'
pool_other_config[XO_SDN_CONTROLLER_OF_FORMAT] = 'channel'
session.xenapi.pool.set_other_config(pool, pool_other_config)
# --------------------------------------------------------------------
if __name__ == '__main__':
from sys import argv
if len(argv) == 1:
usage(error=False)
elif len(argv) > 2:
usage(error=True)
elif argv[1] == 'migrate':
session = xapi_login()
do_migrate(session)
elif argv[1] == 'restore':
session = xapi_login()
do_restore(session)
else:
usage(error=True)