diff --git a/pnp/images.toml b/pnp/images.toml index de4ca596f276c04bb5ae0603f9639f7d5115b526..933f1d7db04fb1c8dfe5987b572bc3df158580b2 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 = [] +models = ["C1117-4PMLTEEAWE"] ["c1100-universalk9.17.06.04.SPA.bin"] version = "17.6.4" @@ -28,18 +28,6 @@ md5 = "2caa962f5ed0ecc52f99b90c733c54de" size = 706565772 models = [] -["c1100-universalk9.17.09.01a.SPA.bin"] -version = "17.9.1a" -md5 = "b3efb230d869fa6e77a98b4130c89585" -size = 684976080 -models = [] - -["c1100-universalk9.17.09.02a.SPA.bin"] -version = "17.9.2a" -md5 = "5a1e3c44a40d63d6d8d83370fe37d21d" -size = 685295244 -models = ["C1117-4PMLTEEAWE"] - ["c1100-universalk9.17.10.01a.SPA.bin"] version = "17.10.1a" md5 = "a0cd6218c42f19bed425e3c63a11bcda" diff --git a/pnp/open-pnp.py b/pnp/open-pnp.py index 2deb3a8a76e77d5332be8582cc87c2f2dd98efab..47083576c159ab53e0a00fe651fbc30a28f98a8e 100755 --- a/pnp/open-pnp.py +++ b/pnp/open-pnp.py @@ -25,7 +25,9 @@ # added reload data function # changed open-pnp.ini/images.json to open-pnp.toml/images.toml for better handling # 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 # pip install flask xmltodict requests ifaddr tomli # pip install python-dotenv # for vscode @@ -45,19 +47,93 @@ from tomli import TOMLDecodeError # define global variables -CFG_FILE = 'open-pnp.toml' -IMAGE_DATA: Optional[str] = None # 'images.toml' -BIND_PNP_SERVER: Optional[str] = None # '0.0.0.0' -PORT: Optional[int] = None # 8080 -TIME_FORMAT: Optional[str] = None # '%Y-%m-%dT%H:%M:%S' -STATUS_REFRESH: Optional[int] = None # 60 -DEBUG: Optional[bool] = None # False -LOG_TO_FILE: Optional[bool] = None # True -LOG_FILE: Optional[str] = None # 'log/pnp_debug.log' IMAGES: Optional[Dict[str, any]] = None # {} -IMAGE_BASE_URL: Optional[str] = None # '' -CONFIG_BASE_URL: Optional[str] = None # '' -DEFAULT_CFG_FILE: Optional[str] = None # 'DEFAULT.cfg' + + +class Settings: + def __init__( + self, + cfg_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, + 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_file: Optional[str] = 'log/pnp_debug.log', + image_base_url: Optional[str] = '', + config_base_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, + } + + def update(self, settings: Dict[str, any]): + self.__settings.update(settings) + + @property + def cfg_file(self) -> str: + return self.__settings['CFG_FILE'] + + @property + def image_data(self) -> str: + return self.__settings['IMAGE_DATA'] + + @property + def bind_pnp_server(self) -> str: + return self.__settings['BIND_PNP_SERVER'] + + @property + def port(self) -> int: + return self.__settings['PORT'] + + @property + def time_format(self) -> str: + return self.__settings['TIME_FORMAT'] + + @property + def status_refresh(self) -> int: + return self.__settings['STATUS_REFRESH'] + + @property + def debug(self) -> bool: + return self.__settings['DEBUG'] + + @property + def log_to_file(self) -> bool: + return self.__settings['LOG_TO_FILE'] + + @property + def log_file(self) -> str: + return self.__settings['LOG_FILE'] + + @property + def image_base_url(self) -> str: + return self.__settings['IMAGE_BASE_URL'] + + @property + def config_base_url(self) -> str: + return self.__settings['CONFIG_BASE_URL'] + + @property + def default_cfg_file(self) -> str: + return self.__settings['DEFAULT_CFG_FILE'] + + +SETTINGS = Settings() class SoftwareImage: @@ -232,7 +308,7 @@ class Device: app = Flask(__name__, template_folder='./templates') -if DEBUG: +if SETTINGS.debug: app.debug = True else: # disable FLASK console output @@ -264,75 +340,41 @@ def configure_logger(path): def log_info(message): - if LOG_TO_FILE: - log = logging.getLogger('root') - log.info(message) + if SETTINGS.debug: + if SETTINGS.log_to_file: + log = logging.getLogger('root') + log.info(message) def log_critical(message): - if LOG_TO_FILE: - log = logging.getLogger('root') - log.critical(message) + if SETTINGS.debug: + if SETTINGS.log_to_file: + log = logging.getLogger('root') + log.critical(message) def load_data(): + global SETTINGS global IMAGES - global IMAGE_DATA - global BIND_PNP_SERVER - global PORT - global TIME_FORMAT - global STATUS_REFRESH - global DEBUG - global LOG_TO_FILE - global LOG_FILE - global IMAGE_BASE_URL - global CONFIG_BASE_URL - global DEFAULT_CFG_FILE - - settings = { - 'BIND_PNP_SERVER': '0.0.0.0', - 'CONFIG_BASE_URL': '', - 'DEBUG': False, - 'DEFAULT_CFG_FILE': 'DEFAULT.cfg', - 'IMAGE_BASE_URL': '', - 'IMAGE_DATA': 'images.toml', - 'LOG_FILE': 'log/pnp_debug.log', - 'LOG_TO_FILE': True, - 'PORT': 8080, - 'STATUS_REFRESH': 60, - 'TIME_FORMAT': '%Y-%m-%dT%H:%M:%S' - } try: - with open(CFG_FILE, 'rb') as f: - settings.update(toml_load(f)) + with open(SETTINGS.cfg_file, 'rb') as f: + SETTINGS.update(toml_load(f)) except FileNotFoundError as e: - print(f'ERROR: Data file {CFG_FILE} not found! ({e})') + print(f'ERROR: Data file {SETTINGS.cfg_file} not found! ({e})') exit(1) except TOMLDecodeError as e: - print(f'ERROR: Data file {CFG_FILE} is not valid toml! ({e})') + print(f'ERROR: Data file {SETTINGS.cfg_file} is not valid toml! ({e})') exit(2) - BIND_PNP_SERVER = settings.get('BIND_PNP_SERVER') - PORT = settings.get('PORT') - TIME_FORMAT = settings.get('TIME_FORMAT') - STATUS_REFRESH = settings.get('STATUS_REFRESH') - DEBUG = settings.get('DEBUG') - LOG_TO_FILE = settings.get('LOG_TO_FILE') - LOG_FILE = settings.get('LOG_FILE') - IMAGE_DATA = settings.get('IMAGE_DATA') - IMAGE_BASE_URL = settings.get('IMAGE_BASE_URL').rstrip('/') - CONFIG_BASE_URL = settings.get('CONFIG_BASE_URL').rstrip('/') - DEFAULT_CFG_FILE = settings.get('DEFAULT_CFG_FILE') - try: - with open(IMAGE_DATA, 'rb') as f: + with open(SETTINGS.image_data, 'rb') as f: IMAGES = toml_load(f) except FileNotFoundError as e: - print(f'ERROR: Data file {IMAGE_DATA} not found! ({e})') + print(f'ERROR: Data file {SETTINGS.image_data} not found! ({e})') exit(1) except TOMLDecodeError as e: - print(f'ERROR: Data file {IMAGE_DATA} is not valid toml! ({e})') + print(f'ERROR: Data file {SETTINGS.image_data} is not valid toml! ({e})') exit(2) @@ -348,8 +390,7 @@ def pnp_device_info(udi: str, correlator: str, info_type: str) -> str: 'info_type': info_type } _template = render_template('device_info.xml', **jinja_context) - if DEBUG: - log_info(_template) + log_info(_template) return _template @@ -368,8 +409,7 @@ def pnp_backoff(udi: str, correlator: str, minutes: Optional[int] = 1) -> str: 'hours': hours, } _template = render_template('backoff.xml', **jinja_context) - if DEBUG: - log_info(_template) + log_info(_template) return _template @@ -384,14 +424,13 @@ def pnp_backoff_terminate(udi: str, correlator: str) -> str: 'correlator': correlator, } _template = render_template('backoff_terminate.xml', **jinja_context) - if DEBUG: - log_info(_template) + log_info(_template) return _template def pnp_install_image(udi: str, correlator: str) -> Optional[str]: device = devices[udi] - response = head(f'{IMAGE_BASE_URL}/{device.target_image.image}') + response = head(f'{SETTINGS.image_base_url}/{device.target_image.image}') if response.status_code == 200: device.current_job = 'urn:cisco:pnp:image-install' device.pnp_flow = PNPFLOW.UPDATE_START @@ -400,15 +439,14 @@ def pnp_install_image(udi: str, correlator: str) -> Optional[str]: jinja_context = { 'udi': udi, 'correlator': correlator, - 'base_url': IMAGE_BASE_URL, + 'base_url': SETTINGS.image_base_url, 'image': device.target_image.image, 'md5': device.target_image.md5.lower(), 'destination': device.destination_name, 'delay': 0, # reload in seconds } _template = render_template('image_install.xml', **jinja_context) - if DEBUG: - log_info(_template) + log_info(_template) return _template else: device.error_code = ERROR.ERROR_NO_IMAGE_FILE @@ -418,10 +456,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'{CONFIG_BASE_URL}/{cfg_file}') + response = head(f'{SETTINGS.config_base_url}/{cfg_file}') if response.status_code != 200: # SERIAL.cfg not found - cfg_file = DEFAULT_CFG_FILE - response = head(f'{CONFIG_BASE_URL}/{cfg_file}') + cfg_file = SETTINGS.default_cfg_file + response = head(f'{SETTINGS.config_base_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 @@ -432,14 +470,13 @@ def pnp_config_upgrade(udi: str, correlator: str) -> Optional[str]: jinja_context = { 'udi': udi, 'correlator': correlator, - 'base_url': CONFIG_BASE_URL, + 'base_url': SETTINGS.config_base_url, 'serial_number': cfg_file, 'delay': 30, # reload in seconds 'cfg_file': cfg_file, } _template = render_template('config_upgrade.xml', **jinja_context) - if DEBUG: - log_info(_template) + log_info(_template) return _template @@ -449,8 +486,7 @@ def pnp_bye(udi: str, correlator: str) -> str: 'correlator': correlator, } _template = render_template('bye.xml', **jinja_context) - if DEBUG: - log_info(_template) + log_info(_template) return _template @@ -461,8 +497,8 @@ def create_new_device(udi: str, src_add: str): platform, hw_rev, serial = SERIAL_NUM_RE.findall(udi)[0] devices[udi] = Device( udi=udi, - first_seen=strftime(TIME_FORMAT), - last_contact=strftime(TIME_FORMAT), + first_seen=strftime(SETTINGS.time_format), + last_contact=strftime(SETTINGS.time_format), src_address=src_add, serial=serial, platform=platform, @@ -493,7 +529,7 @@ def update_device_info(data: Dict[str, Any]): device.version = data['pnp']['response']['imageInfo']['versionString'] device.image = data['pnp']['response']['imageInfo']['imageFile'].split(':')[1] device.refresh_data = False - device.last_contact = strftime(TIME_FORMAT) + device.last_contact = strftime(SETTINGS.time_format) for filesystem in data['pnp']['response']['fileSystemList']['fileSystem']: if filesystem['@name'] in ['bootflash', 'flash']: destination = filesystem @@ -538,14 +574,12 @@ def status(): device_list.append(device) jinja_context = { 'devices': device_list, - 'refresh': STATUS_REFRESH, - 'config_base_url': CONFIG_BASE_URL, - 'image_base_url': IMAGE_BASE_URL, + 'refresh': SETTINGS.status_refresh, + 'config_base_url': SETTINGS.config_base_url, + 'image_base_url': SETTINGS.image_base_url, + 'debug': SETTINGS.debug, } - if DEBUG: - result = render_template('status_debug.html', **jinja_context) - else: - result = render_template('status.html', **jinja_context) + result = render_template('status.html', **jinja_context) return Response(result) @@ -554,7 +588,7 @@ def buttons(): udi = list(request.form.keys())[0] button = list(request.form.values())[0] - if button == 'Reload': + if button == 'Reload CFG': load_data() if udi in devices.keys(): @@ -587,20 +621,18 @@ def pnp_hello(): def pnp_work_request(): src_add = request.environ.get('HTTP_X_REAL_IP', request.remote_addr) data = xml_parse(request.data) - if DEBUG: - log_info(f'REQUEST: {data}') + log_info(f'REQUEST: {data}') correlator = data['pnp']['info']['@correlator'] udi = data['pnp']['@udi'] if udi in devices.keys(): device = devices[udi] - device.last_contact = strftime(TIME_FORMAT) + device.last_contact = strftime(SETTINGS.time_format) device.ip_address = src_add if device.hard_error: return Response(pnp_backoff(udi, correlator, 10), mimetype='text/xml') pass if device.backoff: - if DEBUG: - log_info('BACKOFF') + log_info('BACKOFF') # backoff more and more on errors, max error_count = 11 -> 5 * 11 = 55 # error_count == 12 -> like hard_error minutes = device.error_count + 1 @@ -608,33 +640,26 @@ def pnp_work_request(): device.hard_error = True return Response(pnp_backoff(udi, correlator, minutes), mimetype='text/xml') if device.pnp_flow == PNPFLOW.NEW: - if DEBUG: - log_info('PNPFLOW.NEW') + log_info('PNPFLOW.NEW') device.pnp_flow = PNPFLOW.INFO return Response(pnp_device_info(udi, correlator, 'all'), mimetype='text/xml') if device.pnp_flow == PNPFLOW.UPDATE_NEEDED: - if DEBUG: - log_info('PNPFLOW.UPDATE_NEEDED') + log_info('PNPFLOW.UPDATE_NEEDED') device.pnp_flow = PNPFLOW.UPDATE_START return Response(pnp_install_image(udi, correlator), mimetype='text/xml') if device.pnp_flow == PNPFLOW.UPDATE_RELOAD: - if DEBUG: - log_info('PNPFLOW.UPDATE_RELOAD') + log_info('PNPFLOW.UPDATE_RELOAD') return Response(pnp_device_info(udi, correlator, 'all'), mimetype='text/xml') if device.pnp_flow == PNPFLOW.UPDATE_DOWN: - if DEBUG: - log_info('PNPFLOW.UPDATE_DOWN') + log_info('PNPFLOW.UPDATE_DOWN') return Response(pnp_config_upgrade(udi, correlator), mimetype='text/xml') if device.pnp_flow == PNPFLOW.CONFIG_DOWN: # will never reach this point, as pnp is removed bei EEM :-) - if DEBUG: - log_info('PNPFLOW.CONFIG_DOWN') + log_info('PNPFLOW.CONFIG_DOWN') return Response(pnp_backoff_terminate(udi, correlator), mimetype='text/xml') - if DEBUG: - log_info(f'Other PNP_FLOW: {PNPFLOW.readable(device.pnp_flow)}') + log_info(f'Other PNP_FLOW: {PNPFLOW.readable(device.pnp_flow)}') return Response('', 200) else: - if DEBUG: - log_info('REQUEST NEW DEVICE FOUND') + log_info('REQUEST NEW DEVICE FOUND') create_new_device(udi, src_add) # return Response(device_info(udi, correlator, 'all'), mimetype='text/xml') devices[udi].pnp_flow = PNPFLOW.NEW @@ -645,8 +670,7 @@ def pnp_work_request(): @app.route('/pnp/WORK-RESPONSE', methods=['POST']) def pnp_work_response(): data = xml_parse(request.data) - if DEBUG: - log_info(f'RESPONSE: {data}') + log_info(f'RESPONSE: {data}') src_add = request.environ.get('HTTP_X_REAL_IP', request.remote_addr) udi = data['pnp']['@udi'] job_type = data['pnp']['response']['@xmlns'] @@ -655,18 +679,17 @@ def pnp_work_response(): device = devices[udi] device.ip_address = src_add - device.last_contact = strftime(TIME_FORMAT) + device.last_contact = strftime(SETTINGS.time_format) if job_type == 'urn:cisco:pnp:fault': # error without job info (correlator):-( device.error = data['pnp']['response']['fault']['detail']['XSVC-ERR:error']['XSVC-ERR:details'] else: correlator = data['pnp']['response']['@correlator'] job_status = int(data['pnp']['response']['@success']) - if DEBUG: - log_info(correlator) - log_info(job_type) - log_info(device.pnp_flow_readable) - log_info(job_status) + log_info(f'Correlator: {correlator}') + log_info(f'Job type: {job_type}') + log_info(f'PnP flow: {device.pnp_flow_readable}') + log_info(f'Job status: {job_status}') if job_status == 1: # success if job_type not in ['urn:cisco:pnp:backoff']: device.backoff = True @@ -687,8 +710,7 @@ def pnp_work_response(): # device.pnp_flow = PNPFLOW.INFO pass _response = pnp_bye(udi, correlator) - if DEBUG: - log_info(_response) + log_info(_response) return Response(_response, mimetype='text/xml') elif job_status == 0: error_code = int(data['pnp']['response']['errorInfo']['errorCode'].split(' ')[-1]) @@ -699,39 +721,38 @@ def pnp_work_response(): device.hard_error = True return Response(pnp_bye(udi, correlator), mimetype='text/xml') device.current_job = 'none' - if DEBUG: - log_info('Empty Response') + log_info('Empty Response') return Response('') if __name__ == '__main__': load_data() - if IMAGE_BASE_URL == '': + if SETTINGS.image_base_url == '': print('IMAGE_BASE_URL not set, check ./vars/vars.py') exit(1) - if CONFIG_BASE_URL == '': + if SETTINGS.config_base_url == '': print('CONFIG_BASE_URL not set, check ./vars/vars.py') exit(1) - if DEBUG: - configure_logger(LOG_FILE) + if SETTINGS.debug: + configure_logger(SETTINGS.log_file) log_info('STARTED LOGGER') print() print('Running PnP server. Stop with ctrl+c') - print(f'Bind to IP-address : {BIND_PNP_SERVER}') - print(f'Listen on port : {PORT}') - print(f'Image file(s) base URL : {IMAGE_BASE_URL}') - print(f'Config file(s) base URL : {CONFIG_BASE_URL}') + 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() print('The PnP server is running on the following URL(s)') - if BIND_PNP_SERVER in ['0.0.0.0', '::']: + if SETTINGS.bind_pnp_server in ['0.0.0.0', '::']: addresses = get_local_ip_addresses() for address in addresses: - print(f' http://{address}:{PORT}') + print(f' http://{address}:{SETTINGS.port}') else: - print(f'Status page running on : http://{BIND_PNP_SERVER}:{PORT}') + print(f'Status page running on : http://{SETTINGS.bind_pnp_server}:{SETTINGS.port}') print() print() - app.run(host=BIND_PNP_SERVER, port=PORT) + app.run(host=SETTINGS.bind_pnp_server, port=SETTINGS.port) diff --git a/pnp/templates/status.html b/pnp/templates/status.html index 2427c317ffd14d9af2f97df057096fe57b450676..1ebe26eab1b39e99bc1c157d7f017eadc3165008 100644 --- a/pnp/templates/status.html +++ b/pnp/templates/status.html @@ -1,111 +1,222 @@ -<!DOCTYPE html> -<html> -<head> - <meta http-equiv="refresh" content="{{ refresh }}"> - <style> - body {background-color: rgb(141, 133, 133);} - h1 { text-align: center;} - table { border-spacing: 0; width: 100%} - tr:nth-child(even) { background-color: #D6EEEE; } - th { - border: 1px solid #2e2c2c; - background-color: #d3cdcd; - padding: 5px; - } - td { - border: 1px solid #c5bfbf; - padding: 5px; - text-align: center; - } - td.error { - background-color: #dc143c; - } - td.warning { - background-color: #ffff00; - } - td.finished { - background-color: #7cfc00; - } - input { width: 70px; } - </style> -</head> -<body> -<h1>PnP Day0 status page</h1> -<!-- ToDo: find a nice place for the rload (data) button --> -<form method="post" action="/buttons"> - <input type="submit" value="Reload" name="reload_data"/> -</form> -<table> +{% macro header_row() %} <tr> <th>Count</th> <th>Action</th> - <th>Serial<br>number</th> + <th>Serial number</th> <th>Platform</th> - <th>HW<br>rev.</th> + <th>HW rev.</th> <th>IP-Address</th> - <th>PnP<br>flow</th> + <th>PnP flow</th> <th>Status</th> - <th>First<br>seen</th> - <th>Last<br>contact</th> - <th>Current<br>job</th> - <th>Current<br>version</th> - <th>Target<br>version</th> -<!-- <th>Current<br>image</th>--> -<!-- <th>Target<br>image</th>--> -<!-- <th>Size</th>--> -<!-- <th>Free space</th>--> -<!-- <th>Destination</th>--> - <th>Error<br>code</th> - <th>Last<br>error</th> - <th>Last<br>message</th> - <th>Error<br>count</th> - <th>Hard<br>error</th> -<!-- <th>Backoff</th>--> + <th>First seen</th> + <th>Last contact</th> + <th>Current job</th> + <th>Current version</th> + <th>Target version</th> + {% if debug %} + <th>Current image</th> + <th>Target image</th> + <th>Size</th> + <th>Free space</th> + <th>Destination</th> + {% endif %} + <th>Error code</th> + <th>Last error</th> + <th>Last message</th> + <th>Error count</th> + <th>Hard error</th> + {% if debug %} + <th>Backoff</th> + {% endif %} </tr> - {% for device in devices %} - <tr> - <td style="text-align: center;"> {{ loop.index }}</td> - <td> -<!-- +{% endmacro %} + +{% macro add_empty_rows() %} + {% set min_rows = 50 %} + {% if devices|length < min_rows %} + {% set current_rows = devices|length %} + {% set num_columns = 17 %} + {%if debug %} + {% set num_columns = num_columns + 6 %} + {% 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> + {% endfor %} + {% endif %} +{% endmacro %} + + +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="refresh" content="{{ refresh }}"> + <style> + body { + background-color: rgb(141, 133, 133); + padding:0px; + margin:0px; + overflow: hidden; + } + h1 { + text-align: center; + } + input { + width: 100px; + } + div.header { + width: 100%; + padding: 5px; + margin:0px; + height:70px; + } + /* start https://stackoverflow.com/questions/23989463/how-to-set-tbody-height-with-overflow-scroll */ + /* answer from brandonkal */ + /* define a container for the table, limit height, */ + div.scrollTable { + height: calc(100vh - 130px); /* header(70) + footer(50) + 10px safety */ + overflow-y: auto; + overflow-x: auto; + } + /* keep the header row visible at the top of the wrapper div */ + thead tr th { + position: sticky; + top: 0; + } + /* keep the footer row visible at the bottom of the wrapper div */ + tfoot tr th { + position: sticky; + bottom: 0; + } + table { + border-collapse: collapse; + text-align: center; + width: 100%; + } + /* end https://stackoverflow.com/questions/23989463/how-to-set-tbody-height-with-overflow-scroll */ + + th { + background-color: #d3cdcd; + } + td, th { + padding: 5px; + border: 1px solid #2e2c2c; + } + tr:nth-child(even) { + background-color: #D6EEEE; + } + tr:nth-child(odd) { + background-color: #06EEEE; + } + td.error { + background-color: #dc143c; + } + td.warning { + background-color: #ffff00; + } + td.finished { + background-color: #7cfc00; + } + div.footer{ + position: absolute; + bottom: 0px; + width: 100%; + padding: 0px; + padding-left: 5px; + margin:0px; + height: 50px; + } + footer{ + width:600px;; + } + div.reload_cfg { + position:absolute; + right:0px; + top:15px; + height:50px; + width: 120px; + } + </style> + </head> + <body> + <div class="header"> + <h1>Open PnP server status</h1> + <!-- ToDo: find a nice place for the reload configuration button --> <form method="post" action="/buttons"> - <input type="submit" value="Refresh" name="{{ device.udi }}" {{ device.refresh_button }}""/> - </form> ---> - <form method="post" action="/buttons"> - <input type="submit" value="Remove" name="{{ device.udi }}"/> + <input type="submit" value="Reload CFG" name="reload_data"/> </form> - </td> - <td class="{{ device.status_class }}">{{ device.serial }}</td> - <td>{{ device.platform }}</td> - <td>{{ device.hw_rev }}</td> - <td>{{ device.ip_address }}</td> - <td>{{ device.pnp_flow_readable }}</td> - <td>{{ device.status }} </td> - <td>{{ device.first_seen }}</td> - <td>{{ device.last_contact }}</td> - <td>{{ device.current_job }}</td> - <td>{{ device.version }}</td> - <td>{{ device.target_image.version }}</td> -<!-- <td>{{ device.image }}</td>--> -<!-- <td>{{ device.target_image.image }}</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_code_readable }}</td> - <td>{{ device.error_message }}</td> - <td>{{ device.error_count }}</td> - <td>{{ device.hard_error }}</td> -<!-- <td>{{ device.backoff }}</td>--> - </tr> - {% endfor %} -</table> -<footer> - <br><br> - <p>Written by: <a href="mailto:thl-cmk@outlook.com?subject=About your PnP server“,">thl-cmk</a> | Date: 2022-12-10 | for more information see - <a href="https://thl-cmk.hopto.org/gitlab/bits-and-bytes/cisco_day0_provision" target="_blank">Cisco Day0 provisioning</a> </p> -</footer> -</body> + </div> + <div class="scrollTable"> + <table> + <thead> + {{ header_row() }} + </thead> + <tbody> + {% for device in devices %} + <tr> + <td> {{ loop.index }}</td> + <td> + <!-- + <form method="post" action="/buttons"> + <input type="submit" value="Refresh" name="{{ device.udi }}" {{ device.refresh_button }}""/> + </form> + --> + <form method="post" action="/buttons"> + <input 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> + {% else %} + <td class="{{ device.status_class }}">{{ device.serial }}</td> + {% endif %} + <td>{{ device.platform }}</td> + <td>{{ device.hw_rev }}</td> + <td>{{ device.ip_address }}</td> + <td>{{ device.pnp_flow_readable }}</td> + <td>{{ device.status }}</td> + <td>{{ device.first_seen }}</td> + <td>{{ device.last_contact }}</td> + <td>{{ device.current_job }}</td> + <td>{{ device.version }}</td> + <td>{{ device.target_image.version }}</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> + {% endif %} + <td>{{ device.error_code }}</td> + <td>{{ device.error_code_readable }}</td> + <td>{{ device.error_message }}</td> + <td>{{ device.error_count }}</td> + <td>{{ device.hard_error }}</td> + {% if debug %} + <td>{{ device.backoff }}</td> + {% endif %} + </tr> + {% endfor %} + {{ add_empty_rows() }} + </tbody> + <tfoot> + {{ header_row() }} + </tfoot> + </table> + </div> + <div class="footer"> + <footer> + <p>Written by: <a href="mailto:thl-cmk@outlook.com?subject=About your PnP server“,">thl-cmk</a> | Date: 2022-12-10 | for more information see <a href="https://thl-cmk.hopto.org/gitlab/bits-and-bytes/cisco_day0_provision" target="_blank">Cisco Day0 provisioning</a></p> + </footer> + <div class="reload_cfg"> + <form class="reload_cfg" method="post" action="/buttons"> + <input type="submit" value="Reload CFG" name="reload_data"/> + </form> + </div> + </div> + </body> </html> - - diff --git a/pnp/templates/status_debug.html b/pnp/templates/status_debug.html deleted file mode 100644 index 00ee9812e61f6faeb68322eace4017c8f5b45704..0000000000000000000000000000000000000000 --- a/pnp/templates/status_debug.html +++ /dev/null @@ -1,133 +0,0 @@ -<!DOCTYPE html> -<html> - -<head> - <meta http-equiv="refresh" content="{{ refresh }}"> - <style> - body { - background-color: rgb(141, 133, 133); - } - h1 { text-align: center;} - - table { - border-spacing: 0; - width: 100% - } - - tr:nth-child(even) { - background-color: #D6EEEE; - } - - th { - border: 1px solid #2e2c2c; - background-color: #d3cdcd; - padding: 5px; - } - - td { - border: 1px solid #c5bfbf; - padding: 5px; - } - - td.error { - background-color: #dc143c; - } - - td.warning { - background-color: #ffff00; - } - - td.finished { - background-color: #7cfc00; - } - - input { - width: 70px; - } - </style> -</head> - -<body> - <h1>PnP Day0 status page</h1> - <!-- ToDo: find a nice place for the rload (data) button --> - <form method="post" action="/buttons"> - <input type="submit" value="Reload" name="reload_data" /> - </form> - <table> - <tr> - <th>Count</th> - <th>Action</th> - <th>Serial<br>number</th> - <th>Platform</th> - <th>HW<br>rev.</th> - <th>IP-Address</th> - <th>PnP<br>flow</th> - <th>Status</th> - <th>First<br>seen</th> - <th>Last<br>contact</th> - <th>Current<br>job</th> - <th>Current<br>version</th> - <th>Target<br>version</th> - <th>Current<br>image</th> - <th>Target<br>image</th> - <th>Size</th> - <th>Free<br>space</th> - <th>Destination</th> - <th>Error<br>code</th> - <th>Last<br>error</th> - <th>Last<br>message</th> - <th>Error<br>count</th> - <th>Hard<br>error</th> - <th>Backoff</th> - </tr> - {% for device in devices %} - <tr> - <td style="text-align: center;"> {{ loop.index }}</td> - <td> - <!-- - <form method="post" action="/buttons"> - <input type="submit" value="Refresh" name="{{ device.udi }}" {{ device.refresh_button }}""/> - </form> - --> - <form method="post" action="/buttons"> - <input type="submit" value="Remove" name="{{ device.udi }}" /> - </form> - </td> - <td class="{{ device.status_class }}"><a href="{{ config_base_url }}/{{ device.serial }}.cfg" target="_blank">{{ - device.serial }}</a></td> - <td>{{ device.platform }}</td> - <td>{{ device.hw_rev }}</td> - <td>{{ device.ip_address }}</td> - <td>{{ device.pnp_flow_readable }}</td> - <td>{{ device.status }} </td> - <td>{{ device.first_seen }}</td> - <td>{{ device.last_contact }}</td> - <td>{{ device.current_job }}</td> - <td>{{ device.version }}</td> - <td>{{ device.target_image.version }}</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_code_readable }}</td> - <td>{{ device.error_message }}</td> - <td>{{ device.error_count }}</td> - <td>{{ device.hard_error }}</td> - <td>{{ device.backoff }}</td> - </tr> - {% endfor %} - </table> - <footer> - <br><br> - <p>Written by: <a href="mailto:thl-cmk@outlook.com?subject=About your PnP server“,">thl-cmk</a> | Date: - 2022-12-10 | for more information see - <a href="https://thl-cmk.hopto.org/gitlab/bits-and-bytes/cisco_day0_provision" target="_blank">Cisco Day0 - provisioning</a> - </p> - </footer> -</body> - -</html> \ No newline at end of file