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