1
0
mirror of https://github.com/CumulusNetworks/ifupdown2.git synced 2024-05-06 15:54:50 +00:00

233 lines
7.9 KiB
Python
Raw Normal View History

# Copyright (C) 2017, 2018, 2019 Cumulus Networks, Inc. all rights reserved
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; version 2.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
#
# https://www.gnu.org/licenses/gpl-2.0-standalone.html
#
# Author:
# Julien Fortin, julien@cumulusnetworks.com
#
# ifupdown2 client-side
#
import struct
import pickle
import SocketServer
import logging
import logging.handlers
import os
import re
import sys
import json
import socket
import signal
try:
from ifupdown2.lib.io import SocketIO
from ifupdown2.lib.status import Status
from ifupdown2.lib.log import LogManager, root_logger
from ifupdown2.lib.exceptions import ExitWithStatus, ExitWithStatusAndError
from ifupdown2.ifupdown.argv import Parse
except:
from lib.status import Status
from lib.io import SocketIO
from lib.log import LogManager, root_logger
from lib.exceptions import ExitWithStatus, ExitWithStatusAndError
from ifupdown.argv import Parse
class LogRecordStreamHandler(SocketServer.StreamRequestHandler):
"""
Handler for a streaming logging request.
This basically logs the record using whatever logging policy is configured
locally.
"""
def handle(self):
"""
Handle multiple requests - each expected to be a 4-byte length,
followed by the LogRecord in pickle format.
"""
while True:
chunk = self.connection.recv(4)
if len(chunk) < 4:
break
slen = struct.unpack(">L", chunk)[0]
chunk = self.connection.recv(slen)
while len(chunk) < slen:
chunk = chunk + self.connection.recv(slen - len(chunk))
record = logging.makeLogRecord(pickle.loads(chunk))
logging.getLogger(record.name).handle(record)
class LogRecordSocketReceiver(SocketServer.TCPServer):
"""
Simple TCP socket-based logging receiver. In ifupdown2d context, the running
daemon is the "sender" and the client is the "receiver". The TCPServer is
setup on the client/receiver side, the daemon will connect to the server to
transmit and stream LogRecord to a socket.
"""
allow_reuse_address = True
def __init__(
self,
host="localhost",
handler=LogRecordStreamHandler,
port=LogManager.DEFAULT_TCP_LOGGING_PORT
):
SocketServer.TCPServer.__init__(self, (host, port), handler)
class Client(SocketIO):
def __init__(self, argv):
SocketIO.__init__(self)
# we setup our log receiver which reads LogRecord from a socket
# thus handing the logging-handling to the logging module and it's
# dedicated classes.
self.socket_receiver = LogRecordSocketReceiver()
self.stdin = None
self.argv = argv
# First we need to set the correct log level for the client.
# Unfortunately the only reliable way to do this is to use our main
# argument parser. We can't simply have a parse to catch -v and -d, a
# simple command like "ifup -av" wouldn't be recognized...
# Ideally it would be great to be able to send the Namespace returned by
# parse_args, and send it on the socket in pickle format. Unfortunately
# this might be a serious security issue. It needs to be studied and
# evaluated a bit more, that way we would save time and resources only
# parsing argv once.
args_parse = Parse(argv)
args_parse.validate()
# store the args namespace to send it to the daemon, we don't want
# the daemon to spend time to parsing argv again...
self.args = args_parse.get_args()
LogManager.get_instance().start_client_logging(self.args)
root_logger.info("starting ifupdown2 client...")
self.uds = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
self.uds.connect("/var/run/ifupdown2d/uds")
except socket.error:
self.__shutdown()
sys.stderr.write("""
ERROR: %s could not connect to ifupdown2 daemon
Try starting ifupdown2 daemon with:
sudo systemctl start ifupdown2
To configure ifupdown2d to start when the box boots:
sudo systemctl enable ifupdown2\n\n""" % argv[0])
raise ExitWithStatus(status=Status.Client.STATUS_COULD_NOT_CONNECT)
signal.signal(signal.SIGINT, self.__signal_handler)
signal.signal(signal.SIGTERM, self.__signal_handler)
signal.signal(signal.SIGQUIT, self.__signal_handler)
try:
self.SO_PEERCRED = socket.SO_PEERCRED
except AttributeError:
# powerpc is the only non-generic we care about. alpha, mips,
# sparc, and parisc also have non-generic values.
machine = os.uname()[4]
if re.search(r"^(ppc|powerpc)", machine):
self.SO_PASSCRED = 20
self.SO_PEERCRED = 21
else:
self.SO_PASSCRED = 16
self.SO_PEERCRED = 17
try:
self.uds.setsockopt(socket.SOL_SOCKET, self.SO_PASSCRED, 1)
except Exception as e:
self.__shutdown()
raise Exception("setsockopt: %s" % str(e))
self.daemon_pid, _, _ = self.get_socket_peer_cred(self.uds)
if self.daemon_pid < 0:
self.__shutdown()
raise ExitWithStatusAndError(
status=Status.Client.STATUS_NO_PID,
message="could not get ifupdown2 daemon PID"
)
root_logger.info("connection to ifupdown2d successful (server pid %s)" % self.daemon_pid)
def __shutdown(self):
try:
self.uds.close()
self.uds = None
except:
pass
try:
self.socket_receiver.server_close()
self.socket_receiver = None
except:
pass
def __signal_handler(self, sig, frame):
""" Forward all signals to daemon """
if self.daemon_pid > 0:
os.kill(self.daemon_pid, sig)
def __get_stdin(self):
"""
If stdin data is provided we need to store it to forward it to the
daemon
"""
if hasattr(self.args, "interfacesfile") and self.args.interfacesfile == "-":
return sys.stdin.read()
def run(self):
try:
# First we need to send the user request to the daemon (argv + stdin)
self.tx_data(self.uds, json.dumps({
"argv": self.argv,
"stdin": self.__get_stdin()
}))
# Then "handle_request" will block until the daemon closes
# the channel, meaning that the request was processed.
self.socket_receiver.handle_request()
self.socket_receiver.server_close()
# Next the daemon should send us a dictionary containing stdout and
# stderr buffers as well as the request's exit status. We print those
# buffers in the correct channel and exit with the request status.
response = self.rx_json_packet(self.uds)
if response:
sys.stdout.write(response.get("stdout", ""))
sys.stderr.write(response.get("stderr", ""))
status = response.get("status", Status.Client.STATUS_EMPTY)
else:
status = Status.Client.STATUS_EMPTY
return status
finally:
self.__shutdown()