Skip to content

Commit

Permalink
Initial cleanup of XMPP support
Browse files Browse the repository at this point in the history
  • Loading branch information
dlobato committed Sep 13, 2024
1 parent 6896da9 commit 4525190
Show file tree
Hide file tree
Showing 21 changed files with 30 additions and 506 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Quick Overview
=====================
ZTPServer provides a bootstrap environment for Arista EOS based products. ZTPserver interacts with the ZeroTouch Provisioning (ZTP) mode of Arista EOS. The default ZTP start up mode triggers an unprovisioned Arista EOS nodes to enter a bootstrap readdy state if a valid configuration file is not already present on the internal flash storage.

ZTPServer provides a number of configurable bootstrap operation workflows that extend beyond simply loading an configuration and boot image. It provides the ability to define the target node through the introduction of definitions and templates that call pre-built actions and statically defined or dynamically generated attributes. The attributes and actions can also be extended to provide custom functionality that are specific to a given implementation. ZTPServer also provides a topology validation engine with a simple syntax to express LLDP neighbor adjacencies. It is written mostly in Python and leverages standard protocols like DHCP and DHCP options for boot functions, HTTP for bi-directional transport, and XMPP and syslog for logging. Most of the files that the user interacts with are YAML based.
ZTPServer provides a number of configurable bootstrap operation workflows that extend beyond simply loading an configuration and boot image. It provides the ability to define the target node through the introduction of definitions and templates that call pre-built actions and statically defined or dynamically generated attributes. The attributes and actions can also be extended to provide custom functionality that are specific to a given implementation. ZTPServer also provides a topology validation engine with a simple syntax to express LLDP neighbor adjacencies. It is written mostly in Python and leverages standard protocols like DHCP and DHCP options for boot functions, HTTP for bi-directional transport, and syslog for logging. Most of the files that the user interacts with are YAML based.

ZTPServer Features
==================
Expand All @@ -15,7 +15,7 @@ ZTPServer Features
* Config and device templates with resource allocation for dynamic deployments
* Zero touch replacement and upgrade capabilities
* User extensible actions
* Email, XMPP, syslog based logging and accounting of all processes
* Syslog based logging and accounting of all processes

Docs
====
Expand Down
179 changes: 9 additions & 170 deletions client/bootstrap
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ from string import Template # pylint: disable=W0402
from subprocess import PIPE

import jsonrpclib
import sleekxmpp
from six import PY2, PY3, binary_type, raise_from, text_type
from six.moves import urllib

Expand Down Expand Up @@ -107,29 +106,14 @@ RESTORE_FACTORY_FLASH = True

# 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 comment out the following lines:
for logger in ["sleekxmpp.xmlstream.xmlstream", "sleekxmpp.basexmpp"]:
xmpp_log = logging.getLogger(logger)
xmpp_log.addHandler(logging.NullHandler())
# --------------------------------XMPP------------------------

# --------------------------------SYSLOG----------------------
# Comment out this section in order to enable syslog debug
# logging
logging.raiseExceptions = False


# --------------------------------XMPP------------------------


# ------------------Utilities---------------------------------
# pylint: disable=C0103,C0123,R1705
# ensure_str backport from six 1.6 as older EOS packs six 1.11
Expand Down Expand Up @@ -182,15 +166,6 @@ def ensure_text(s, encoding="utf-8", errors="strict"):
def _exit(code):
# pylint: disable=W0702

# Wait for XMPP messages to drain
time.sleep(3)

if xmpp_client:
try:
xmpp_client.abort()
except:
pass

if not RESTORE_FACTORY_FLASH:
for path in [STARTUP_CONFIG, RC_EOS, BOOT_EXTENSIONS]:
filename = os.path.basename(path)
Expand Down Expand Up @@ -239,30 +214,13 @@ def _exit(code):
sys.stderr.flush()

# pylint: disable=W0212
# Need to close background sleekxmpp threads as well
sys.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 = "{}: {} - {}{}".format(
SYSTEM_ID if SYSTEM_ID else "N/A", timestamp, "ERROR: " if error else "", msg
)

if xmpp and xmpp_client and xmpp_client.connected:
xmpp_client.message(xmpp_msg)

def log(msg, error=False):
if SYSTEM_ID:
syslog_msg = "{}: {}".format(SYSTEM_ID, msg)
else:
Expand Down Expand Up @@ -746,7 +704,7 @@ class Node:
self._append_lines(RC_EOS, lines)

def log_msg(self, msg, error=False):
"""Log message via configured syslog/XMPP.
"""Log message via configured syslog.
Args:
msg (string): Message
Expand Down Expand Up @@ -1265,129 +1223,13 @@ class Server:
self._save_file_contents(response, path, url)


class XmppClient(sleekxmpp.ClientXMPP):
def __init__(self, user, domain, password, rooms, nick, xmpp_server, xmpp_port):
self.xmpp_jid = "{}@{}".format(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: {}".format(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("{}@conference.{}".format(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 is not 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, _):
log("XmppClient: Session connected ({})".format(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 {} as {}".format(room, self.xmpp_nick), xmpp=False)

def _session_disconnected(self, _):
log("XmppClient: Session disconnected ({})".format(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': "
"{} not in ['debug', 'info']".format(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 load_module(module_name, source_path):
Expand Down Expand Up @@ -1416,7 +1258,7 @@ def execute_action(server, action_details, special_attr):

log("Executing action {}".format(action))
if "onstart" in action_details:
log("Action {}: {}".format(action, action_details["onstart"]), xmpp=True)
log("Action {}: {}".format(action, action_details["onstart"]))

try:
if action not in sys.modules:
Expand All @@ -1432,10 +1274,10 @@ def execute_action(server, action_details, special_attr):

log("Action executed succesfully ({})".format(action))
if "onsuccess" in action_details:
log("Action {}: {}".format(action, action_details["onsuccess"]), xmpp=True)
log("Action {}: {}".format(action, action_details["onsuccess"]))
except Exception as err: # pylint: disable=W0703
if "onfailure" in action_details:
log("Action {}: {}".format(action, action_details["onfailure"]), xmpp=True)
log("Action {}: {}".format(action, action_details["onfailure"]))
raise_from(ZtpActionError("executing action failed ({}): {}".format(action, err)), err)


Expand All @@ -1459,21 +1301,18 @@ def main():
syslog_manager = SyslogManager()
server = Server()

# Retrieve and apply logging/XMPP configuration from server
# XMPP not configured yet
log("Retrieving config from server", xmpp=False)
# Retrieve and apply logging configuration from server
log("Retrieving config from server")
_, _, config = server.get_config()

# Creating node
node = Node(server)

# XMPP not configured yet
log("Config retrieved from server", xmpp=False)
log("Config retrieved from server")
apply_config(config.json(), node)

# Checking node on server
# XMPP not configured yet
log("Collecting node information", xmpp=False)
log("Collecting node information")
_, _, location = server.post_nodes(node.details())

# Get definition
Expand Down
8 changes: 0 additions & 8 deletions conf/bootstrap.conf
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,5 @@
# level: <LOGGING_LEVEL>
# ...
#
# xmpp:
# username: <XMPP_USERNAME>
# password: <XMPP_PASSWORD>
# domain: <XMPP_DOMAIN>
# msg_type : [debug|info] # default is 'debug'
# rooms:
# - <XMPP_ROOM>
# ...
#
# See documentation for the detailed list of possible values.
11 changes: 0 additions & 11 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,6 @@ GET bootstrap logging configuration
//by default
“level”*: <DEBUG | CRITICAL | ...>,
} ]
},
“xmpp”*:{
“server”: <IP or HOSTNAME>,
“port”: <PORT>, // Optional, default 5222
“username”*: <USERNAME>,
“domain”*: <DOMAIN>,
“password”*: <PASSWORD>,
“nickname”: <NICKNAME>, // Optional, default ‘username’
“rooms”*: [ <ROOM>, … ]
}
}
}

**Note**: \* Items are mandatory (even if value is empty list/dict)
Expand Down
11 changes: 0 additions & 11 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -170,17 +170,6 @@ Bootstrap configuration
level: CRITICAL
- destination: 10.0.1.1:9000
level: CRITICAL
xmpp:
domain: im.ztps-test.com
username: bootstrap
password: eosplus
rooms:
- ztps
- ztps-room2
.. note::

In order for XMPP logging to work, a non-EOS user need to be connected to the room specified in bootstrap.conf, before the ZTP process starts. The room has to be created (by the non-EOS user) before the bootstrap client starts logging the ZTP process via XMPP.
.. _static_provisioning:
Expand Down
47 changes: 0 additions & 47 deletions docs/cookbook/clientLogging.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,50 +45,3 @@ The node will request the contents of the ``bootstrap.conf`` when it performs
send logs to the ``destination(s):`` listed under ``logging:``.

.. End of Configure Syslog Logging
Configure XMPP Logging
------------------------

Objective
^^^^^^^^^

I want to send client logs to specific XMPP server rooms.

Solution
^^^^^^^^

.. code-block:: console
# Edit the bootstrap configuration file
admin@ztpserver:~# vi /usr/share/ztpserver/bootstrap/bootstrap.conf
Add any XMPP servers and associated rooms:

.. code-block:: yaml
---
xmpp:
domain: <XMPP-SERVER-URL>
username: bootstrap
password: eosplus
rooms:
- ztps
- devops
- admins
Explanation
^^^^^^^^^^^

The node will request the contents of the ``bootstrap.conf`` when it performs
``GET /bootstrap/config`` file and try to join the rooms listed with the
credentials provided. Typically when joining a room, you would use a string
like, ``[email protected]``. Be sure to just provide the
``domain: xmpp-server.example.com`` leaving out the ``conference`` prefix.

.. note:: In order for XMPP logging to work, a non-EOS user need to be connected
to the room specified in bootstrap.conf, before the ZTP process starts.
The room has to be created (by the non-EOS user) before the bootstrap
client starts logging the ZTP process via XMPP.

.. End of Configure XMPP Logging
Loading

0 comments on commit 4525190

Please sign in to comment.