diff --git a/pnp/CHANGELOG b/pnp/CHANGELOG index 9edbf87f0ac5bc2de8bf6e5b9430bceb6a23879a..fead578052617a8332ab54b0246e5b7b3b8edc74 100644 --- a/pnp/CHANGELOG +++ b/pnp/CHANGELOG @@ -11,5 +11,5 @@ 2023-01-27: added default IOS/IOS-XE config file fallback 2023-01-28: moved global variables to Settings class integrated status_debug.html with status.html -2023-01-29 rework of status page, make table body scrollable - +2023-01-29: rework of status page, make table body scrollable +2023-02-01: added cli options, changed debug/log output diff --git a/pnp/README.md b/pnp/README.md index baf0e882dd2e4dfda9356daf96cc500e49f2c9cd..38a859080a5a2a5338212e95f3af243e8e156f7c 100644 --- a/pnp/README.md +++ b/pnp/README.md @@ -102,7 +102,7 @@ You can check if the PnP server is running by opening a web browser and accessin to use the PnP server you need to configure the server by modifying the following files -- [**_settings.toml_**](/pnp/open-pnp.toml) +- [**_open-pnp.toml_**](/pnp/open-pnp.toml) - [**_images.toml_**](/pnp/images.toml) **NOTE:** after changing the PnP server configuration you need to restart the PnP server. @@ -113,28 +113,28 @@ to use the PnP server you need to configure the server by modifying the followin #### Global settings [**open-pnp.toml**](/pnp/open-pnp.toml) ``` -# BIND_PNP_SERVER = "0.0.0.0" -# PORT = 8080 -# TIME_FORMAT = "%Y-%m-%dT%H:%M:%S" -# STATUS_REFRESH = 60 -# DEBUG = false -# LOG_TO_FILE = true -# LOG_FILE = "log/pnp_debug.log" -# IMAGE_DATA = "images.toml" -IMAGE_BASE_URL = "http://192.168.10.133:8080/images" -CONFIG_BASE_URL = "http://192.168.10.133:8080/configs" +# bind_pnp_server = "0.0.0.0" +# port = 8080 +# time_format = "%Y-%m-%dT%H:%M:%S" +# status_refresh = 60 +# debug = false +# log_to_console = false +# log_file = "log/pnp_debug.log" +# image_data = "images.toml" +# image_base_url = "http://192.168.10.133:8080/images" +# config_base_url = "http://192.168.10.133:8080/configs" ``` -- **BIND_PNP_SERVER**: the IP-address of your open-pnp server box. (Use `"::"` for IPv6) -- **PORT**: the TCP port the server should listen on (remember for port 80 the server needs to run as root) -- **TIME_FORMAT**: the time format used in the status page -- **STATUS_REFRESH**: the interval in seconds the status page will automatically reload -- **DEBUG**: enable debug output with `DEBUG = true`. Can be `true` or `false`. -- **LOG_FILE**: path/name of the log file -- **LOG_TO_FILE**: write log output to file. Can be `true` or `false`. -- **IMAGE_DATA**: the file containing the data of your IOS/IOS-XE images -- **IMAGE_BASE_URL**: the base URL for your images -- **CONFIG_BASE_URL**: the base URL for your configuration files +- **bind_pnp_server**: the IP-address of your open-pnp server box. (Use `"::"` for IPv6) +- **port**: the TCP port the server should listen on (remember for port 80 the server needs to run as root) +- **time_format**: the time format used in the status page +- **status_refresh**: the interval in seconds the status page will automatically reload +- **debug**: enable debug output with `debug = true`. Can be `true` or `false`. +- **log_file**: path/name of the log file +- **log_to_console**: send debug output to stdout. Can be `true` or `false`. +- **image_data**: the file containing the data of your IOS/IOS-XE images +- **image_base_url**: the base URL for your images +- **config_base_url**: the base URL for your configuration files **Note**: you need to uncomment (remove `# `) the lines if you change the values. @@ -157,7 +157,58 @@ models = ["C1000-8T-2G-L", "C1000-24P-4G-L", "C1000-24T-4G-L", "C1000-24T-4X-L", ``` -**NOTE:** By default _open-pnp_ expects the image data in _images.json_. You can change this with the key _IMAGE_DATA_ in _settings.ini_. +**NOTE:** By default _open-pnp_ expects the image data in _images.toml_. You can change this with the key _image_data_ in _open-pnp.toml_. + +--- +### Command Line Options + +With the Command Line Options you can override the default values, and the values from the _open-pnp.toml_ config file. +With the option --config_file CONFIG_FILE you can specify a costume config file to use instead of _open.pnp.toml_. + +``` +$ ./open-pnp.py -h +usage: open-pnp.py [-h] [--bind_pnp_server BIND_PNP_SERVER] [--port PORT] + [--time_format TIME_FORMAT] + [--status_refresh STATUS_REFRESH] [--debug] + [--log_to_console] [--log_file LOG_FILE] + [--image_data IMAGE_DATA] [--image_url IMAGE_URL] + [--config_url CONFIG_URL] [--config_file CONFIG_FILE] + +This is a basic implementation of the Cisco PnP protocol. It is intended to +roll out image updates and configurations for Cisco IOS/IOS-XE devices on +day0. + +optional arguments: + -h, --help show this help message and exit + --bind_pnp_server BIND_PNP_SERVER, -b BIND_PNP_SERVER + Bind PnP server to IP-address. (default: 0.0.0.0) + --port PORT, -p PORT TCP port to listen on. (default: 8080) + --time_format TIME_FORMAT + Format string to render time. (default: + %Y-%m-%dT%H:%M:%S) + --status_refresh STATUS_REFRESH, -r STATUS_REFRESH + Time in seconds to refresh PnP server status page. + (default: 60) + --debug Enable Debug output send to "log_file". + --log_to_console Enable debug output send to stdout (requires --debug). + --log_file LOG_FILE Path/name of the logfile. (default: log/pnp_debug.log, + requires --debug) + --image_data IMAGE_DATA + File containing the image description. (default: + images.toml) + --image_url IMAGE_URL + Download URL for image files. I.e. + http://192.168.10.133:8080/images + --config_url CONFIG_URL + Download URL for config files. I.e. + http://192.168.10.133:8080/configs + --config_file CONFIG_FILE + Path/name of open PnP server config file. (default: + open-pnp.toml) + +Written by: thl-cmk, for more information see: https://thl-cmk.hopto.org + +``` --- ### PnP server discovery diff --git a/pnp/images.toml b/pnp/images.toml index 933f1d7db04fb1c8dfe5987b572bc3df158580b2..40fac8c8d8a23b5afe9e5ab287027ca9531576a2 100644 --- a/pnp/images.toml +++ b/pnp/images.toml @@ -20,7 +20,7 @@ models = ["C1000-8T-2G-L", "C1000-24P-4G-L", "C1000-24T-4G-L", "C1000-24T-4X-L", version = "16.12.1a" md5 = "045d73625025b4f77c65c7800b7faa2b" size = 541469788 -models = ["C1117-4PMLTEEAWE"] +models = [] ["c1100-universalk9.17.06.04.SPA.bin"] version = "17.6.4" @@ -32,7 +32,7 @@ models = [] version = "17.10.1a" md5 = "a0cd6218c42f19bed425e3c63a11bcda" size = 689542648 -models = [] +models = ["C1117-4PMLTEEAWE"] ["c3560cx-universalk9-mz.152-7.E7.bin"] version = "15.2(7)E7" diff --git a/pnp/open-pnp.py b/pnp/open-pnp.py index 47083576c159ab53e0a00fe651fbc30a28f98a8e..3fd9059df46986c8888d541c7c4aa49a7cdab291 100755 --- a/pnp/open-pnp.py +++ b/pnp/open-pnp.py @@ -27,10 +27,12 @@ # 2023-01-27: added default IOS/IOS-XE config file fallback # 2023-01-28: moved global variables to Settings class # integrated status_debug.html with status.html -# 2023-01-29 rework of status page, make table body scrollable - +# 2023-01-29: rework of status page, make table body scrollable +# 2023-02-01: added cli options, changed debug/log output +# # pip install flask xmltodict requests ifaddr tomli -# pip install python-dotenv # for vscode +# + import logging from logging.handlers import RotatingFileHandler from re import compile as re_compile @@ -38,6 +40,8 @@ from pathlib import Path from time import strftime from typing import Optional, List, Dict, Any from requests import head +from sys import stdout +import argparse from flask import Flask, request, send_from_directory, render_template, Response, redirect, cli from xmltodict import parse as xml_parse @@ -60,77 +64,83 @@ class Settings: time_format: Optional[str] = '%Y-%m-%dT%H:%M:%S', status_refresh: Optional[int] = 60, debug: Optional[bool] = False, - log_to_file: Optional[bool] = True, + log_to_console: Optional[bool] = False, log_file: Optional[str] = 'log/pnp_debug.log', - image_base_url: Optional[str] = '', - config_base_url: Optional[str] = '', + image_url: Optional[str] = '', + config_url: Optional[str] = '', default_cfg_file: Optional[str] = 'DEFAULT.cfg', ): self.__settings = { - 'CFG_FILE': cfg_file, - 'IMAGE_DATA': image_data, - 'BIND_PNP_SERVER': bind_pnp_server, - 'PORT': port, - 'TIME_FORMAT': time_format, - 'STATUS_REFRESH': status_refresh, - 'DEBUG': debug, - 'LOG_TO_FILE': log_to_file, - 'LOG_FILE': log_file, - 'IMAGE_BASE_URL': image_base_url, - 'CONFIG_BASE_URL': config_base_url, - 'DEFAULT_CFG_FILE': default_cfg_file, + 'cfg_file': cfg_file, + 'image_data': image_data, + 'bind_pnp_server': bind_pnp_server, + 'port': port, + 'time_format': time_format, + 'status_refresh': status_refresh, + 'debug': debug, + 'log_to_console': log_to_console, + 'log_file': log_file, + 'image_url': image_url, + 'config_url': config_url, + 'default_cfg_file': default_cfg_file, } + self.__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) def update(self, settings: Dict[str, any]): self.__settings.update(settings) + self.__settings.update(self.__args) @property def cfg_file(self) -> str: - return self.__settings['CFG_FILE'] + return self.__settings['cfg_file'] @property def image_data(self) -> str: - return self.__settings['IMAGE_DATA'] + return self.__settings['image_data'] @property def bind_pnp_server(self) -> str: - return self.__settings['BIND_PNP_SERVER'] + return self.__settings['bind_pnp_server'] @property def port(self) -> int: - return self.__settings['PORT'] + return self.__settings['port'] @property def time_format(self) -> str: - return self.__settings['TIME_FORMAT'] + return self.__settings['time_format'] @property def status_refresh(self) -> int: - return self.__settings['STATUS_REFRESH'] + return self.__settings['status_refresh'] @property def debug(self) -> bool: - return self.__settings['DEBUG'] + return self.__settings['debug'] @property - def log_to_file(self) -> bool: - return self.__settings['LOG_TO_FILE'] + def log_to_console(self) -> bool: + return self.__settings['log_to_console'] @property def log_file(self) -> str: - return self.__settings['LOG_FILE'] + return self.__settings['log_file'] @property - def image_base_url(self) -> str: - return self.__settings['IMAGE_BASE_URL'] + def image_url(self) -> str: + return self.__settings['image_url'] @property - def config_base_url(self) -> str: - return self.__settings['CONFIG_BASE_URL'] + def config_url(self) -> str: + return self.__settings['config_url'] @property def default_cfg_file(self) -> str: - return self.__settings['DEFAULT_CFG_FILE'] + return self.__settings['default_cfg_file'] SETTINGS = Settings() @@ -322,9 +332,12 @@ devices: Dict[str, Device] = {} def configure_logger(path): log_formatter = logging.Formatter('%(asctime)s :: %(levelname)s :: %(name)s :: %(module)s ::%(message)s') + log = logging.getLogger('root') + log.setLevel(logging.INFO) + log_file = path # create a new file > 5 mb size - log_handler = RotatingFileHandler( + log_handler_file = RotatingFileHandler( log_file, mode='a', maxBytes=5 * 1024 * 1024, @@ -332,25 +345,28 @@ def configure_logger(path): # encoding=None, # delay=0 ) - log_handler.setFormatter(log_formatter) - log_handler.setLevel(logging.INFO) - log = logging.getLogger('root') - log.setLevel(logging.INFO) - log.addHandler(log_handler) + + log_handler_file.setFormatter(log_formatter) + log_handler_file.setLevel(logging.INFO) + log.addHandler(log_handler_file) + + if SETTINGS.log_to_console: + log_handler_console = logging.StreamHandler(stdout) + log_handler_console.setFormatter(log_formatter) + log_handler_console.setLevel(logging.INFO) + log.addHandler(log_handler_console) def log_info(message): if SETTINGS.debug: - if SETTINGS.log_to_file: - log = logging.getLogger('root') - log.info(message) + log = logging.getLogger('root') + log.info(message) def log_critical(message): if SETTINGS.debug: - if SETTINGS.log_to_file: - log = logging.getLogger('root') - log.critical(message) + log = logging.getLogger('root') + log.critical(message) def load_data(): @@ -378,6 +394,40 @@ def load_data(): exit(2) +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser( + prog='open-pnp.py', + description='This is a basic implementation of the Cisco PnP protocol. It is intended to roll out image updates' + ' and configurations for Cisco IOS/IOS-XE devices on day0.', + # formatter_class=argparse.ArgumentDefaultsHelpFormatter, + epilog='Written by: thl-cmk, for more information see: https://thl-cmk.hopto.org', + ) + parser.add_argument('--bind_pnp_server', '-b', type=str, + help='Bind PnP server to IP-address. (default: 0.0.0.0)') + parser.add_argument('--port', '-p', type=int, + help='TCP port to listen on. (default: 8080)') + parser.add_argument('--time_format', type=str, + help='Format string to render time. (default: %%Y-%%m-%%dT%%H:%%M:%%S)') + parser.add_argument('--status_refresh', '-r', type=int, + help='Time in seconds to refresh PnP server status page. (default: 60)') + parser.add_argument('--debug', default=False, action="store_const", const=True, + help='Enable Debug output send to "log_file".') + parser.add_argument('--log_to_console', default=False, action="store_const", const=True, + help='Enable debug output send to stdout (requires --debug).') + parser.add_argument('--log_file', type=str, + help='Path/name of the logfile. (default: log/pnp_debug.log, requires --debug) ') + parser.add_argument('--image_data', type=str, + help='File containing the image description. (default: images.toml)') + parser.add_argument('--image_url', type=str, + help='Download URL for image files. I.e. http://192.168.10.133:8080/images') + parser.add_argument('--config_url', type=str, + help='Download URL for config files. I.e. http://192.168.10.133:8080/configs') + parser.add_argument('--config_file', type=str, + help='Path/name of open PnP server config file. (default: open-pnp.toml)') + + return parser.parse_args() + + def pnp_device_info(udi: str, correlator: str, info_type: str) -> str: # info_type can be one of: # image, hardware, filesystem, udi, profile, all @@ -430,7 +480,7 @@ def pnp_backoff_terminate(udi: str, correlator: str) -> str: def pnp_install_image(udi: str, correlator: str) -> Optional[str]: device = devices[udi] - response = head(f'{SETTINGS.image_base_url}/{device.target_image.image}') + response = head(f'{SETTINGS.image_url}/{device.target_image.image}') if response.status_code == 200: device.current_job = 'urn:cisco:pnp:image-install' device.pnp_flow = PNPFLOW.UPDATE_START @@ -439,7 +489,7 @@ def pnp_install_image(udi: str, correlator: str) -> Optional[str]: jinja_context = { 'udi': udi, 'correlator': correlator, - 'base_url': SETTINGS.image_base_url, + 'base_url': SETTINGS.image_url, 'image': device.target_image.image, 'md5': device.target_image.md5.lower(), 'destination': device.destination_name, @@ -456,10 +506,10 @@ def pnp_install_image(udi: str, correlator: str) -> Optional[str]: def pnp_config_upgrade(udi: str, correlator: str) -> Optional[str]: device = devices[udi] cfg_file = f'{device.serial}.cfg' - response = head(f'{SETTINGS.config_base_url}/{cfg_file}') + response = head(f'{SETTINGS.config_url}/{cfg_file}') if response.status_code != 200: # SERIAL.cfg not found cfg_file = SETTINGS.default_cfg_file - response = head(f'{SETTINGS.config_base_url}/{cfg_file}') + response = head(f'{SETTINGS.config_url}/{cfg_file}') if response.status_code != 200: # DEFAULT.cfg also not found device.error_code = ERROR.ERROR_NO_CFG_FILE device.hard_error = True @@ -470,7 +520,7 @@ def pnp_config_upgrade(udi: str, correlator: str) -> Optional[str]: jinja_context = { 'udi': udi, 'correlator': correlator, - 'base_url': SETTINGS.config_base_url, + 'base_url': SETTINGS.config_url, 'serial_number': cfg_file, 'delay': 30, # reload in seconds 'cfg_file': cfg_file, @@ -575,8 +625,8 @@ def status(): jinja_context = { 'devices': device_list, 'refresh': SETTINGS.status_refresh, - 'config_base_url': SETTINGS.config_base_url, - 'image_base_url': SETTINGS.image_base_url, + 'config_url': SETTINGS.config_url, + 'image_url': SETTINGS.image_url, 'debug': SETTINGS.debug, } result = render_template('status.html', **jinja_context) @@ -726,13 +776,15 @@ def pnp_work_response(): if __name__ == '__main__': + + SETTINGS.set_cli_args(vars(parse_arguments())) load_data() - if SETTINGS.image_base_url == '': - print('IMAGE_BASE_URL not set, check ./vars/vars.py') + if SETTINGS.image_url == '': + print(f'image_url not set, check {SETTINGS.cfg_file}') exit(1) - if SETTINGS.config_base_url == '': - print('CONFIG_BASE_URL not set, check ./vars/vars.py') + if SETTINGS.config_url == '': + print(f'config_url not set, check {SETTINGS.cfg_file}') exit(1) if SETTINGS.debug: @@ -740,11 +792,12 @@ if __name__ == '__main__': log_info('STARTED LOGGER') print() - print('Running PnP server. Stop with ctrl+c') + print(f'Running open PnP server. Stop with ctrl+c') + print() print(f'Bind to IP-address : {SETTINGS.bind_pnp_server}') print(f'Listen on port : {SETTINGS.port}') - print(f'Image file(s) base URL : {SETTINGS.image_base_url}') - print(f'Config file(s) base URL : {SETTINGS.config_base_url}') + print(f'Image file(s) base URL : {SETTINGS.image_url}') + print(f'Config file(s) base URL : {SETTINGS.config_url}') print() print('The PnP server is running on the following URL(s)') if SETTINGS.bind_pnp_server in ['0.0.0.0', '::']: @@ -754,5 +807,6 @@ if __name__ == '__main__': else: print(f'Status page running on : http://{SETTINGS.bind_pnp_server}:{SETTINGS.port}') print() + print(f'Writen by thl-cmk, 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.toml b/pnp/open-pnp.toml index df44fe86d63abaf968831f5a9de53d4540987933..303747c371f1d906c559f1331da016e4f6dd9d2b 100644 --- a/pnp/open-pnp.toml +++ b/pnp/open-pnp.toml @@ -1,13 +1,13 @@ # [settings] -# BIND_PNP_SERVER = "0.0.0.0" -# BIND_PNP_SERVER = "::" -# PORT = 8080 -# TIME_FORMAT = "%Y-%m-%dT%H:%M:%S" -STATUS_REFRESH = 10 -# DEBUG = false -# LOG_TO_FILE = true -# LOG_FILE = "log/pnp_debug.log" -# DEFAULT_CFG_FILE = "DEFAULT.cfg" -# IMAGE_DATA = "images.json" -IMAGE_BASE_URL = "http://192.168.10.133:8080/images" -CONFIG_BASE_URL = "http://192.168.10.133:8080/configs" +# bind_pnp_server = "0.0.0.0" +# bind_pnp_server = "::" +# port = 8080 +# time_format = "%y-%m-%dt%h:%m:%s" +status_refresh = 10 +# debug = false +# log_to_file = true +# log_file = "log/pnp_debug.log" +# default_cfg_file = "default.cfg" +# image_data = "images.toml" +image_url = "http://192.168.10.133:8080/images" +config_url = "http://192.168.10.133:8080/configs" diff --git a/pnp/templates/status.html b/pnp/templates/status.html index 1ebe26eab1b39e99bc1c157d7f017eadc3165008..7219931a4cda50939729f05b9cc8c6bdf88c7a03 100644 --- a/pnp/templates/status.html +++ b/pnp/templates/status.html @@ -57,6 +57,7 @@ <meta http-equiv="refresh" content="{{ refresh }}"> <style> body { + border-collapse: collapse; background-color: rgb(141, 133, 133); padding:0px; margin:0px;