Skip to content

Commit 28de6c4

Browse files
committed
Convert built-in plugins to module names
The built-in plugins are now structured as Python modules (for example, bgp.session plugin is in netsim/extra/bgp/session directory). That change required: * Additional plugin loading code (if the "traditional" plugin cannot be found, we try to load a module). As the 'package:extra' element has been removed from the search path, this should not impact the correct operation. * The device configuration templates are no longer in 'a.b' directory under 'extra' directory but in 'a/b' one. Extra elements with the 'custom_slash_config' variable were added to custom template file names to cope with that * Removal of additional mypy checking of 'plugin.py' files (the were all renamed to '__init__.py') Breaking change: * The 'proxy-arp' plugin has been renamed to 'proxy_arp'.
1 parent de5e089 commit 28de6c4

108 files changed

Lines changed: 94 additions & 37 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/t-pull.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ jobs:
3434
run: |
3535
set -e
3636
mypy -p netsim
37-
for file in netsim/extra/*/plugin.py; do
38-
python3 -m mypy $file
39-
done
4037
- name: Run transformation tests
4138
if: ${{ github.event.pull_request.head.repo.full_name != 'ipspace/netlab' }}
4239
run: |

.github/workflows/t-push.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ jobs:
2828
run: |
2929
set -e
3030
mypy -p netsim
31-
for file in netsim/extra/*/plugin.py; do
32-
python3 -m mypy $file
33-
done
3431
- name: Run transformation tests
3532
run: |
3633
cd tests

netsim/augment/plugin.py

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,19 @@
1616
from ..utils import sort as _sort
1717
from . import config
1818

19-
'''
20-
merge_plugin_defaults: Merge plugin defaults with topology defaults
21-
22-
We cannot simply overwrite the topology defaults with plugin defaults as the
23-
defaults might have been changed by the user, so we're using a dirty trick:
2419

25-
* Topology defaults are augmented with plugin defaults (which means the global
26-
defaults take precedence)
27-
* Whatever is really important should be in the 'important' dictionary and will
28-
take precedence over anything else
29-
'''
3020
def merge_plugin_defaults(defaults: typing.Optional[Box], topology: Box) -> None:
21+
'''
22+
merge_plugin_defaults: Merge plugin defaults with topology defaults
23+
24+
We cannot simply overwrite the topology defaults with plugin defaults as the
25+
defaults might have been changed by the user, so we're using a dirty trick:
26+
27+
* Topology defaults are augmented with plugin defaults (which means the global
28+
defaults take precedence)
29+
* Whatever is really important should be in the 'important' dictionary and will
30+
take precedence over anything else
31+
'''
3132
if not defaults:
3233
return
3334

@@ -41,7 +42,39 @@ def merge_plugin_defaults(defaults: typing.Optional[Box], topology: Box) -> None
4142
if important_stuff is not None:
4243
topology.defaults = topology.defaults + important_stuff
4344

45+
def load_plugin_defaults(dir_path: str, topology: Box) -> None:
46+
'''
47+
Given the path to plugin directory, read the plugin defaults
48+
'''
49+
defaults_file = dir_path + '/defaults.yml'
50+
if os.path.exists(defaults_file):
51+
merge_plugin_defaults(_read.read_yaml(defaults_file),topology)
52+
53+
def handle_plugin_redirects(p_module: object, p_name: str, topology: Box) -> typing.Optional[object]:
54+
'''
55+
Check whether a plugin redirects to another plugin. If it does,
56+
return the new plugin (and check it exists), otherwise return
57+
None (for no redirection)
58+
'''
59+
redirect = getattr(p_module,'_redirect',None)
60+
if not isinstance(redirect,str):
61+
return None
62+
63+
log.warning(
64+
text=f'Plugin {p_name} has been replaced by {redirect}. Please update your lab topology',
65+
module='plugins',
66+
flag='redirect')
67+
68+
topology.plugin = [ redirect if p == p_name else p for p in topology.plugin ]
69+
return load_plugin(redirect,topology)
70+
4471
def load_plugin_from_path(path: str, plugin: str, topology: Box) -> typing.Optional[object]:
72+
'''
73+
Try to load plugin from the specified path. It could be either a
74+
.py file, plugin.py within the 'plugin' subdirectory, or __init__.py
75+
within the same subdirectory. Subdirectory-based plugins can have
76+
system defaults which are also loaded.
77+
'''
4578
module_path = path+'/'+plugin
4679
if not os.path.exists(module_path):
4780
if os.path.exists(module_path+'.py'):
@@ -88,34 +121,66 @@ def load_plugin_from_path(path: str, plugin: str, topology: Box) -> typing.Optio
88121
print(f"Failed to load plugin {module_name} from {module_path}\nError reported by module loader: {str(sys.exc_info()[1])}")
89122
log.fatal('Aborting the transformation process','plugin')
90123

91-
redirect = getattr(pymodule,'_redirect',None)
92-
if isinstance(redirect,str):
93-
topology.plugin = [ redirect if p == plugin else p for p in topology.plugin ]
94-
return load_plugin_from_path(path,redirect,topology)
124+
r_module = handle_plugin_redirects(pymodule,plugin,topology)
125+
if r_module:
126+
return r_module
95127

96128
if config_name:
97129
setattr(pymodule,'config_name',config_name)
98130
setattr(pymodule,'_config_name',config_name)
99131

100132
if plugin_is_dir:
101-
defaults_file = dir_path + '/defaults.yml'
102-
if os.path.exists(defaults_file):
103-
merge_plugin_defaults(_read.read_yaml(defaults_file),topology)
133+
load_plugin_defaults(dir_path,topology)
104134

105135
if log.VERBOSE:
106136
print(f"loaded plugin {module_name}")
107137
return pymodule
108138

109-
'''
110-
Load plugin: try to load the plugin from any of the search path directories
111-
'''
139+
def load_plugin_module(pname: str, topology: Box) -> typing.Optional[object]:
140+
'''
141+
Try loading a netsim.extra module as a plugin.
142+
143+
Report loading errors (apart from "no module" which means the user used an
144+
incorrect plugin name) and handle the usual plugin processing (redirects,
145+
config_name, defaults)
146+
'''
147+
try:
148+
pymodule = importlib.import_module(f'netsim.extra.{pname}')
149+
except ModuleNotFoundError:
150+
return None
151+
except Exception as ex:
152+
log.error(
153+
f"Failed to load netsim.extra.{pname} module",
154+
more_data=f"Error reported by module loader: {str(ex)}",
155+
category=log.FatalError,
156+
module='plugin')
157+
log.exit_on_error()
158+
159+
r_module = handle_plugin_redirects(pymodule,pname,topology)
160+
if r_module:
161+
return r_module
162+
163+
setattr(pymodule,'config_name',pname)
164+
setattr(pymodule,'_config_name',pname)
165+
mod_file = pymodule.__file__
166+
if mod_file:
167+
load_plugin_defaults(os.path.dirname(mod_file),topology)
168+
169+
if log.VERBOSE:
170+
print(f"loaded module netsim.extra.{pname} as plugin")
171+
return pymodule
172+
112173
def load_plugin(pname: str,topology: Box) -> typing.Optional[object]:
174+
'''
175+
Load plugin: try to load the plugin from any of the search path directories.
176+
Failing that, try to load a netsim.extra module
177+
'''
113178
for path in topology.defaults.paths.plugin:
114179
plugin = load_plugin_from_path(path,pname,topology) # Try to load plugin from the current search path directory
115180
if plugin: # Got it, get out of the loop
116181
return plugin
117182

118-
return None
183+
return load_plugin_module(pname,topology)
119184

120185
'''
121186
Sort plugins based on their _requires and _execute_after attributes

netsim/defaults/paths.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ plugin: # Search path for transformation plugins
1111
- "topology:"
1212
- "~/.netlab"
1313
- "/etc/netlab"
14-
- "package:extra"
14+
# - "package:extra" # Internal plugins are loaded as netsim modules
1515

1616
custom: # Custom configuration templates
1717
dirs: # ... search directories
@@ -34,6 +34,10 @@ custom: # Custom configuration templates
3434
- "{{custom_config}}.{{inventory_hostname}}.j2"
3535
- "{{custom_config}}.{{netlab_device_type}}.j2"
3636
- "{{custom_config}}.{{ansible_network_os}}.j2"
37+
- "{{custom_slash_config}}/{{netlab_device_type}}-{{node_provider}}.j2"
38+
- "{{custom_slash_config}}/{{netlab_device_type}}.j2"
39+
- "{{custom_slash_config}}/{{ansible_network_os}}-{{node_provider}}.j2"
40+
- "{{custom_slash_config}}/{{ansible_network_os}}.j2"
3741
- "{{custom_config}}.j2"
3842
tasks:
3943
- "{{ custom_config }}/deploy-{{ inventory_hostname }}.yml"

0 commit comments

Comments
 (0)