2019-12-17 01:04:54 +01:00
|
|
|
# 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
|
|
|
|
|
2019-12-17 16:55:49 +01:00
|
|
|
import socketserver
|
2019-12-17 01:04:54 +01:00
|
|
|
|
|
|
|
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
|
2019-12-17 17:25:32 +01:00
|
|
|
except (ImportError, ModuleNotFoundError):
|
2019-12-17 01:04:54 +01:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2019-12-17 16:55:49 +01:00
|
|
|
class LogRecordStreamHandler(socketserver.StreamRequestHandler):
|
2019-12-17 01:04:54 +01:00
|
|
|
"""
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
2019-12-17 16:55:49 +01:00
|
|
|
class LogRecordSocketReceiver(socketserver.TCPServer):
|
2019-12-17 01:04:54 +01:00
|
|
|
"""
|
|
|
|
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
|
|
|
|
):
|
2019-12-17 16:55:49 +01:00
|
|
|
socketserver.TCPServer.__init__(self, (host, port), handler)
|
2019-12-17 01:04:54 +01:00
|
|
|
|
|
|
|
|
|
|
|
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()
|