Source code for ztps.bootstrap

#!/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


[docs]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))
[docs] 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:]
[docs] 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:]
[docs] 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
[docs] 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
[docs] 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())
[docs] 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()
[docs] 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)
[docs] 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)
[docs] 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)
[docs] def rc_eos(self): '''Get rc.eos path. Returns: string: rc.eos path ''' return RC_EOS
[docs] def flash(self): '''Get flash path. Returns: string: flash path ''' return FLASH
[docs] def startup_config(self): '''Get startup-config path. Returns: string: startup-config path ''' return STARTUP_CONFIG
[docs] 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
[docs] 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)