From 2d7e71089c53f14cab32046092d4d1385dd55561 Mon Sep 17 00:00:00 2001 From: Neil Schark <neil-jocelyn.schark@stud.h-da.de> Date: Thu, 8 Sep 2022 16:52:12 +0200 Subject: [PATCH] arista bootstrap skript --- ztp-root/bootstrap/bootstrap | 1032 ++++++++++++++++++++++++++++++++++ 1 file changed, 1032 insertions(+) diff --git a/ztp-root/bootstrap/bootstrap b/ztp-root/bootstrap/bootstrap index e69de29..45260e8 100644 --- a/ztp-root/bootstrap/bootstrap +++ b/ztp-root/bootstrap/bootstrap @@ -0,0 +1,1032 @@ +#!/usr/bin/env python +# +# Copyright (c) 2014, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# - Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# - Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# - Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# Bootstrap script +# +# Written by: +# EOS+, Arista Networks + + +import datetime +import imp +import json +import jsonrpclib +import logging +import os +import os.path +import re +import sleekxmpp +import shutil +import socket +import subprocess +import sys +import time +import traceback +import urllib2 +import urlparse + +from collections import namedtuple +from logging.handlers import SysLogHandler +from subprocess import PIPE + +# Server will replace this value with the correct IP address/hostname +# before responding to the bootstrap request. +SERVER = '$SERVER' + +LOGGING_FACILITY = 'ztpbootstrap' +SYSLOG = '/dev/log' + +CONTENT_TYPE_PYTHON = 'text/x-python' +CONTENT_TYPE_HTML = 'text/html' +CONTENT_TYPE_OTHER = 'text/plain' +CONTENT_TYPE_JSON = 'application/json' + +TEMP = '/tmp' + +COMMAND_API_SERVER = 'localhost' +COMMAND_API_USERNAME = 'ztps' +COMMAND_API_PASSWORD = 'ztps-password' +COMMAND_API_PROTOCOL = 'http' + +HTTP_STATUS_OK = 200 +HTTP_STATUS_CREATED = 201 +HTTP_STATUS_BAD_REQUEST = 400 +HTTP_STATUS_NOT_FOUND = 404 +HTTP_STATUS_CONFLICT = 409 +HTTP_STATUS_INTERNAL_SERVER_ERROR = 500 + +FLASH = '/mnt/flash' + +STARTUP_CONFIG = '%s/startup-config' % FLASH +RC_EOS = '%s/rc.eos' % FLASH +BOOT_EXTENSIONS = '%s/boot-extensions' % FLASH +BOOT_EXTENSIONS_FOLDER = '%s/.extensions' % FLASH + +HTTP_TIMEOUT = 10 + +#pylint: disable=C0103 +syslog_manager = None +xmpp_client = None +#pylint: enable=C0103 + +#---------------------------------XMPP------------------------ +# Uncomment this section in order to enable XMPP debug logging +# logging.basicConfig(level=logging.DEBUG, +# format='%(levelname)-8s %(message)s') + +# You will also have to uncomment the following lines: +for logger in ['sleekxmpp.xmlstream.xmlstream', + 'sleekxmpp.basexmpp']: + xmpp_log = logging.getLogger(logger) + xmpp_log.addHandler(logging.NullHandler()) +#---------------------------------XMPP------------------------ + + +# ------------------Utilities---------------------------- +def _exit(code): + #pylint: disable=W0702 + + # Wait for XMPP messages to drain + time.sleep(3) + + if xmpp_client: + try: + xmpp_client.abort() + except: + pass + + sys.stdout.flush() + sys.stderr.flush() + + #pylint: disable=W0212 + # Need to close background sleekxmpp threads as well + os._exit(code) + +SYSTEM_ID = None +XMPP_MSG_TYPE = None +def log_xmpp(): + return XMPP_MSG_TYPE == 'debug' + +def log(msg, error=False, xmpp=None): + if xmpp is None: + xmpp = log_xmpp() + + timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H:%M:%S') + xmpp_msg = 'ZTPS:%s: %s%s' % (timestamp, + 'ERROR: ' if error else '', + msg) + + if xmpp and xmpp_client and xmpp_client.connected: + xmpp_client.message(xmpp_msg) + + if SYSTEM_ID: + syslog_msg = '%s: %s' % (SYSTEM_ID, msg) + else: + syslog_msg = msg + + if error: + print 'ERROR: %s' % syslog_msg + else: + print syslog_msg + + if syslog_manager: + if error: + syslog_manager.log.error(syslog_msg) + else: + syslog_manager.log.info(syslog_msg) + +#pylint: disable=C0103 +_ntuple_diskusage = namedtuple('usage', 'total used free') +#pylint: enable=C0103 +def flash_usage(): + stats = os.statvfs(FLASH) + free = stats.f_bavail * stats.f_frsize + total = stats.f_blocks * stats.f_frsize + used = (stats.f_blocks - stats.f_bfree) * stats.f_frsize + return _ntuple_diskusage(total, used, free) +# ------------------Utilities---------------------------- + + +# ------------------4.12.x support---------------------------- +def download_file(url, path): + if not urlparse.urlsplit(url).scheme: #pylint: disable=E1103 + url = urlparse.urljoin(SERVER, url) + + log('Retrieving URL: %s' % url) + + url = urllib2.urlopen(url) + output_file = open(path, 'wb') + output_file.write(url.read()) + output_file.close() + +#pylint: disable=C0103 +REQUESTS = 'requests-2.3.0' +REQUESTS_URL = '%s/files/lib/%s.tar.gz' % (SERVER, REQUESTS) +try: + import requests +except ImportError: + requests_url = '/tmp/%s.tar.gz' % REQUESTS + download_file(REQUESTS_URL, requests_url) + cmd = 'sudo tar -xzvf %s -C /tmp;' \ + 'cd /tmp/%s;' \ + 'sudo python setup.py build;' \ + 'sudo python setup.py install' % \ + (requests_url, REQUESTS) + res = os.system(cmd) + if res: + log('%s returned %s' % (cmd, res), error=True) + _exit(1) + import requests +#pylint: enable=C0103 +# ------------------4.12.x support---------------------------- + + +class ZtpError(Exception): + pass + +class ZtpActionError(ZtpError): + pass + +class ZtpUnexpectedServerResponseError(ZtpError): + pass + + +class Attributes(object): + + def __init__(self, local_attr=None, special_attr=None): + self.local_attr = local_attr if local_attr else [] + self.special_attr = special_attr if special_attr else [] + + def get(self, attr, default=None): + if attr in self.local_attr: + return self.local_attr[attr] + elif attr in self.special_attr: + return self.special_attr[attr] + else: + return default + + def copy(self): + attrs = dict() + if self.special_attr: + attrs = self.special_attr.copy() + if self.local_attr: + attrs.update(self.local_attr) + return attrs + + +class Node(object): + #pylint: disable=R0201 + + '''Node object which can be used by actions via: + attributes.get('NODE') + Attributes: + client (jsonrpclib.Server): jsonrpclib connect to Command API engine + ''' + + def __init__(self, server): + self.server_ = server + + Node._enable_api() + + url = '%s://%s:%s@%s/command-api' % (COMMAND_API_PROTOCOL, + COMMAND_API_USERNAME, + COMMAND_API_PASSWORD, + COMMAND_API_SERVER) + self.client = jsonrpclib.Server(url) + + try: + self.api_enable_cmds([]) + except socket.error: + raise ZtpError('unable to enable eAPI') + + # Workaround for BUG89374 + try: + self._disable_copp() + except jsonrpclib.jsonrpc.ProtocolError as err: + log('unable to disable COPP: %s' % err, error=True) + + global SYSTEM_ID #pylint: disable=W0603 + SYSTEM_ID = \ + self.api_enable_cmds(['show version'])[0]['serialNumber'] + + @classmethod + def _cli_enable_cmd(cls, cli_cmd): + bash_cmd = ['FastCli', '-p', '15', '-A', '-c', cli_cmd] + proc = subprocess.Popen(bash_cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) + (out, err) = proc.communicate() + code = proc.returncode #pylint: disable=E1101 + return (code, out, err) + + @classmethod + def _cli_config_cmds(cls, cmds): + cls._cli_enable_cmd('\n'.join(['configure'] + cmds)) + + @classmethod + def _enable_api(cls): + cls._cli_config_cmds(['username %s secret %s privilege 15' % + (COMMAND_API_USERNAME, + COMMAND_API_PASSWORD), + 'management api http-commands', + 'no protocol https', + 'protocol %s' % COMMAND_API_PROTOCOL, + 'no shutdown']) + + _, out, _ = cls._cli_enable_cmd('show management api http-commands |' + ' grep running') + retries = 3 + while not out and retries: + log('Waiting for CommandAPI to be enabled...') + time.sleep(1) + retries = retries - 1 + _, out, _ = cls._cli_enable_cmd( + 'show management api http-commands | grep running') + + def _disable_copp(self): + # COPP does not apply to vEOS + if self.system()['model'] != 'vEOS': + self.api_config_cmds(['control-plane', + 'no service-policy input copp-system-policy']) + + def _has_rc_eos(self): + return os.path.isfile(RC_EOS) + + def _append_lines(self, filename, lines): + with open(filename, 'a') as output: + output.write('\n') + output.write('\n'.join(lines)) + + def api_enable_cmds(self, cmds, text_format=False): + '''Run CLI commands via Command API, starting from enable mode. + Commands are ran in order. + Args: + cmds (list): List of CLI commands. + text_format (bool, optional): If true, Command API request will run + in text mode (instead of JSON). + Returns: + list: List of Command API results corresponding to the + input commands. + ''' + req_format = 'text' if text_format else 'json' + result = self.client.runCmds(1, ['enable'] + cmds, req_format) + if text_format: + return [x.values()[0] for x in result][1:] + else: + return result[1:] + + def api_config_cmds(self, cmds): + '''Run CLI commands via Command API, starting from config mode. + Commands are ran in order. + Args: + cmds (list): List of CLI commands. + Returns: + list: List of Command API results corresponding to the + input commands. + ''' + return self.api_enable_cmds(['configure'] + cmds)[1:] + + def system(self): + '''Get system information. + Returns: + dict: System information + Format:: + {'model': <MODEL>, + 'version': <EOS_VERSION>, + 'systemmac': <SYSTEM_MAC>, + 'serialnumber': <SERIAL_NUMBER>} + ''' + + result = {} + info = self.api_enable_cmds(['show version'])[0] + + result['model'] = info['modelName'] + result['version'] = info['version'] + result['systemmac'] = info['systemMacAddress'] + result['serialnumber'] = info['serialNumber'] + + return result + + def neighbors(self): + '''Get neighbors. + Returns: + dict: LLDP neighbor + Format:: + {'neighbors': {<LOCAL_PORT>: + [{'device': <REMOTE_DEVICE>, + 'port': <REMOTE_PORT>}, ...], + ...}} + ''' + + result = {} + info = self.api_enable_cmds(['show lldp neighbors'])[0] + result['neighbors'] = {} + for entry in info['lldpNeighbors']: + neighbor = {} + neighbor['device'] = entry['neighborDevice'] + neighbor['port'] = entry['neighborPort'] + if entry['port'] in result['neighbors']: + result['neighbors'][entry['port']] += [neighbor] + else: + result['neighbors'][entry['port']] = [neighbor] + return result + + def details(self): + '''Get details. + Returns: + dict: System details + Format:: + {'model': <MODEL>, + 'version': <EOS_VERSION>, + 'systemmac': <SYSTEM_MAC>, + 'serialnumber': <SERIAL_NUMBER>, + 'neighbors': <NEIGHBORS> # see neighbors() + } + ''' + + return dict(self.system().items() + + self.neighbors().items()) + + def has_startup_config(self): + '''Check whether startup-config is configured or not. + Returns: + bool: True is startup-config is configured; false otherwise. + ''' + return os.path.isfile(STARTUP_CONFIG) and \ + open(STARTUP_CONFIG).read().strip() + + def append_startup_config_lines(self, lines): + '''Add lines to startup-config. + Args: + lines (list): List of CLI commands + ''' + self._append_lines(STARTUP_CONFIG, lines) + + def append_rc_eos_lines(self, lines): + '''Add lines to rc.eos. + Args: + lines (list): List of bash commands + ''' + if not self._has_rc_eos(): + lines = ['#!/bin/bash'] + lines + self._append_lines(RC_EOS, lines) + + def log_msg(self, msg, error=False): + '''Log message via configured syslog/XMPP. + Args: + msg (string): Message + error (bool, optional): True if msg is an error; false otherwise. + ''' + log(msg, error) + + def rc_eos(self): + '''Get rc.eos path. + Returns: + string: rc.eos path + ''' + return RC_EOS + + def flash(self): + '''Get flash path. + Returns: + string: flash path + ''' + return FLASH + + def startup_config(self): + '''Get startup-config path. + Returns: + string: startup-config path + ''' + return STARTUP_CONFIG + + def retrieve_url(self, url, path): + '''Download resource from server. + If 'path' is somewhere on flash, the client will first request the + metainformation for the resource from the server (in order to Check + whether there is enogh disk space available). + Raises: + ZtpError: resource cannot be retrieved: + - metainformation cannot be retrieved from server OR + - disk space on flash is insufficient OR + - file cannot be written to disk + Returns: + string: startup-config path + ''' + self.server_.get_resource(url, path) + + @classmethod + def server_address(cls): + '''Get ZTP Server URL. + Returns: + string: ZTP Server URL. + ''' + return SERVER + + +class SyslogManager(object): + + def __init__(self): + self.log = logging.getLogger('ztpbootstrap') + self.log.setLevel(logging.DEBUG) + self.formatter = logging.Formatter('ZTPS - %(levelname)s: ' + '%(message)s') + + # syslog to localhost enabled by default + self._add_syslog_handler() + + def _add_handler(self, handler, level=None): + if level is None: + level = 'DEBUG' + + try: + handler.setLevel(logging.getLevelName(level)) + except ValueError: + log('SyslogManager: unknown logging level (%s) - using ' + 'log.DEFAULT instead' % level, error=True) + handler.setLevel(logging.DEBUG) + + handler.setFormatter(self.formatter) + self.log.addHandler(handler) + + def _add_syslog_handler(self): + log('SyslogManager: adding localhost handler') + self._add_handler(SysLogHandler(address=SYSLOG)) + + def _add_file_handler(self, filename, level=None): + log('SyslogManager: adding file handler (%s - level:%s)' % + (filename, level)) + self._add_handler(logging.FileHandler(filename), level) + + def _add_remote_syslog_handler(self, host, port, level=None): + log('SyslogManager: adding remote handler (%s:%s - level:%s)' % + (host, port, level)) + self._add_handler(SysLogHandler(address=(host, port)), level) + + def add_handlers(self, handler_config): + for entry in handler_config: + match = re.match('^file:(.+)', + entry['destination']) + if match: + self._add_file_handler(match.groups()[ 0 ], + entry['level']) + else: + match = re.match('^(.+):(.+)', + entry['destination']) + if match: + self._add_remote_syslog_handler(match.groups()[ 0 ], + int(match.groups()[ 1 ]), + entry['level']) + else: + log('SyslogManager: Unable to create syslog handler for' + ' %s' % str(entry), error=True) + + +class Server(object): + + def __init__(self): + pass + + @classmethod + def _http_request(cls, path=None, method='get', headers=None, + payload=None, files=None): + if headers is None: + headers = {} + if files is None: + files = [] + + request_files = [] + for entry in files: + request_files[entry] = open(entry,'rb') + + if not urlparse.urlsplit(path).scheme: #pylint: disable=E1103 + full_url = urlparse.urljoin(SERVER, path) + else: + full_url = path + + try: + if method == 'get': + log('GET %s' % full_url) + response = requests.get(full_url, + data=json.dumps(payload), + headers=headers, + files=request_files, + timeout=HTTP_TIMEOUT) + elif method == 'post': + log('POST %s' % full_url) + response = requests.post(full_url, + data=json.dumps(payload), + headers=headers, + files=request_files, + timeout=HTTP_TIMEOUT) + else: + log('Unknown method %s' % method, + error=True) + except requests.exceptions.ConnectionError: + raise ZtpError('server connection error') + + return response + + def _get_request(self, url): + # resource or action + headers = {'content-type': CONTENT_TYPE_HTML} + result = self._http_request(url, + headers=headers) + log('Server response to GET request: status=%s' % result.status_code) + + return (result.status_code, + result.headers['content-type'].split(';')[0], + result) + + def _save_file_contents(self, contents, path, url=None): + if path.startswith('/mnt/flash'): + if not url: + raise ZtpError('attempting to save file to %s, but cannot' + 'retrieve content metadata.') + + _, _, metadata = self.get_metadata(url) + metadata = metadata.json() + + usage = flash_usage() + if (metadata['size'] > usage.free): + raise ZtpError('not enough memory on flash for saving %s to %s ' + '(free: %s bytes, required: %s bytes)' % + (url, path, usage.free, metadata['size'])) + elif (metadata['size'] + usage.used > 0.9 * usage.total): + percent = (metadata['size'] + usage.used) * 100.0 / usage.total + log('WARNING: flash disk usage will exceeed %s%% after ' + 'saving %s to %s' % (percent, url, path)) + + log('Writing %s...' % path) + + # Save contents to file + try: + with open(path, 'wb') as result: + for chunk in contents.iter_content(chunk_size=1024): + if chunk: + result.write(chunk) + result.close() + except IOError as err: + raise ZtpError('unable to write %s: %s' % (path, err)) + + # Set permissions + os.chmod(path, 0777) + + def get_config(self): + headers = {'content-type': CONTENT_TYPE_HTML} + result = self._http_request('bootstrap/config', + headers=headers) + log('Server response to GET config: contents=%s' % result.json()) + + status = result.status_code + content = result.headers['content-type'].split(';')[0] + if(status != HTTP_STATUS_OK or + content != CONTENT_TYPE_JSON): + raise ZtpUnexpectedServerResponseError( + 'unexpected reponse from server (status=%s; content-type=%s)' % + (status, content)) + + return (status, content, result) + + def post_nodes(self, node): + headers = {'content-type': CONTENT_TYPE_JSON} + result = self._http_request('nodes', + method='post', + headers=headers, + payload=node) + location = result.headers['location'] \ + if 'location' in result.headers \ + else None + log('Server response to POST nodes: status=%s, location=%s' % + (result.status_code, location)) + + status = result.status_code + content = result.headers['content-type'].split(';')[0] + if(status not in [HTTP_STATUS_CREATED, + HTTP_STATUS_BAD_REQUEST, + HTTP_STATUS_CONFLICT] or + content != CONTENT_TYPE_HTML): + raise ZtpUnexpectedServerResponseError( + 'unexpected reponse from server (status=%s; content-type=%s)' % + (status, content)) + elif status == HTTP_STATUS_BAD_REQUEST: + raise ZtpError('node not found on server (status=%s)' % status) + + return (status, content, location) + + def get_definition(self, location): + headers = {'content-type': CONTENT_TYPE_HTML} + result = self._http_request(location, + headers=headers) + + if result.status_code == HTTP_STATUS_OK: + log('Server response to GET definition: status=%s, contents=%s' % + (result.status_code, result.json())) + else: + log('Server response to GET definition: status=%s' % + result.status_code) + + status = result.status_code + content = result.headers['content-type'].split(';')[0] + if not ((status == HTTP_STATUS_OK and + content == CONTENT_TYPE_JSON) or + (status == HTTP_STATUS_BAD_REQUEST and + content == CONTENT_TYPE_HTML)): + raise ZtpUnexpectedServerResponseError( + 'unexpected reponse from server (status=%s; content-type=%s)' % + (status, content)) + elif status == HTTP_STATUS_BAD_REQUEST: + raise ZtpError('server-side topology check failed (status=%s)' % + status) + + return (status, content, result) + + def get_action(self, action): + status, content, action_response = \ + self._get_request('actions/%s' % action) + + if not ((status == HTTP_STATUS_OK and + content == CONTENT_TYPE_PYTHON) or + (status == HTTP_STATUS_NOT_FOUND and + content == CONTENT_TYPE_HTML)): + raise ZtpUnexpectedServerResponseError( + 'unexpected reponse from server (status=%s; content-type=%s)' % + (status, content)) + elif status == HTTP_STATUS_NOT_FOUND: + raise ZtpError('action not found on server (status=%s)' % status) + + filename = os.path.join(TEMP, action) + self._save_file_contents(action_response, filename) + return filename + + def get_metadata(self, url): + if urlparse.urlsplit(url).scheme: #pylint: disable=E1103 + aux = url.split('/') + if aux[3] != 'meta': + aux = aux[0:3] + ['meta'] + aux[3:] + url = '/'.join(aux) + else: + aux = [x for x in url.split('/') if x] + if aux[0] != 'meta': + url = '/'.join(['meta'] + aux) + + headers = {'content-type': CONTENT_TYPE_HTML} + result = self._http_request(url, + headers=headers) + log('Server response to GET meta: contents=%s' % result.json()) + + status = result.status_code + content = result.headers['content-type'].split(';')[0] + + if not ((status == HTTP_STATUS_OK and + content == CONTENT_TYPE_JSON) or + (status == HTTP_STATUS_NOT_FOUND and + content == CONTENT_TYPE_HTML) or + (status == HTTP_STATUS_INTERNAL_SERVER_ERROR and + content == CONTENT_TYPE_HTML)): + raise ZtpUnexpectedServerResponseError( + 'unexpected reponse from server (status=%s; content-type=%s)' % + (status, content)) + elif status == HTTP_STATUS_NOT_FOUND: + raise ZtpError('metadata not found on server (status=%s)' % + status) + elif status == HTTP_STATUS_INTERNAL_SERVER_ERROR: + raise ZtpError( + 'unable to retrieve metadata from server (status=%s)' % + status) + + return (status, content, result) + + def get_resource(self, url, path): + if not urlparse.urlsplit(url).scheme: #pylint: disable=E1103 + url = urlparse.urljoin(SERVER, url) + + status, content, response = self._get_request(url) + if not ((status == HTTP_STATUS_OK and + content == CONTENT_TYPE_OTHER) or + (status == HTTP_STATUS_NOT_FOUND and + content == CONTENT_TYPE_HTML)): + raise ZtpUnexpectedServerResponseError( + 'unexpected reponse from server (status=%s; content-type=%s)' % + (status, content)) + elif status == HTTP_STATUS_NOT_FOUND: + raise ZtpError('resource not found on server (status=%s)' % status) + + self._save_file_contents(response, path, url) + + +class XmppClient(sleekxmpp.ClientXMPP): + #pylint: disable=W0613, R0904, R0201, R0924 + + def __init__(self, user, domain, password, rooms, + nick, xmpp_server, xmpp_port): + + self.xmpp_jid = '%s@%s' % (user, domain) + self.connected = False + + try: + sleekxmpp.ClientXMPP.__init__(self, self.xmpp_jid, + password) + except sleekxmpp.jid.InvalidJID: + log('Unable to connect XMPP client because of invalid jid: %s' % + self.xmpp_jid, xmpp=False) + return + + self.xmpp_nick = nick + self.xmpp_rooms = rooms + + self.xmpp_rooms = [] + for room in rooms: + self.xmpp_rooms.append('%s@conference.%s' % (room, domain)) + + self.add_event_handler('session_start', self._session_connected) + self.add_event_handler('connect', self._session_connected) + self.add_event_handler('disconnected', self._session_disconnected) + + # Multi-User Chat + self.register_plugin('xep_0045') + # XMPP Ping + self.register_plugin('xep_0199') + # Service Discovery + self.register_plugin('xep_0030') + + log('XmppClient connecting to server...', xmpp=False) + if xmpp_server != None: + self.connect((xmpp_server, xmpp_port), reattempt=False) + else: + self.connect(reattempt=False) + + self.process(block=False) + + retries = 3 + while not self.connected and retries: + # Wait to connect + time.sleep(1) + retries -= 1 + + def _session_connected(self, event): + log('XmppClient: Session connected (%s)' % self.xmpp_jid, + xmpp=False) + self.send_presence() + self.get_roster() + + self.connected = True + + # Joining rooms + for room in self.xmpp_rooms: + self.plugin['xep_0045'].joinMUC(room, + self.xmpp_nick, + wait=True) + log('XmppClient: Joined room %s as %s' % + (room, self.xmpp_nick), + xmpp=False) + + def _session_disconnected(self, event): + log('XmppClient: Session disconnected (%s)' % self.xmpp_jid, + xmpp=False) + self.connected = False + + def message(self, message): + for room in self.xmpp_rooms: + self.send_message(mto=room, + mbody=message, + mtype='groupchat') + +def apply_config(config, node): + global xmpp_client #pylint: disable=W0603 + + log('Applying server config') + + + # XMPP not configured yet + xmpp_config = config.get('xmpp', {}) + + global XMPP_MSG_TYPE #pylint: disable=W0603 + XMPP_MSG_TYPE = xmpp_config.get('msg_type', 'debug') + if XMPP_MSG_TYPE not in ['debug', 'info']: + log('XMPP configuration failed because of unexpected \'msg_type\': ' + '%s not in [\'debug\', \'info\']' % XMPP_MSG_TYPE, error=True, + xmpp=False) + else: + if xmpp_config: + log('Configuring XMPP', xmpp=False) + if ('username' in xmpp_config and + 'domain' in xmpp_config and + 'password' in xmpp_config and + 'rooms' in xmpp_config and + xmpp_config['rooms']): + nick = node.system()['serialnumber'] + if not nick: + # vEOS might not have a serial number configured + nick = node.system()['systemmac'] + xmpp_client = XmppClient(xmpp_config['username'], + xmpp_config['domain'], + xmpp_config['password'], + xmpp_config['rooms'], + nick, + xmpp_config.get('server', None), + xmpp_config.get('port', 5222)) + else: + # XMPP not configured yet + log('XMPP configuration failed because server response ' + 'is missing config details', + error=True, xmpp=False) + else: + log('No XMPP configuration received from server', xmpp=False) + + log_config = config.get('logging', []) + if log_config: + log('Configuring syslog') + syslog_manager.add_handlers(log_config) + else: + log('No XMPP configuration received from server') + + +def execute_action(server, action_details, special_attr): + action = action_details['action'] + + description = '' + if 'description'in action_details: + description = '(%s)' % action_details['description'] + + if action not in sys.modules: + log('Downloading action %s%s' % (action, description)) + filename = server.get_action(action) + + log('Executing action %s' % action) + if 'onstart' in action_details: + log('Action %s: %s' % (action, action_details['onstart']), + xmpp=True) + + try: + if action in sys.modules: + module = sys.modules[action] + else: + module = imp.load_source(action, filename) + + local_attr = action_details['attributes'] \ + if 'attributes' in action_details \ + else [] + ret = module.main(Attributes(local_attr, special_attr)) + if ret: + raise ZtpActionError('action returned %s' % ret) + log('Action executed succesfully (%s)' % action) + if 'onsuccess' in action_details: + log('Action %s: %s' % (action, action_details['onsuccess']), + xmpp=True) + except Exception as err: #pylint: disable=W0703 + if 'onfailure' in action_details: + log('Action %s: %s' % (action, action_details['onfailure']), + xmpp=True) + raise ZtpActionError('executing action failed (%s): %s' % (action, err)) + +def restore_factory_default(): + for filename in [RC_EOS, BOOT_EXTENSIONS]: + if os.path.exists(filename): + os.remove(filename) + + shutil.rmtree(BOOT_EXTENSIONS_FOLDER, ignore_errors=True) + + +def main(): + #pylint: disable=W0603,R0912,R0915 + global syslog_manager + + restore_factory_default() + + syslog_manager = SyslogManager() + server = Server() + + # Retrieve and apply logging/XMPP configuration from server + # XMPP not configured yet + log('Retrieving config from server', xmpp=False) + _, _, config = server.get_config() + + # Creating node + node = Node(server) + + # XMPP not configured yet + log('Config retrieved from server', xmpp=False) + apply_config(config.json(), node) + + # Checking node on server + # XMPP not configured yet + log('Collecting node information', xmpp=False) + _, _, location = server.post_nodes(node.details()) + + # Get definition + _, _, definition = server.get_definition(location) + + # Execute actions + definition = definition.json() + + for attr in ['name', 'actions']: + if attr not in definition: + raise ZtpError('\'%s\' section missing from definition' % attr) + + definition_name = definition['name'] + log('Applying definition %s' % definition_name) + + + special_attr = {} + special_attr['NODE'] = node + for details in definition['actions']: + execute_action(server, details, special_attr) + + log('Definition %s applied successfully' % definition_name) + + # Check for startup-config + if not node.has_startup_config(): + raise ZtpError('startup configuration is missing at the end of the ' + 'bootstrap process') + + log('ZTP bootstrap completed successfully!') + + _exit(0) + + +if __name__ == '__main__': + try: + main() + except ZtpError as exception: + log('''Bootstrap process failed: + %s''' % str(exception), + error=True) + _exit(1) + except KeyboardInterrupt: + log('Bootstrap process keyboard-interrupted', + error=True) + log(sys.exc_info()[0]) + log(traceback.format_exc()) + _exit(1) + except Exception, exception: + log('''Bootstrap process failed because of unknown exception: + %s''' % + exception, error=True) + log(sys.exc_info()[0]) + log(traceback.format_exc()) + _exit(1) \ No newline at end of file -- GitLab