Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# Sphinx
_build
doc/source/api/
doc/source/contributor/api/

# release notes build
releasenotes/build
Expand Down
43 changes: 42 additions & 1 deletion devstack/plugin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,14 @@ function configure_generic_switch {
done
fi
fi
# NOTE(TheJulia): This is not respected presently with uwsgi launched
# neutron as it auto-identifies it's configuration files.
neutron_server_config_add $GENERIC_SWITCH_INI_FILE

# NOTE(JayF): It's possible in some rare cases this config doesn't exist
# if so, no big deal, iniset is used in devstack and should
# not lose our changes. See `write_uwsgi_config` in lib/apache
iniset -sudo /etc/neutron/neutron-api-uwsgi.ini uwsgi env OS_NEUTRON_CONFIG_FILES='/etc/neutron/neutron.conf;/etc/neutron/plugins/ml2/ml2_conf.ini;/etc/neutron/plugins/ml2/ml2_conf_genericswitch.ini'
}

function add_generic_switch_to_ml2_config {
Expand Down Expand Up @@ -241,6 +247,26 @@ function ngs_configure_tempest {
fi
}

function ngs_restart_neutron {
echo_summary "NGS doing required neutron restart. Stopping neutron."
# NOTE(JayF) In practice restarting OVN causes problems, I'm not sure why.
# This avoids the restart.
local existing_skip_stop_ovn
SKIP_STOP_OVN=True
# We are changing the base config, and need ot restart the neutron services
stop_neutron
# NOTE(JayF): Neutron services are initialized in a particular order, this appears to
# match that order as currently defined in stack.sh (2025-05-22).
# TODO(JayF): Introduce a function in upstream devstack that documents this order so
# ironic won't break anytime initialization steps are rearranged.
echo_summary "NGS starting neutron service"
start_neutron_service_and_check
echo_summary "NGS started neutron service, now launch neutron agents"
start_neutron
echo_summary "NGS required neutron restart completed."
SKIP_STOP_OVN=False
}

# check for service enabled
if is_service_enabled generic_switch; then

Expand All @@ -250,7 +276,7 @@ if is_service_enabled generic_switch; then
install_generic_switch

elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then
# Configure after the other layer 1 and 2 services have been configured
# Configure after the other layer 1 and 2 services have been started
echo_summary "Configuring Generic_switch ML2"

# Source ml2 plugin, set default config
Expand All @@ -262,7 +288,22 @@ if is_service_enabled generic_switch; then
Q_PLUGIN_CLASS="ml2"
fi

# TODO(JayF): This currently relies on winning a race, as many of the
# files modified by this method are created during this
# phase. In practice it works, but moving forward we likely
# need a supported-by-devstack/neutron-upstream method to
# ensure this is done at the right moment.
configure_generic_switch

if is_service_enabled neutron; then
# TODO(JayF): Similarly, we'd like to restart neutron to ensure
# our config changes have taken effect; we can't do
# that reliably here because it may not be fully
# configured, and extra phase is too late.
echo_summary "Skipping ngs_restart_neutron"
#ngs_restart_neutron
fi

elif [[ "$1" == "stack" && "$2" == "test-config" ]]; then
if is_service_enabled tempest; then
echo_summary "Configuring Tempest NGS"
Expand Down
150 changes: 150 additions & 0 deletions doc/source/_exts/netmiko_device_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import ast
import inspect
import stevedore

from docutils import nodes
from docutils.parsers import rst
from docutils.statemachine import ViewList
from sphinx.util.nodes import nested_parse_with_titles

command_descriptions = {
'ADD_NETWORK': 'A tuple of command strings used to add a VLAN',
'DELETE_NETWORK': 'A tuple of command strings used to delete a VLAN',
'PLUG_PORT_TO_NETWORK': 'A tuple of command strings used to configure a \
port to connect to a specific VLAN',
'DELETE_PORT': 'A tuple of command strings used to remove a port from the\
VLAN',
'NETMIKO_DEVICE_TYPE': 'Netmiko Supported device type',
'ADD_NETWORK_TO_TRUNK': 'Adds a network to a trunk port.',
'REMOVE_NETWORK_FROM_TRUNK': 'Removes a network from a trunk port.',
'ENABLE_PORT': 'Enables the port',
'DISABLE_PORT': 'Shuts down the port',
'ERROR_MSG_PATTERNS': 'A tuple of regular expressions. These patterns are\
used to match and handle error messages returned by the switch.',
'SET_NATIVE_VLAN': 'Sets a specified native VLAN',
'DELETE_NATIVE_VLAN': 'Removes the native VLAN',
'SAVE_CONFIGURATION': 'Saves the configuration',
'SET_NATIVE_VLAN_BOND': 'Sets the native VLAN for the bond interface',
'DELETE_NATIVE_VLAN_BOND': 'Unsets the native VLAN for the bond \
interface',
'ADD_NETWORK_TO_BOND_TRUNK': 'Adds a VLAN to the bond interface for \
trunking',
'DELETE_NETWORK_ON_BOND_TRUNK': 'Removes a VLAN from the bond interface \
for trunking',
'PLUG_PORT_TO_NETWORK_GENERAL': 'Allows the VLAN and lets it carry \
untagged frames',
'DELETE_PORT_GENERAL': 'Removes VLAN from allowed list and stops allowing\
it to carry untagged frames',
'QUERY_PORT': 'Shows details about the switch for that port',
'PLUG_BOND_TO_NETWORK': 'Adds bond to the bridge as a port for the VLAN',
'UNPLUG_BOND_FROM_NETWORK': 'Removes bond\'s access VLAN assignment',
'ENABLE_BOND': 'Enables bond interface by removing link down state',
'DISABLE_BOND': 'Disables bond interface by setting its link state to \
down',
}


class DeviceCommandsDirective(rst.Directive):

def parse_tuples(value):
"""Parses the value in the tuples and returns a list of its contents"""
tuple_values = []
for elt in value.elts:
# Parsing if the item in the tuple is a function call
if isinstance(elt, ast.Call):
func_name = ''
if isinstance(elt.func, ast.Attribute):
func_name = f"{ast.unparse(elt.func.value)}.{elt.func.attr}"
elif isinstance(elt.func, ast.Name):
func_name = elt.func.id
args = [ast.literal_eval(arg) for arg in elt.args]
call_command = str(func_name) + '(\'' + str(args[0]) + '\')'
tuple_values.append(call_command)

else:
tuple_values.append(ast.literal_eval(elt))
return tuple_values

def parse_file(file_content, filename):
"""Uses ast to split document body into nodes and parse them"""
tree = ast.parse(file_content, filename=filename)
classes = {}
for node in tree.body:
if isinstance(node, ast.ClassDef):
device_name = node.name
cli_commands = {}
docstring = ast.get_docstring(node)

if docstring:
cli_commands['__doc__'] = docstring
# Iterates through nodes, checks for type of node and extracts the value
for subnode in node.body:
if isinstance(subnode, ast.Assign):
for target in subnode.targets:
command_name = target.id
if isinstance(target, ast.Name):
ast_type = subnode.value
if isinstance(ast_type, ast.Tuple):
cli_commands[command_name] = DeviceCommandsDirective.parse_tuples(ast_type)
else:
cli_commands[command_name] = ast.literal_eval(ast_type)
if cli_commands:
classes[device_name] = cli_commands
return classes

def format_output(switch_details):
"""Formats output that is to be displayed"""
formatted_output = ViewList()
if '__doc__' in switch_details:
for line in switch_details['__doc__'].splitlines():
formatted_output.append(f" {line}", "")
formatted_output.append("", "")
del switch_details['__doc__']
for command_name, cli_commands in switch_details.items():
desc = command_descriptions.get(command_name, 'No description provided')
formatted_output.append(f" - {command_name}: {desc}", "")
formatted_output.append(f" - CLI commands:", "")
if isinstance(cli_commands, list):
if cli_commands:
for command in cli_commands:
formatted_output.append(f" - {command}", "")
else:
formatted_output.append(f" - No cli commands for this switch command", "")
else:
formatted_output.append(f" - {cli_commands}", "")
return formatted_output

def run(self):
"""Loads the files, parses them and formats the output"""
manager = stevedore.ExtensionManager(
namespace='generic_switch.devices',
invoke_on_load=False,
)
output_lines = ViewList()
output_lines.append("Switches", "")
output_lines.append("========", "")

for file_loader in manager.extensions:
switch = file_loader.plugin
module = inspect.getmodule(switch)
file_content = inspect.getsource(module)
filename = module.__file__
parsed_device_file = DeviceCommandsDirective.parse_file(file_content, filename)
switch_name = switch.__name__
output_lines.append(f"{switch_name}:", "")
subheading_characters = "^"
subheading = subheading_characters * (len(switch_name) + 1)
output_lines.append(subheading, "")

if switch_name in parsed_device_file:
switch_details = parsed_device_file[switch_name]
output_lines.extend(DeviceCommandsDirective.format_output(switch_details))
output_lines.append("", "")

node = nodes.section()
node.document = self.state.document
nested_parse_with_titles(self.state, output_lines, node)
return node.children

def setup(app):
app.add_directive('netmiko-device-commands', DeviceCommandsDirective)
5 changes: 4 additions & 1 deletion doc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
import sys

sys.path.insert(0, os.path.abspath('../..'))
sys.path.insert(0, os.path.join(os.path.abspath('.'), '_exts'))
# -- General configuration ----------------------------------------------------

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'sphinxcontrib.apidoc',
'openstackdocstheme'
'openstackdocstheme',
'netmiko_device_commands'
]

# openstackdocstheme options
Expand Down Expand Up @@ -85,4 +87,5 @@
apidoc_output_dir = 'contributor/api'
apidoc_excluded_paths = [
'tests',
'devices/netmiko_devices',
]
Loading