diff --git a/pnp/CHANGELOG b/pnp/CHANGELOG index b0049a34a7a503a2522d31837c3d2c870c25ebfb..ee9bdac21cdece27ec4b1f773db890d65c1a0cbf 100644 --- a/pnp/CHANGELOG +++ b/pnp/CHANGELOG @@ -19,4 +19,8 @@ 2023-02-23: added cli option -v/--version, --default_cfg 2023-02-26: reorganized open-pnp.py in to open_pnp_classes.py and open_pnp_utils.py 2023-32-07: changed '_' in cli options to '-' -> better readable/more bash like -2023-03-13: fixed default-cfg option, added --no-default-cfg option, moved error codes in status.html in to debug section +2023-03-13: fixed default-cfg option, added --no-default-cfg option, moved error codes in status.html to the debug section +2023-04-11: added flask options to remove whitespaces by default, changed version format (major.minor.micro-date) + changed to load settings from config file (open-pnp.toml by default) in the SETTINGS.__init__ + don't exit if cfg_file not found (not strictly needed, can all set viy cli), just print a warning + fixed: --config-file option (was cfg_file instead of config-file) \ No newline at end of file diff --git a/pnp/open-pnp.py b/pnp/open-pnp.py index 34f7c98f724f2155216a0a4b9802106ba873d4ca..6e622746bf8c08eaf95d7a683f722b433e46fe38 100755 --- a/pnp/open-pnp.py +++ b/pnp/open-pnp.py @@ -3,17 +3,21 @@ # # License: GNU General Public License v2 # + # Author: thl-cmk[at]outlook[dot]com # URL : https://thl-cmk.hopto.org # Date : 2022-12-10 # File : open-pnp.py + # # Basic Cisco PnP server for Day0 provisioning # -# based on https://github.com/oliverl-21/Open-PnP-Server -# -# Cisco doc on https://developer.cisco.com/site/open-plug-n-play/learn/learn-open-pnp-protocol/ -# + +# Based on : https://github.com/oliverl-21/Open-PnP-Server +# Cisco doc at : https://developer.cisco.com/site/open-plug-n-play/learn/learn-open-pnp-protocol/ +# TOML doc at : https://toml.io/en/ +# Jinja2 doc at : https://jinja.palletsprojects.com/en/3.0.x/templates/ + # 2022-12-14: added count in status page # added check if image/config file is available # 2022-12-15: renamed ./vars/vars.py to ./vars/settings.py @@ -36,11 +40,14 @@ # 2023-02-26: reorganized open-pnp.py in to open_pnp_classes.py and open_pnp_utils.py # 2023-32-07: changed '_' in cli options to '-' -> better readable/more bash like # 2023-03-13: added '--no-default-cfg' option +# 2023-04-11: added flask options to remove whitespaces by default, changed version format (major.minor.micro-date) + # # pip install flask xmltodict requests ifaddr tomli # + # ToDo: -# add remove inactive job on IOS-XE devices if no space for image update +# add "install remove inactive" images job on IOS-XE devices if no space for image update # system libs # from re import compile as re_compile @@ -78,7 +85,7 @@ from open_pnp_utils import ( ) -PNP_SERVER_VERSION = '20230313.v1.0.3' +PNP_SERVER_VERSION = '1.0.4-202300411' def pnp_device_info(udi: str, correlator: str, info_type: str) -> str: @@ -289,6 +296,8 @@ def check_update(udi: str): # flask app = Flask(__name__, template_folder='./templates') +app.jinja_env.lstrip_blocks = True +app.jinja_env.trim_blocks = True # removes empty lines, i.e. in loops @app.route('/') @@ -320,7 +329,7 @@ def buttons(): if button == 'Reload CFG': IMAGES.load_image_data(SETTINGS.image_data) - SETTINGS.update(SETTINGS.cfg_file) + SETTINGS.update(SETTINGS.config_file) if udi in devices.keys(): device = devices[udi] @@ -458,11 +467,13 @@ def pnp_work_response(): if __name__ == '__main__': + # clear screen + print("\033c\033[3J", end='') ERROR = ErrorCodes() PNPFLOW = PnpFlow() SETTINGS = Settings(vars(parse_arguments(PNP_SERVER_VERSION))) - SETTINGS.update(SETTINGS.cfg_file) + # SETTINGS.update(SETTINGS.cfg_file) if SETTINGS.version: print(PNP_SERVER_VERSION) @@ -480,10 +491,10 @@ if __name__ == '__main__': cli.show_server_banner = lambda *args: None if SETTINGS.image_url == '': - print(f'image-url not set, check {SETTINGS.cfg_file} or see open-pnp.py -h') + print(f'image-url not set, check {SETTINGS.config_file} or see open-pnp.py -h') exit(1) if SETTINGS.config_url == '': - print(f'config-url not set, check {SETTINGS.cfg_file} or see open-pnp.py -h') + print(f'config-url not set, check {SETTINGS.config_file} or see open-pnp.py -h') exit(1) if SETTINGS.debug: @@ -507,6 +518,7 @@ if __name__ == '__main__': print(f'Status page running on : http://{SETTINGS.bind_pnp_server}:{SETTINGS.port}') print() print(f'{PNP_SERVER_VERSION} | ' - f'Written by thl-cmk, see https://thl-cmk.hopto.org/gitlab/bits-and-bytes/cisco_day0_provision') + f'Written by thl-cmk[at]outlook[dot]com | ' + f'see https://thl-cmk.hopto.org/gitlab/bits-and-bytes/cisco_day0_provision') print() app.run(host=SETTINGS.bind_pnp_server, port=SETTINGS.port) diff --git a/pnp/open_pnp_classes.py b/pnp/open_pnp_classes.py index 14bc2041ef31e4ae2267e8c1d9e7832a5bfa643a..fd7f3d4c94f2e9bd526e42907394edaf15bc56e3 100644 --- a/pnp/open_pnp_classes.py +++ b/pnp/open_pnp_classes.py @@ -11,6 +11,9 @@ # # 2023-03-13: fixed default_cfg option # added no_default_cfg option +# 2023-04-11: changed to load settings from config file (open-pnp.toml by default) in the SETTINGS.__init__ +# don't exit if cfg_file not found (not strictly needed, can all set viy cli), just print a warning +# fixed: --config-file option (was cfg_file instead of config-file) from typing import Dict, Optional, Any from tomli import load as toml_load @@ -105,7 +108,7 @@ class Settings: self, cli_args: Dict[str, Any], version: bool = False, - cfg_file: Optional[str] = 'open-pnp.toml', + config_file: Optional[str] = 'open-pnp.toml', image_data: Optional[str] = 'images.toml', bind_pnp_server: Optional[str] = '0.0.0.0', port: Optional[int] = 8080, @@ -120,7 +123,7 @@ class Settings: no_default_cfg: Optional[bool] = False, ): self.__settings = { - 'cfg_file': cfg_file, + 'config_file': config_file, 'version': version, 'image_data': image_data, 'bind_pnp_server': bind_pnp_server, @@ -135,30 +138,28 @@ class Settings: 'default_cfg': default_cfg, 'no_default_cfg': no_default_cfg, } - self.__args = {} - self.__set_cli_args(cli_args) - - def __set_cli_args(self, cli_args: Dict[str, Any]): self.__args = ({k: v for k, v in cli_args.items() if v}) self.__settings.update(self.__args) + self.update(self.config_file) - def update(self, cfg_file: str): + def update(self, config_file: str): try: - with open(cfg_file, 'rb') as f: + with open(config_file, 'rb') as f: self.__settings.update(toml_load(f)) except FileNotFoundError as e: - print(f'ERROR: Data file {cfg_file} not found! ({e})') - exit(1) + # don't exit if config_file not found (not strictly needed, can all set viy cli), just print a warning + print(f'WARNING: Settings file {config_file} not found! ({e})') + # exit(1) except TOMLDecodeError as e: print( - f'ERROR: Data file {cfg_file} is not in valid toml format! ({e})') + f'ERROR: Settings file {config_file} is not in valid toml format! ({e}), (see https://toml.io/en/)') exit(2) - - self.__settings.update(self.__args) + else: + self.__settings.update(self.__args) @property - def cfg_file(self) -> str: - return self.__settings['cfg_file'] + def config_file(self) -> str: + return self.__settings['config_file'] @property def version(self) -> bool: @@ -317,7 +318,7 @@ class Images: exit(1) except TOMLDecodeError as e: print( - f'ERROR: Data file {images_file} is not in valid toml format! ({e})') + f'ERROR: Data file {images_file} is not in valid toml format! ({e}) (see https://toml.io/en/)') exit(2) @property diff --git a/pnp/templates/backoff.xml b/pnp/templates/backoff.xml index 844ea7afb9627ab0c015ebcf2ef63652b182162b..d1cf7c8fc8b2d5156b6a01385b43262402ed56ed 100644 --- a/pnp/templates/backoff.xml +++ b/pnp/templates/backoff.xml @@ -10,5 +10,4 @@ </callbackAfter> </backoff> </request> -</pnp> - +</pnp> \ No newline at end of file diff --git a/pnp/templates/backoff_terminate.xml b/pnp/templates/backoff_terminate.xml index 51d304b548a84be2e4a2c6667ac2fb58c4cdfa78..e6be788c38c234cbb116af18c14db4a2553e791c 100644 --- a/pnp/templates/backoff_terminate.xml +++ b/pnp/templates/backoff_terminate.xml @@ -6,5 +6,4 @@ <terminate/> </backoff> </request> -</pnp> - +</pnp> \ No newline at end of file diff --git a/pnp/templates/bye.xml b/pnp/templates/bye.xml index bfd399d790a91bf11433f97cfd756810a8b992ec..1ae912d54d508ed6cab4c4e87171670b5fa78d90 100644 --- a/pnp/templates/bye.xml +++ b/pnp/templates/bye.xml @@ -1,8 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> <pnp xmlns="urn:cisco:pnp" version="1.0" udi="{{ udi }}"> <info xmlns="urn:cisco:pnp:work-info" correlator="{{ correlator }}"> <workInfo> <bye/> </workInfo> </info> -</pnp> - +</pnp> \ No newline at end of file diff --git a/pnp/templates/device_info.xml b/pnp/templates/device_info.xml index c4374e7706905aa586cf0d3095cc85471cb29478..9d29e418964715a8e0489198bd8f1719e7efbadb 100644 --- a/pnp/templates/device_info.xml +++ b/pnp/templates/device_info.xml @@ -3,4 +3,4 @@ <request correlator="{{ correlator }}" xmlns="urn:cisco:pnp:device-info"> <deviceInfo type="{{ info_type }}"/> </request> -</pnp> +</pnp> \ No newline at end of file diff --git a/pnp/templates/status.html b/pnp/templates/status.html index 9ce2ea57fd9984e9ffe333e6aa2c21c22f92e6fd..20eb1fdba37209c81e9d4c82e0eb27364773eeef 100644 --- a/pnp/templates/status.html +++ b/pnp/templates/status.html @@ -6,42 +6,42 @@ # File : status.html # -# 2023-03-13: moved error columns to debug output +# 2023-03-13: moved error columns to the debug output +# 2023-04-11: fine tuning output (formatting) # #} - {% macro header_row() %} <tr> - <th>Count</th> - <th>Action</th> - <th>Serial number</th> - <th>Platform</th> - <th>HW rev.</th> - <th>IP-Address</th> - <th>PnP flow</th> - <th>Status</th> - <th>First seen</th> - <th>Last contact</th> - <th>Current job</th> - <th>Current version</th> - <th>Target version</th> - <th>Last message</th> - {% if debug %} - <th>Current image</th> - <th>Target image</th> - <th>Size</th> - <th>Free space</th> - <th>Destination</th> - <th>Error code</th> - <th>Last error</th> - <th>Error count</th> - <th>Hard error</th> - <th>Backoff</th> - {% endif %} - </tr> + <th>Count</th> + <th>Action</th> + <th>Serial number</th> + <th>Platform</th> + <th>HW rev.</th> + <th>IP-Address</th> + <th>PnP flow</th> + <th>Status</th> + <th>First seen</th> + <th>Last contact</th> + <th>Current job</th> + <th>Current version</th> + <th>Target version</th> + <th>Last message</th> + {% if debug %} + <th>Current image</th> + <th>Target image</th> + <th>Size</th> + <th>Free space</th> + <th>Destination</th> + <th>Error code</th> + <th>Last error</th> + <th>Error count</th> + <th>Hard error</th> + <th>Backoff</th> + {% endif %} + </tr> {% endmacro %} - +{# --- #} {% macro add_empty_rows() %} {% set min_rows = 50 %} {% if devices|length < min_rows %} @@ -51,17 +51,11 @@ {% set num_columns = num_columns + 10 %} {% endif %} {% for i in range (0, min_rows - current_rows) %} - <tr> - <td> {{ loop.index + current_rows}}</td> - {% for c in range(0, num_columns) %} - <td></td> - {% endfor %} - </tr> + <tr><td>{{ loop.index + current_rows}}</td>{% for c in range(0, num_columns) %}<td></td>{% endfor %}</tr> {% endfor %} {% endif %} {% endmacro %} - - +{# --- #} <!DOCTYPE html> <html> <head> @@ -168,26 +162,29 @@ <div class="scrollTable"> <table> <thead> - {{ header_row() }} + {{header_row() -}} +{# needed to keep format #} </thead> <tbody> {% for device in devices %} <tr> - <td> {{ loop.index }}</td> + <td>{{ loop.index }}</td> <td> +{# <!-- <form method="post" action="/buttons"> <input class="action" type="submit" value="Refresh" name="{{ device.udi }}" {{ device.refresh_button }}""/> </form> --> +#} <form method="post" action="/buttons"> <input class="action" type="submit" value="Remove" name="{{ device.udi }}"/> </form> </td> {% if debug %} - <td class="{{ device.status_class }}"><a href="{{ config_base_url }}/{{ device.serial }}.cfg" target="_blank">{{device.serial }}</a></td> + <td class="{{ device.status_class }}"><a href="{{ config_base_url }}/{{ device.serial }}.cfg" target="_blank">{{device.serial }}</a></td> {% else %} - <td class="{{ device.status_class }}">{{ device.serial }}</td> + <td class="{{ device.status_class }}">{{ device.serial }}</td> {% endif %} <td>{{ device.platform }}</td> <td>{{ device.hw_rev }}</td> @@ -201,23 +198,25 @@ <td>{{ device.target_image.version }}</td> <td>{{ device.error_code_readable }}</td> {% if debug %} - <td>{{ device.image }}</td> - <td> <a href="{{ image_base_url }}/{{ device.target_image.image }}" target="_blank">{{device.target_image.image }}</a></td> - <td>{{ device.target_image.size }}</td> - <td>{{ device.destination_free }}</td> - <td>{{ device.destination_name }}</td> - <td>{{ device.error_code }}</td> - <td>{{ device.error_message }}</td> - <td>{{ device.error_count }}</td> - <td>{{ device.hard_error }}</td> - <td>{{ device.backoff }}</td> + <td>{{ device.image }}</td> + <td> <a href="{{ image_base_url }}/{{ device.target_image.image }}" target="_blank">{{device.target_image.image }}</a></td> + <td>{{ device.target_image.size }}</td> + <td>{{ device.destination_free }}</td> + <td>{{ device.destination_name }}</td> + <td>{{ device.error_code }}</td> + <td>{{ device.error_message }}</td> + <td>{{ device.error_count }}</td> + <td>{{ device.hard_error }}</td> + <td>{{ device.backoff }}</td> {% endif %} </tr> {% endfor %} - {{ add_empty_rows() }} + {{- add_empty_rows() -}} +{# needed to keep format #} </tbody> <tfoot> - {{ header_row() }} + {{header_row() -}} +{# needed to keep format #} </tfoot> </table> </div>