mirror of
https://github.com/xcat2/confluent.git
synced 2026-09-29 16:50:57 +00:00
Compare commits
164 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a9ba385996 | |||
| 29a9d417d6 | |||
| 7347125b6f | |||
| 66347a937e | |||
| ee01dfd05e | |||
| 611e1f5bc3 | |||
| 7622145fea | |||
| 678bd53857 | |||
| 3295778566 | |||
| b3acf011bd | |||
| dc4cafc29f | |||
| 3340585fb4 | |||
| 4456767122 | |||
| 556bb1d0ff | |||
| a201e9886e | |||
| f82993efe7 | |||
| f8366a50ef | |||
| d369dcac55 | |||
| 3b2d92a219 | |||
| ffacb66c62 | |||
| 2073f421da | |||
| ed2eed66dc | |||
| d8f9b6c8e6 | |||
| d9a18a7bf6 | |||
| 439a930188 | |||
| 00b2afd42b | |||
| 90c2a4cf73 | |||
| c691dc7159 | |||
| a7c188b812 | |||
| 5ba43ecaa0 | |||
| 2f53d3bde6 | |||
| 9fe9e8672a | |||
| 0fe60175f3 | |||
| d411041243 | |||
| cc101d12bc | |||
| 2f08ee81f2 | |||
| 1e9231eea6 | |||
| 24cb05e535 | |||
| 72b95abb4f | |||
| 57a170d0d8 | |||
| daeabc6fe5 | |||
| 28a8f6f0d6 | |||
| 7542897b43 | |||
| c69952265f | |||
| 01cc86fa55 | |||
| d6e3c7e837 | |||
| dcb6aeca65 | |||
| 7bc76b62e6 | |||
| db313628c5 | |||
| f260323d2f | |||
| d60bc7f524 | |||
| ff0d4cdadf | |||
| db6475c4da | |||
| 6d27e8a009 | |||
| f363796439 | |||
| dec118a985 | |||
| 38eb0d7b10 | |||
| ae338daa43 | |||
| 6ad3f0d70c | |||
| c0b9bb3ab1 | |||
| b32755b0d3 | |||
| c54ac530e1 | |||
| 8990622470 | |||
| 93a35d7e77 | |||
| c49b2fd8ab | |||
| 3ce2a5bc26 | |||
| 69d984b9dc | |||
| a123165712 | |||
| b91b10552c | |||
| 779b07d2c2 | |||
| df73c14475 | |||
| f78b301143 | |||
| 9b00fe5521 | |||
| 13a6444541 | |||
| 52db46be93 | |||
| 550dfbf6a0 | |||
| e0951b11a6 | |||
| 1a87701fee | |||
| e185f2224f | |||
| 69beaad3c9 | |||
| 74dda48513 | |||
| 08b2e1d008 | |||
| 582842aec8 | |||
| 63307c331e | |||
| 318608cde3 | |||
| ef7d2414ad | |||
| 722a0b874a | |||
| 1deb76989e | |||
| 480d399f44 | |||
| 07369667f7 | |||
| e1d4b72f32 | |||
| 86783a2f12 | |||
| 99063eb049 | |||
| 0975bd9e62 | |||
| 3058dd4141 | |||
| 21c9158491 | |||
| e6c19388a2 | |||
| 048780e16d | |||
| 61d7a49163 | |||
| f8b8ce3847 | |||
| a0a5887214 | |||
| ccaf22f44f | |||
| 72c4868073 | |||
| afb6356f9d | |||
| 6e6ac67b3d | |||
| 99d10896e8 | |||
| 488f23e3ed | |||
| 6ca62cbb35 | |||
| 45bc9788b4 | |||
| 289c31e7ac | |||
| 1a684f2012 | |||
| a4229fc58d | |||
| 31c1a865dc | |||
| ff84fcf6e9 | |||
| 56dfb6dc6b | |||
| d7577a04a7 | |||
| b72d6c9cfc | |||
| 523c93dfc3 | |||
| 04e983a2d3 | |||
| 2464e0ff4f | |||
| c196bf9d55 | |||
| 12d886a4f6 | |||
| 6a26ece782 | |||
| 3cbac38d57 | |||
| 224f349053 | |||
| 9d361d376d | |||
| ec39de3df0 | |||
| a3b768c70f | |||
| 4f75d4942b | |||
| 4d2f36917c | |||
| a2a50d34d1 | |||
| 041008a524 | |||
| 5923feaa18 | |||
| 73216fc062 | |||
| 100944490c | |||
| 61b07e0af4 | |||
| 53760ab5dd | |||
| d3e7a49f92 | |||
| 1f688ead28 | |||
| d20c5ac6eb | |||
| 4484216198 | |||
| e1efd6a9c5 | |||
| 58d5209595 | |||
| 53c918042a | |||
| 9148a841b5 | |||
| 6ebb6de107 | |||
| 20292cdfd0 | |||
| b07da455c2 | |||
| cc9a81103b | |||
| 21155d2091 | |||
| 6c0d7ea60e | |||
| 174d204607 | |||
| 2826abb7ab | |||
| 5adb5fa780 | |||
| 5de063212f | |||
| 073f6d1389 | |||
| f755ba9f91 | |||
| cf8c01ef13 | |||
| 8b12047ae0 | |||
| f0a779764d | |||
| 0ad7e99efe | |||
| 24a76612ae | |||
| 6c9c58f464 | |||
| 3125f4171b |
@@ -0,0 +1,8 @@
|
||||
FROM almalinux:10
|
||||
RUN ["yum", "-y","update"]
|
||||
RUN ["yum", "-y","install","gcc","make","rpm-build","python3-devel","python3-setuptools","createrepo","python3", "perl", "perl-DBI", "perl-JSON", "perl-XML-LibXML", "pinentry-tty", "rpm-sign", "git", "golang"]
|
||||
ADD rpmmacro /root/.rpmmacros
|
||||
ADD buildpackages.sh /bin/
|
||||
#VOLUME ["/rpms", "/srpms"]
|
||||
CMD ["/bin/bash","/bin/buildpackages.sh"]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
for package in /srpms/*; do
|
||||
rpmbuild --rebuild $package
|
||||
done
|
||||
find ~/rpmbuild/RPMS -type f -exec cp {} /rpms/ \;
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
%_gpg_digest_algo sha256
|
||||
%_gpg_name Lenovo Scalable Infrastructure
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
FROM almalinux:8
|
||||
RUN ["yum", "-y","update"]
|
||||
RUN ["yum", "-y","install","gcc","make","rpm-build","python3-devel","python3-setuptools","createrepo","python3", "perl", "perl-DBI", "perl-JSON", "perl-Net-DNS", "perl-DB_File", "perl-XML-LibXML", "rpm-sign", "git", "fuse-devel","libcurl-devel"]
|
||||
ADD rpmmacro /root/.rpmmacros
|
||||
ADD buildpackages.sh /bin/
|
||||
#VOLUME ["/rpms", "/srpms"]
|
||||
CMD ["/bin/bash","/bin/buildpackages.sh"]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
for package in /srpms/*; do
|
||||
rpmbuild --rebuild $package
|
||||
done
|
||||
find ~/rpmbuild/RPMS -type f -exec cp {} /rpms/ \;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
%_gpg_digest_algo sha256
|
||||
%_gpg_name Lenovo Scalable Infrastructure
|
||||
@@ -0,0 +1,10 @@
|
||||
FROM almalinux:9
|
||||
RUN ["yum", "-y","update"]
|
||||
RUN ["yum", "-y","install","gcc","make","rpm-build","python3-devel","python3-setuptools","createrepo","python3", "perl", "perl-DBI", "perl-JSON", "perl-Net-DNS", "perl-DB_File", "perl-XML-LibXML", "pinentry-tty", "rpm-sign", "epel-release", "git"]
|
||||
RUN ["crb", "enable"]
|
||||
RUN ["yum", "-y","install","fuse-devel","libcurl-devel"]
|
||||
ADD rpmmacro /root/.rpmmacros
|
||||
ADD buildpackages.sh /bin/
|
||||
#VOLUME ["/rpms", "/srpms"]
|
||||
CMD ["/bin/bash","/bin/buildpackages.sh"]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
for package in /srpms/*; do
|
||||
rpmbuild --rebuild $package
|
||||
done
|
||||
find ~/rpmbuild/RPMS -type f -exec cp {} /rpms/ \;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
%_gpg_digest_algo sha256
|
||||
%_gpg_name Lenovo Scalable Infrastructure
|
||||
@@ -0,0 +1,12 @@
|
||||
FROM ubuntu:noble
|
||||
ADD stdeb.patch /tmp/
|
||||
ADD buildapt.sh /bin/
|
||||
ADD distributions.tmpl /bin/
|
||||
RUN ["apt-get", "update"]
|
||||
RUN ["apt-get", "install", "-y", "reprepro", "python3-stdeb", "gnupg-agent", "devscripts", "debhelper", "libsoap-lite-perl", "libdbi-perl", "quilt", "git", "python3-pyparsing", "python3-dnspython", "python3-eventlet", "python3-netifaces", "python3-paramiko", "dh-python", "libjson-perl", "ronn", "alien", "gcc", "make"]
|
||||
RUN ["mkdir", "-p", "/sources/git/"]
|
||||
RUN ["mkdir", "-p", "/debs/"]
|
||||
RUN ["mkdir", "-p", "/apt/"]
|
||||
RUN ["bash", "-c", "patch -p1 < /tmp/stdeb.patch"]
|
||||
CMD ["/bin/bash", "/bin/buildapt.sh"]
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#cp -a /sources/git /tmp
|
||||
for builder in $(find /sources/git -name builddeb); do
|
||||
cd $(dirname $builder)
|
||||
./builddeb /debs/
|
||||
done
|
||||
cp /prebuilt/* /debs/
|
||||
cp /osd/*.deb /debs/
|
||||
mkdir -p /apt/conf/
|
||||
CODENAME=$(grep VERSION_CODENAME= /etc/os-release | sed -e 's/.*=//')
|
||||
if [ -z "$CODENAME" ]; then
|
||||
CODENAME=$(grep VERSION= /etc/os-release | sed -e 's/.*(//' -e 's/).*//')
|
||||
fi
|
||||
if ! grep $CODENAME /apt/conf/distributions; then
|
||||
sed -e s/#CODENAME#/$CODENAME/ /bin/distributions.tmpl >> /apt/conf/distributions
|
||||
fi
|
||||
cd /apt/
|
||||
reprepro includedeb $CODENAME /debs/*.deb
|
||||
for dsc in /debs/*.dsc; do
|
||||
reprepro includedsc $CODENAME $dsc
|
||||
done
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
Origin: Lenovo HPC Packages
|
||||
Label: Lenovo HPC Packages
|
||||
Codename: #CODENAME#
|
||||
Architectures: amd64 source
|
||||
Components: main
|
||||
Description: Lenovo HPC Packages
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
diff -urN t/usr/lib/python3/dist-packages/stdeb/cli_runner.py t.patch/usr/lib/python3/dist-packages/stdeb/cli_runner.py
|
||||
--- t/usr/lib/python3/dist-packages/stdeb/cli_runner.py 2024-06-11 18:30:13.930328999 +0000
|
||||
+++ t.patch/usr/lib/python3/dist-packages/stdeb/cli_runner.py 2024-06-11 18:32:05.392731405 +0000
|
||||
@@ -8,7 +8,7 @@
|
||||
from ConfigParser import SafeConfigParser # noqa: F401
|
||||
except ImportError:
|
||||
# python 3.x
|
||||
- from configparser import SafeConfigParser # noqa: F401
|
||||
+ from configparser import ConfigParser # noqa: F401
|
||||
from distutils.util import strtobool
|
||||
from distutils.fancy_getopt import FancyGetopt, translate_longopt
|
||||
from stdeb.util import stdeb_cmdline_opts, stdeb_cmd_bool_opts
|
||||
diff -urN t/usr/lib/python3/dist-packages/stdeb/util.py t.patch/usr/lib/python3/dist-packages/stdeb/util.py
|
||||
--- t/usr/lib/python3/dist-packages/stdeb/util.py 2024-06-11 18:32:53.864776149 +0000
|
||||
+++ t.patch/usr/lib/python3/dist-packages/stdeb/util.py 2024-06-11 18:33:02.063952870 +0000
|
||||
@@ -730,7 +730,7 @@
|
||||
example.
|
||||
"""
|
||||
|
||||
- cfg = ConfigParser.SafeConfigParser()
|
||||
+ cfg = ConfigParser.ConfigParser()
|
||||
cfg.read(cfg_files)
|
||||
if cfg.has_section(module_name):
|
||||
section_items = cfg.items(module_name)
|
||||
@@ -801,7 +801,7 @@
|
||||
if len(cfg_files):
|
||||
check_cfg_files(cfg_files, module_name)
|
||||
|
||||
- cfg = ConfigParser.SafeConfigParser(cfg_defaults)
|
||||
+ cfg = ConfigParser.ConfigParser(cfg_defaults)
|
||||
for cfg_file in cfg_files:
|
||||
with codecs.open(cfg_file, mode='r', encoding='utf-8') as fd:
|
||||
cfg.readfp(fd)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
cd ~/confluent
|
||||
git pull
|
||||
rm ~/rpmbuild/RPMS/noarch/*osdeploy*
|
||||
rm ~/rpmbuild/SRPMS/*osdeploy*
|
||||
sh confluent_osdeploy/buildrpm-aarch64
|
||||
mkdir -p $HOME/el9/
|
||||
mkdir -p $HOME/el10/
|
||||
podman run --rm -it -v $HOME:/build el9build bash /build/confluent/confluent_vtbufferd/buildrpm /build/el9/
|
||||
|
||||
@@ -133,6 +133,8 @@ def print_help():
|
||||
|
||||
def updatestatus(stateinfo={}):
|
||||
global powerstate, powertime, clearpowermessage
|
||||
if opts.headless:
|
||||
return
|
||||
status = consolename
|
||||
info = []
|
||||
for statekey in stateinfo:
|
||||
@@ -455,8 +457,10 @@ def do_command(command, server):
|
||||
currconsole = targpath
|
||||
startrequest = {'operation': 'start', 'path': targpath,
|
||||
'parameters': {}}
|
||||
height, width = struct.unpack(
|
||||
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'....'))[:2]
|
||||
height, width = 31, 100
|
||||
if not opts.headless:
|
||||
height, width = struct.unpack(
|
||||
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'....'))[:2]
|
||||
startrequest['parameters']['width'] = width
|
||||
startrequest['parameters']['height'] = height
|
||||
for param in argv[2:]:
|
||||
@@ -624,17 +628,19 @@ def startconsole(nodename):
|
||||
signal.signal(signal.SIGWINCH, do_resize)
|
||||
didconsole = True
|
||||
consolename = nodename
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
|
||||
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl | os.O_NONBLOCK)
|
||||
if not opts.headless:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
|
||||
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl | os.O_NONBLOCK)
|
||||
inconsole = True
|
||||
check_automation('') # give any leading 'sends' a chance
|
||||
|
||||
|
||||
def quitconfetty(code=0, fullexit=False, fixterm=True):
|
||||
global inconsole
|
||||
global currconsole
|
||||
global didconsole
|
||||
if fixterm or didconsole:
|
||||
if (fixterm or didconsole) and not opts.headless:
|
||||
currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
|
||||
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl & ~os.O_NONBLOCK)
|
||||
if oldtcattr is not None:
|
||||
@@ -844,6 +850,20 @@ def check_escape_seq(currinput, filehandle):
|
||||
currinput += filehandle.read()
|
||||
return currinput
|
||||
|
||||
automation_directives = []
|
||||
current_automation_directive = None
|
||||
automation_map = {
|
||||
'<up>': '\x1b[A',
|
||||
'<down>': '\x1b[B',
|
||||
'<right>': '\x1b[C',
|
||||
'<left>': '\x1b[D',
|
||||
'<enter>': '\r',
|
||||
'<esc>': '\x1b',
|
||||
'<tab>': '\t',
|
||||
}
|
||||
|
||||
|
||||
|
||||
parser = optparse.OptionParser()
|
||||
parser.add_option("-s", "--server", dest="netserver",
|
||||
help="Confluent instance to connect to",
|
||||
@@ -851,12 +871,64 @@ parser.add_option("-s", "--server", dest="netserver",
|
||||
parser.add_option("-c", "--control", dest="controlpath",
|
||||
help="Path to offer terminal control",
|
||||
metavar="PATH")
|
||||
parser.add_option('-a', '--automation', type='string', default=None,
|
||||
help='Specify an automation script to run', metavar='SCRIPT')
|
||||
parser.add_option('-e', '--headless', action='store_true', default=False,
|
||||
help='Run in headless mode, which is designed for use with '
|
||||
'automation scripts and disables interactive features')
|
||||
parser.add_option(
|
||||
'-m', '--mintime', default=0,
|
||||
help='Minimum time to run or else pause for input (used to keep a '
|
||||
'terminal from closing quickly on error)')
|
||||
opts, shellargs = parser.parse_args()
|
||||
|
||||
def parse_automation_script(script, session_node):
|
||||
global current_automation_directive
|
||||
for line in script.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if line.startswith('exit'):
|
||||
automation_directives.append(('exit', None))
|
||||
continue
|
||||
if ' ' not in line:
|
||||
sys.stderr.write("Invalid line in automation script: %s\n" % line)
|
||||
continue
|
||||
directive, arg = line.split(' ', 1)
|
||||
directive = directive.strip().lower()
|
||||
if directive not in ('expect', 'send', 'forget'):
|
||||
sys.stderr.write("Unknown directive in automation script: %s\n" % directive)
|
||||
continue
|
||||
arg = arg.strip()
|
||||
origarg = arg
|
||||
if arg[0] not in ('"', "'"):
|
||||
arg = '"' + arg + '"'
|
||||
if arg[0] == "'" and arg[-1] == "'":
|
||||
# do not process '<>' sequences in single quotes
|
||||
arg = arg[1:-1]
|
||||
arg = bytes(arg, "utf-8").decode("unicode_escape")
|
||||
elif arg[0] == '"' and arg[-1] == '"':
|
||||
arg = bytes(arg[1:-1], "utf-8").decode("unicode_escape")
|
||||
for key, value in automation_map.items():
|
||||
arg = arg.replace(key, value)
|
||||
arg = re.sub(r'<env:(\w+)>', lambda m: os.environ[m.group(1)], arg)
|
||||
if '{' in arg: # support confluent expressions
|
||||
for res in session.create('/nodes/{0}/attributes/expression'.format(session_node),
|
||||
{'expression': arg}):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
sys.exit(1)
|
||||
if 'value' in res:
|
||||
arg = res['value']
|
||||
automation_directives.append((directive, arg, origarg))
|
||||
if automation_directives:
|
||||
current_automation_directive = automation_directives.pop(0)
|
||||
if opts.headless and current_automation_directive[0] == 'expect':
|
||||
sys.stdout.write(f'Expecting {repr(current_automation_directive[2])}\r\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
|
||||
username = None
|
||||
passphrase = None
|
||||
def server_connect():
|
||||
@@ -909,6 +981,7 @@ def main():
|
||||
# sys.stdout.write('\x1b[H\x1b[J')
|
||||
# sys.stdout.flush()
|
||||
global powerstate, powertime, clearpowermessage
|
||||
|
||||
|
||||
|
||||
if sys.stdout.isatty():
|
||||
@@ -925,6 +998,9 @@ def main():
|
||||
targ, session_node = get_session_node(shellargs)
|
||||
if session_node is not None:
|
||||
consoleonly = True
|
||||
if opts.automation:
|
||||
with open(opts.automation) as f:
|
||||
parse_automation_script(f.read(), session_node)
|
||||
do_command("start %s" % targ, netserver)
|
||||
doexit = True
|
||||
elif shellargs:
|
||||
@@ -936,9 +1012,13 @@ def main():
|
||||
|
||||
while inconsole or not doexit:
|
||||
if inconsole:
|
||||
if opts.headless:
|
||||
handles = [session.connection]
|
||||
else:
|
||||
handles = (sys.stdin, session.connection)
|
||||
try:
|
||||
rdylist, _, _ = select.select(
|
||||
(sys.stdin, session.connection), (), (), 10)
|
||||
handles, (), (), 10)
|
||||
except select.error:
|
||||
rdylist = ()
|
||||
for fh in rdylist:
|
||||
@@ -976,6 +1056,72 @@ fgcolor = None
|
||||
bgcolor = None
|
||||
fgshifted = False
|
||||
pendseq = ''
|
||||
automation_check = ''
|
||||
|
||||
def check_automation(data):
|
||||
global automation_check
|
||||
global current_automation_directive
|
||||
if type(data) != str:
|
||||
data = data.decode('utf-8', errors='ignore')
|
||||
while current_automation_directive:
|
||||
if current_automation_directive[0] == 'forget' and data:
|
||||
current_automation_directive = None
|
||||
automation_check = ''
|
||||
if automation_directives:
|
||||
current_automation_directive = automation_directives.pop(0)
|
||||
if opts.headless and current_automation_directive[0] == 'expect':
|
||||
sys.stdout.write(f'Expecting {repr(current_automation_directive[2])}\n')
|
||||
sys.stdout.flush()
|
||||
return
|
||||
if current_automation_directive[0] == 'expect':
|
||||
expected = current_automation_directive[1]
|
||||
combined = automation_check + data
|
||||
if expected and expected in combined:
|
||||
data = data[combined.rindex(expected) + len(expected):]
|
||||
|
||||
if opts.headless:
|
||||
sys.stdout.write(f'Detected {repr(current_automation_directive[2])}\r\n')
|
||||
sys.stdout.flush()
|
||||
current_automation_directive = None
|
||||
automation_check = ''
|
||||
if automation_directives:
|
||||
current_automation_directive = automation_directives.pop(0)
|
||||
if opts.headless and current_automation_directive[0] == 'expect':
|
||||
sys.stdout.write(f'Expecting {repr(current_automation_directive[2])}\r\n')
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
# Check if there's potential start of expected data in the incoming data
|
||||
combined = automation_check + data
|
||||
automation_check = ''
|
||||
for i in range(1, min(len(expected), len(combined)) + 1):
|
||||
if expected.startswith(combined[-i:]):
|
||||
automation_check = combined[-i:]
|
||||
return # wait for next check
|
||||
elif current_automation_directive[0] == 'send':
|
||||
data = ''
|
||||
automation_check = ''
|
||||
if opts.headless:
|
||||
sys.stdout.write(f'Sending {repr(current_automation_directive[2])}\r\n')
|
||||
sys.stdout.flush()
|
||||
if current_automation_directive[1]:
|
||||
tlvdata.send(session.connection, current_automation_directive[1])
|
||||
current_automation_directive = None
|
||||
if automation_directives:
|
||||
current_automation_directive = automation_directives.pop(0)
|
||||
if opts.headless and current_automation_directive[0] == 'expect':
|
||||
sys.stdout.write(f'Expecting {repr(current_automation_directive[2])}\r\n')
|
||||
sys.stdout.flush()
|
||||
elif current_automation_directive[0] == 'exit':
|
||||
if opts.headless:
|
||||
sys.stdout.write('Automation completed\r\n')
|
||||
sys.stdout.flush()
|
||||
return True
|
||||
if opts.headless and not current_automation_directive:
|
||||
sys.stdout.write('Automation completed\r\n')
|
||||
sys.stdout.flush()
|
||||
return True
|
||||
return False
|
||||
|
||||
def consume_termdata(fh, bufferonly=False):
|
||||
global clearpowermessage
|
||||
global fgcolor, bgcolor, fgshifted, pendseq
|
||||
@@ -987,6 +1133,11 @@ def consume_termdata(fh, bufferonly=False):
|
||||
updatestatus(data)
|
||||
return ''
|
||||
if data is not None:
|
||||
shouldexit = check_automation(data)
|
||||
if opts.headless:
|
||||
if shouldexit:
|
||||
quitconfetty(fullexit=True)
|
||||
return ''
|
||||
indata = pendseq + client.stringify(data)
|
||||
pendseq = ''
|
||||
data = ''
|
||||
@@ -1066,6 +1217,8 @@ def consume_termdata(fh, bufferonly=False):
|
||||
# this scenario comfortable that it
|
||||
# will come out soon enough
|
||||
pass
|
||||
if shouldexit:
|
||||
quitconfetty(fullexit=True)
|
||||
else:
|
||||
deadline = 5
|
||||
connected = False
|
||||
|
||||
@@ -36,6 +36,36 @@ import confluent.client as client
|
||||
import confluent.sortutil as sortutil
|
||||
devnull = None
|
||||
|
||||
def run_automation(noderange, category, c):
|
||||
automationbynode = {}
|
||||
for res in c.update('/noderange/{0}/deployment/remote_config/run'.format(noderange), {
|
||||
'category': category,
|
||||
}):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode |= res.get('errorcode', 1)
|
||||
if 'created' in res:
|
||||
nodename = res['created'].split('/')[2]
|
||||
automationbynode[nodename] = res['created']
|
||||
while automationbynode:
|
||||
for node in list(automationbynode):
|
||||
for res in c.read(automationbynode[node]):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode |= res.get('errorcode', 1)
|
||||
for result in res.get('results', []):
|
||||
sys.stdout.write('{0}: Task [{1}] {2}\n'.format(
|
||||
node, result['task_name'], result['state']))
|
||||
for warning in result.get('warnings', []):
|
||||
sys.stderr.write('{0}: [WARNING] {1}\n'.format(node, warning))
|
||||
if 'errorinfo' in result:
|
||||
for errorline in result['errorinfo'].splitlines():
|
||||
sys.stderr.write('{0}: [ERROR] {1}\n'.format(node, errorline))
|
||||
if res.get('complete', False):
|
||||
del automationbynode[node]
|
||||
sys.stdout.write('{0}: Automation complete\n'.format(node))
|
||||
|
||||
|
||||
def run():
|
||||
global devnull
|
||||
devnull = open(os.devnull, 'rb')
|
||||
@@ -51,6 +81,8 @@ def run():
|
||||
help='Run the syncfiles associated with the currently completed OS profile on the noderange')
|
||||
argparser.add_option('-P', '--scripts',
|
||||
help='Re-run specified scripts, with full path under scripts, e.g. post.d/first,firstboot.d/second')
|
||||
argparser.add_option('-A', '--automation',
|
||||
help='Run the automation scripts associated with the current OS profile on the noderange, specifying category (onboot.d/firstboot.d/post.d)')
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to run remote ssh command to, '
|
||||
@@ -74,16 +106,13 @@ def run():
|
||||
exitcode = 0
|
||||
|
||||
c.stop_if_noderange_over(args[0], options.maxnodes)
|
||||
if options.automation:
|
||||
run_automation(args[0], options.automation, c)
|
||||
|
||||
nodemap = {}
|
||||
cmdparms = []
|
||||
nodes = []
|
||||
for res in c.read('/noderange/{0}/nodes/'.format(args[0])):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode |= res.get('errorcode', 1)
|
||||
break
|
||||
node = res['item']['href'][:-1]
|
||||
nodes.append(node)
|
||||
|
||||
|
||||
cmdstorun = []
|
||||
if options.security:
|
||||
@@ -94,8 +123,17 @@ def run():
|
||||
for script in options.scripts.split(','):
|
||||
cmdstorun.append(['run_remote', script])
|
||||
if not cmdstorun:
|
||||
if options.automation:
|
||||
sys.exit(0)
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
for res in c.read('/noderange/{0}/nodes/'.format(args[0])):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode |= res.get('errorcode', 1)
|
||||
break
|
||||
node = res['item']['href'][:-1]
|
||||
nodes.append(node)
|
||||
idxbynode = {}
|
||||
cmdvbase = ['bash', '/etc/confluent/functions']
|
||||
for sshnode in nodes:
|
||||
@@ -145,7 +183,7 @@ def run():
|
||||
run_cmdv(node, cmdv, all, poller, pipedesc)
|
||||
elif pendingexecs:
|
||||
node, cmdv = pendingexecs.popleft()
|
||||
run_cmdv(node, cmdv, all, poller. pipedesc)
|
||||
run_cmdv(node, cmdv, all, poller, pipedesc)
|
||||
singlepoller.close()
|
||||
for node in sortutil.natural_sort(pernodeout):
|
||||
for line in pernodeout[node]:
|
||||
|
||||
@@ -76,6 +76,10 @@ if __name__ == '__main__':
|
||||
|
||||
list_parser = subparsers.add_parser('listbmccacerts', help='List BMC CA certificates')
|
||||
|
||||
sign_bmc_parser = subparsers.add_parser('signbmccert', help='Sign BMC certificate')
|
||||
sign_bmc_parser.add_argument('--days', type=int, help='Number of days the certificate is valid for')
|
||||
sign_bmc_parser.add_argument('--added-names', type=str, help='Additional names to include in the certificate')
|
||||
|
||||
args = parser.parse_args()
|
||||
c = client.Command()
|
||||
if args.command == 'installbmccacert':
|
||||
@@ -84,6 +88,17 @@ if __name__ == '__main__':
|
||||
removebmccacert(args.noderange, args.id, c)
|
||||
elif args.command == 'listbmccacerts':
|
||||
listbmccacerts(args.noderange, c)
|
||||
elif args.command == 'signbmccert':
|
||||
payload = {}
|
||||
if args.days is not None:
|
||||
payload['days'] = args.days
|
||||
else:
|
||||
print("Error: --days is required for signbmccert", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if args.added_names:
|
||||
payload['added_names'] = args.added_names
|
||||
for res in c.update(f'/noderange/{args.noderange}/configuration/management_controller/certificate/sign', payload):
|
||||
print(repr(res))
|
||||
else:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
@@ -62,7 +62,7 @@ argparser.add_option('-e', '--extra', dest='extra',
|
||||
'to be extra configuration')
|
||||
argparser.add_option('-x', '--exclude', dest='exclude',
|
||||
action='store_true', default=False,
|
||||
help='Treat positional arguments as items to not '
|
||||
help='Treat named settings as items to not '
|
||||
'examine, compare, or restore default')
|
||||
argparser.add_option('-a', '--advanced', dest='advanced',
|
||||
action='store_true', default=False,
|
||||
@@ -73,7 +73,7 @@ argparser.add_option('-r', '--restoredefault', default=False,
|
||||
dest='restoredefault', metavar="COMPONENT",
|
||||
help='Restore the configuration of the node '
|
||||
'to factory default for given component. '
|
||||
'Currently only uefi is supported')
|
||||
'Currently "uefi" and "bmc" are supported components')
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to configure, '
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import base64
|
||||
import optparse
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
@@ -62,8 +63,13 @@ argparser = optparse.OptionParser(
|
||||
"ctrl-'e', then release ctrl, then 'c', then '?' for a full list. "
|
||||
"For example, ctrl-'e', then 'c', then '.' will exit the current "
|
||||
"console")
|
||||
argparser.add_option('-a', '--automation', type='string', default=None,
|
||||
help='Specify an automation script')
|
||||
argparser.add_option('-t', '--tile', action='store_true', default=False,
|
||||
help='Tile console windows in the terminal')
|
||||
argparser.add_option('-e', '--headless', action='store_true', default=False,
|
||||
help='Run in headless mode, which is designed for use with '
|
||||
'automation scripts and disables interactive features')
|
||||
argparser.add_option('-l', '--log', action='store_true', default=False,
|
||||
help='Enter log replay mode instead of showing a live console')
|
||||
|
||||
@@ -98,6 +104,12 @@ argparser.add_option('-w','--windowed', action='store_true', default=False,
|
||||
|
||||
(options, args) = argparser.parse_args()
|
||||
|
||||
automation_args = []
|
||||
if options.automation:
|
||||
automation_args = ['-a', options.automation]
|
||||
if options.headless:
|
||||
automation_args += ['--headless']
|
||||
|
||||
oldtcattr = None
|
||||
oldfl = None
|
||||
|
||||
@@ -655,7 +667,7 @@ if options.windowed:
|
||||
firstnode=nodes[0]
|
||||
nodes.pop(0)
|
||||
with open(os.devnull, 'wb') as devnull:
|
||||
xopen=subprocess.Popen(envlist + [confettypath, '-c', '/tmp/controlpath-{0}'.format(firstnode), '-m', '5', 'start', '/nodes/{0}/console/session'.format(firstnode) ] , stdin=devnull)
|
||||
xopen=subprocess.Popen(envlist + [confettypath, '-c', '/tmp/controlpath-{0}'.format(firstnode)] + automation_args + ['-m', '5', 'start', '/nodes/{0}/console/session'.format(firstnode)] , stdin=devnull)
|
||||
time.sleep(2)
|
||||
s=socket.socket(socket.AF_UNIX)
|
||||
winid=''
|
||||
@@ -727,7 +739,7 @@ if options.windowed:
|
||||
else:
|
||||
pass
|
||||
with open(os.devnull, 'wb') as devnull:
|
||||
xopen=subprocess.Popen(envlist + [confettypath, '-c', '/tmp/controlpath-{0}'.format(node), '-m', '5', 'start', '/nodes/{0}/console/session'.format(node)] , stdin=devnull)
|
||||
xopen=subprocess.Popen(envlist + [confettypath, '-c', '/tmp/controlpath-{0}'.format(node)] + automation_args + ['-m', '5', 'start', '/nodes/{0}/console/session'.format(node)] , stdin=devnull)
|
||||
sys.exit(0)
|
||||
#end of wcons
|
||||
if options.tile:
|
||||
@@ -752,18 +764,18 @@ if options.tile:
|
||||
panename = '{0}:{1}'.format(sessname, pane)
|
||||
if initial:
|
||||
initial = False
|
||||
confetty_cmd = [confettypath] + automation_args + ['-m', '5', 'start', '/nodes/{0}/console/session'.format(node)]
|
||||
subprocess.call(
|
||||
['tmux', 'new-session', '-d', '-s',
|
||||
sessname, '-x', '800', '-y',
|
||||
'800', '{0} -m 5 start /nodes/{1}/console/session'.format(
|
||||
confettypath, node)])
|
||||
'800', ' '.join(shlex.quote(arg) for arg in confetty_cmd)])
|
||||
else:
|
||||
subprocess.call(['tmux', 'select-pane', '-t', sessname])
|
||||
subprocess.call(['tmux', 'set-option', '-t', panename, 'pane-border-status', 'top'], stderr=null)
|
||||
confetty_cmd = [confettypath] + automation_args + ['-m', '5', 'start', '/nodes/{0}/console/session'.format(node)]
|
||||
subprocess.call(
|
||||
['tmux', 'split', '-h', '-t', sessname,
|
||||
'{0} -m 5 start /nodes/{1}/console/session'.format(
|
||||
confettypath, node)])
|
||||
' '.join(shlex.quote(arg) for arg in confetty_cmd)])
|
||||
subprocess.call(['tmux', 'select-layout', '-t', sessname, 'tiled'], stdout=null)
|
||||
pane += 1
|
||||
subprocess.call(['tmux', 'select-pane', '-t', sessname])
|
||||
@@ -771,5 +783,6 @@ if options.tile:
|
||||
if not in_tmux:
|
||||
os.execlp('tmux', 'tmux', 'attach', '-t', sessname)
|
||||
else:
|
||||
os.execl(confettypath, confettypath, 'start',
|
||||
'/nodes/{0}/console/session'.format(args[0]))
|
||||
execl_args = [confettypath] + automation_args + ['start',
|
||||
'/nodes/{0}/console/session'.format(args[0])]
|
||||
os.execl(confettypath, *execl_args)
|
||||
|
||||
@@ -78,6 +78,7 @@ def main(args):
|
||||
ap = argparse.ArgumentParser(description='Deploy OS to nodes')
|
||||
ap.add_argument('-c', '--clear', help='Clear any pending deployment action', action='store_true')
|
||||
ap.add_argument('-n', '--network', help='Initiate deployment over PXE/HTTP', action='store_true')
|
||||
ap.add_argument('-b', '--bootmethod', help='Specify network boot method (e.g., network, http)', default='network')
|
||||
ap.add_argument('-p', '--prepareonly', help='Prepare only, skip any interaction with a BMC associated with this deployment action', action='store_true')
|
||||
ap.add_argument('-m', '--maxnodes', help='Specifiy a maximum nodes to be deployed')
|
||||
ap.add_argument('-r', '--redeploy', help='Redeploy nodes with the current or pending profile', action='store_true')
|
||||
@@ -213,7 +214,7 @@ def main(args):
|
||||
print('{0}: {1}{2}'.format(node, profile, armed))
|
||||
sys.exit(0)
|
||||
if not args.clear and args.network and not args.prepareonly:
|
||||
rc = c.simple_noderange_command(args.noderange, '/boot/nextdevice', 'network',
|
||||
rc = c.simple_noderange_command(args.noderange, '/boot/nextdevice', args.bootmethod,
|
||||
bootmode='uefi',
|
||||
persistent=False,
|
||||
errnodes=errnodes)
|
||||
|
||||
@@ -59,6 +59,8 @@ argparser = optparse.OptionParser(
|
||||
"%prog <noderange> [list][updatestatus][update [--backup <file>]]|[<components>]")
|
||||
argparser.add_option('-b', '--backup', action='store_true',
|
||||
help='Target a backup bank rather than primary')
|
||||
argparser.add_option('-p', '--parameterfile', type='string',
|
||||
help='When updating, use the specified parameter file')
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='When updating, prompt if more than the specified '
|
||||
'number of servers will be affected')
|
||||
@@ -112,6 +114,10 @@ def update_firmware(session, filename):
|
||||
upargs = {'filename': filename}
|
||||
if options.backup:
|
||||
upargs['bank'] = 'backup'
|
||||
if options.parameterfile:
|
||||
with open(options.parameterfile, 'rb') as pf:
|
||||
pfdata = pf.read()
|
||||
upargs['parameterdata'] = pfdata
|
||||
noderrs = {}
|
||||
if session.unixdomain:
|
||||
filesbynode = {}
|
||||
|
||||
@@ -84,13 +84,13 @@ def main():
|
||||
healthexplanations[node] = []
|
||||
for sensor in health[node]['sensors']:
|
||||
explanation = sensor['name'] + ':'
|
||||
if sensor['value'] is not None:
|
||||
if sensor.get('value', None) is not None:
|
||||
explanation += str(sensor['value'])
|
||||
if sensor['units'] is not None:
|
||||
if sensor.get('units', None) is not None:
|
||||
explanation += sensor['units']
|
||||
if sensor['states']:
|
||||
if sensor.get('states', None):
|
||||
explanation += ','
|
||||
if sensor['states']:
|
||||
if sensor.get('states', None):
|
||||
explanation += ','.join(sensor['states'])
|
||||
healthexplanations[node].append(explanation)
|
||||
if node in healthbynode and node in healthexplanations:
|
||||
|
||||
@@ -123,7 +123,7 @@ def sensorpass(showout=True, appendtime=False):
|
||||
if 'sensors' not in reading[node]:
|
||||
continue
|
||||
for sensedata in reading[node]['sensors']:
|
||||
if sensedata['value'] is None and options.skipnumberless:
|
||||
if sensedata.get('value', None) is None and options.skipnumberless:
|
||||
continue
|
||||
for redundant_state in ('Non-Critical', 'Critical'):
|
||||
try:
|
||||
@@ -134,17 +134,17 @@ def sensorpass(showout=True, appendtime=False):
|
||||
resultdata[node][sensedata['name']] = sensedata
|
||||
sensorname = sensedata['name']
|
||||
sensorheaders[sensorname] = sensorname
|
||||
if sensedata['units'] not in (None, u''):
|
||||
if sensedata.get('units', None) not in (None, u''):
|
||||
sensorheaders[sensorname] += u' ({0})'.format(
|
||||
sensedata['units'])
|
||||
if showout:
|
||||
if sensedata['value'] is None:
|
||||
if sensedata.get('value', None) is None:
|
||||
showval = ''
|
||||
elif isinstance(sensedata['value'], float):
|
||||
showval = u' {0} '.format(floatformat(sensedata['value']))
|
||||
else:
|
||||
showval = u' {0} '.format(sensedata['value'])
|
||||
if sensedata['units'] not in (None, u''):
|
||||
showval = u' {0} '.format(sensedata.get('value', ''))
|
||||
if sensedata.get('units', None) not in (None, u''):
|
||||
showval += sensedata['units']
|
||||
if sensedata.get('health', 'ok') != 'ok':
|
||||
datadescription = [sensedata['health']]
|
||||
|
||||
@@ -32,7 +32,7 @@ if path.startswith('/opt'):
|
||||
import confluent.client as client
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage='Usage: %prog [options] <noderange> [default|cd|network|setup|hd|usb|floppy]')
|
||||
usage='Usage: %prog [options] <noderange> [default|cd|network|http|setup|hd|usb|floppy]')
|
||||
argparser.add_option('-b', '--bios', dest='biosmode',
|
||||
action='store_true', default=False,
|
||||
help='Request BIOS style boot (rather than UEFI)')
|
||||
|
||||
@@ -281,6 +281,7 @@ class Command(object):
|
||||
if maxnodes is None:
|
||||
return
|
||||
nsize = self.get_noderange_size(noderange)
|
||||
maxnodes = int(maxnodes)
|
||||
if nsize > maxnodes:
|
||||
if nsize == 1:
|
||||
nodename = list(self.read(
|
||||
@@ -391,8 +392,13 @@ class Command(object):
|
||||
cacert = None
|
||||
certreqs = ssl.CERT_NONE
|
||||
knownhosts = True
|
||||
self.connection = ssl.wrap_socket(self.connection, ca_certs=cacert,
|
||||
cert_reqs=certreqs)
|
||||
tlsctx = ssl.create_default_context()
|
||||
if certreqs == ssl.CERT_NONE:
|
||||
tlsctx.check_hostname = False
|
||||
tlsctx.verify_mode = certreqs
|
||||
if cacert:
|
||||
tlsctx.load_verify_locations(cacert)
|
||||
self.connection = tlsctx.wrap_socket(self.connection, server_hostname=server)
|
||||
if knownhosts:
|
||||
certdata = self.connection.getpeercert(binary_form=True)
|
||||
fingerprint = 'sha512$' + hashlib.sha512(certdata).hexdigest()
|
||||
|
||||
@@ -24,6 +24,8 @@ For a full list of attributes, run `nodeattrib <node> all` against a node.
|
||||
If `-c` is specified, this will set the nodeattribute to a null value.
|
||||
This is different from setting the value to an empty string.
|
||||
|
||||
Arbitrary custom attributes can also be created with the `custom.` prefix.
|
||||
|
||||
Attributes may be specified by wildcard, for example `net.*switch` will report
|
||||
all attributes that begin with `net.` and end with `switch`.
|
||||
|
||||
|
||||
@@ -49,8 +49,7 @@ actually be in effect until a reboot.
|
||||
|
||||
* `-r COMPONENT`, `--restoredefault=COMPONENT`:
|
||||
Request that the specified component of the targeted nodes will have its
|
||||
configuration reset to default. Currently the only component implemented
|
||||
is uefi.
|
||||
configuration reset to default. The component may be "uefi" or "bmc".
|
||||
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to configure, prompting if over
|
||||
|
||||
@@ -35,7 +35,10 @@ the out of band facilities. Firmware updates can end in one of three states:
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
When updating, prompt if more than the specified number of servers will
|
||||
be affected
|
||||
|
||||
|
||||
* `-p PARAMETERFILE`, `--paramaterfile=PARAMETERFILE`:
|
||||
For updating, a parameter file to provife along with the update payload
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ nodegroupattrib(8) -- List or change confluent nodegroup attributes
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodegroupattrip` queries the confluent server to get information about nodes.
|
||||
`nodegroupattrib` queries the confluent server to get information about nodes.
|
||||
In the simplest form, it simply takes the given group and lists the attributes of that group.
|
||||
|
||||
Contrasted with nodeattrib(8), settings managed by nodegroupattrib will be added
|
||||
|
||||
@@ -32,6 +32,13 @@ BMCs map a virtual USB device to that url. Content is loaded on demand, and
|
||||
as such that URL is referenced potentially once for every IO operation that
|
||||
the host platform attempts.
|
||||
|
||||
## NOTES
|
||||
|
||||
When doing an attach of an https:// url, you may hit an error if you have not enrolled your certificate authority.
|
||||
In a general confluent environment, you can usually address it by:
|
||||
`# for cert in /var/lib/confluent/public/site/tls/*.pem; do nodecertutil s1-s4 installbmccacert $cert; done`
|
||||
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-h`, `--help`:
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/python3
|
||||
import glob
|
||||
import gzip
|
||||
import base64
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
def collect_certificates(tmpdir):
|
||||
certdata = ''
|
||||
for cacert in glob.glob(f'{tmpdir}/*.pem'):
|
||||
with open(cacert, 'r') as f:
|
||||
certdata += f.read()
|
||||
return certdata
|
||||
|
||||
def embed_certificates(incfg, certdata):
|
||||
if not certdata:
|
||||
raise Exception('No certificates found to embed')
|
||||
incfg = incfg.replace('%CONFLUENTCERTCOLL%', certdata)
|
||||
return incfg
|
||||
|
||||
def embed_identity(incfg, identityjson):
|
||||
incfg = incfg.replace('%IDENTJSON%', identityjson)
|
||||
return incfg
|
||||
|
||||
def embed_apiclient(incfg, apiclient):
|
||||
with open(apiclient, 'r') as f:
|
||||
apiclientdata = f.read()
|
||||
compressed = gzip.compress(apiclientdata.encode())
|
||||
encoded = base64.b64encode(compressed).decode()
|
||||
incfg = incfg.replace('%APICLIENTZ64%', encoded)
|
||||
return incfg
|
||||
|
||||
def embed_data(tmpdir, outfile):
|
||||
templatefile = f'{tmpdir}/bfb.cfg.template'
|
||||
with open(templatefile, 'r') as f:
|
||||
incfg = f.read()
|
||||
|
||||
certdata = collect_certificates(tmpdir)
|
||||
incfg = embed_certificates(incfg, certdata)
|
||||
|
||||
with open(f'{tmpdir}/identity.json', 'r') as f:
|
||||
identityjson = f.read()
|
||||
|
||||
incfg = embed_identity(incfg, identityjson)
|
||||
|
||||
incfg = embed_apiclient(incfg, f'{tmpdir}/../apiclient')
|
||||
|
||||
with open(outfile, 'w') as f:
|
||||
f.write(incfg)
|
||||
|
||||
def get_identity_json(node):
|
||||
identity_file = f'/var/lib/confluent/private/site/identity_files/{node}.json'
|
||||
try:
|
||||
with open(identity_file, 'r') as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 4:
|
||||
print("Usage: bfb-autoinstall <node> <bfbfile> <rshim>")
|
||||
sys.exit(1)
|
||||
|
||||
node = sys.argv[1]
|
||||
bfbfile = sys.argv[2]
|
||||
rshim = sys.argv[3]
|
||||
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
currdir = os.getcwd()
|
||||
tempdir = tempfile.mkdtemp(prefix=f'bfb-autoinstall-{node}-')
|
||||
embed_data(f'{currdir}/{node}', f'{tempdir}/bfb.cfg')
|
||||
subprocess.check_call(['bfb-install', '-b', bfbfile, '-c', f'{tempdir}/bfb.cfg', '-r', rshim])
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/python3
|
||||
import glob
|
||||
import gzip
|
||||
import base64
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
def collect_certificates(tmpdir):
|
||||
certdata = ''
|
||||
for cacert in glob.glob(f'{tmpdir}/*.pem'):
|
||||
with open(cacert, 'r') as f:
|
||||
certdata += f.read()
|
||||
return certdata
|
||||
|
||||
def embed_certificates(incfg, certdata):
|
||||
if not certdata:
|
||||
raise Exception('No certificates found to embed')
|
||||
incfg = incfg.replace('%CONFLUENTCERTCOLL%', certdata)
|
||||
return incfg
|
||||
|
||||
def embed_identity(incfg, identityjson):
|
||||
incfg = incfg.replace('%IDENTJSON%', identityjson)
|
||||
return incfg
|
||||
|
||||
def embed_apiclient(incfg, apiclient):
|
||||
with open(apiclient, 'r') as f:
|
||||
apiclientdata = f.read()
|
||||
compressed = gzip.compress(apiclientdata.encode())
|
||||
encoded = base64.b64encode(compressed).decode()
|
||||
incfg = incfg.replace('%APICLIENTZ64%', encoded)
|
||||
return incfg
|
||||
|
||||
def embed_data(tmpdir, outfile):
|
||||
templatefile = f'{tmpdir}/bfb.cfg.template'
|
||||
with open(templatefile, 'r') as f:
|
||||
incfg = f.read()
|
||||
|
||||
certdata = collect_certificates(tmpdir)
|
||||
incfg = embed_certificates(incfg, certdata)
|
||||
|
||||
with open(f'{tmpdir}/identity.json', 'r') as f:
|
||||
identityjson = f.read()
|
||||
|
||||
incfg = embed_identity(incfg, identityjson)
|
||||
|
||||
incfg = embed_apiclient(incfg, f'{tmpdir}/../apiclient')
|
||||
|
||||
with open(outfile, 'w') as f:
|
||||
f.write(incfg)
|
||||
|
||||
def get_identity_json(node):
|
||||
identity_file = f'/var/lib/confluent/private/site/identity_files/{node}.json'
|
||||
try:
|
||||
with open(identity_file, 'r') as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 4:
|
||||
print("Usage: bfb-autoinstall <node> <bfbfile> <rshim>")
|
||||
sys.exit(1)
|
||||
|
||||
node = sys.argv[1]
|
||||
bfbfile = sys.argv[2]
|
||||
rshim = sys.argv[3]
|
||||
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
currdir = os.getcwd()
|
||||
tempdir = tempfile.mkdtemp(prefix=f'bfb-autoinstall-{node}-')
|
||||
embed_data(f'{currdir}/{node}', f'{tempdir}/bfb.cfg')
|
||||
subprocess.check_call(['bfb-install', '-b', bfbfile, '-c', f'{tempdir}/bfb.cfg', '-r', rshim])
|
||||
@@ -0,0 +1,76 @@
|
||||
function bfb_modify_os() {
|
||||
echo 'ubuntu:!' | chpasswd -e
|
||||
mkdir -p /mnt/opt/confluent/bin/
|
||||
cat > /mnt/opt/confluent/bin/confluentbootstrap.sh << 'END_OF_EMBED'
|
||||
#!/bin/bash
|
||||
cat > /usr/local/share/ca-certificates/confluent.crt << 'END_OF_CERTS'
|
||||
%CONFLUENTCERTCOLL%
|
||||
END_OF_CERTS
|
||||
update-ca-certificates
|
||||
mkdir -p /opt/confluent/bin /etc/confluent/
|
||||
cp /usr/local/share/ca-certificates/confluent.crt /etc/confluent/ca.pem
|
||||
cat > /opt/confluent/bin/apiclient.gz.b64 << 'END_OF_CLIENT'
|
||||
%APICLIENTZ64%
|
||||
END_OF_CLIENT
|
||||
base64 -d /opt/confluent/bin/apiclient.gz.b64 | gunzip > /opt/confluent/bin/apiclient
|
||||
cat > /etc/confluent/ident.json << 'END_OF_IDENT'
|
||||
%IDENTJSON%
|
||||
END_OF_IDENT
|
||||
python3 /opt/confluent/bin/apiclient -i /etc/confluent/ident.json /confluent-api/self/deploycfg2 > /etc/confluent/confluent.deploycfg
|
||||
PROFILE=$(grep ^profile: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
ROOTPASS=$(grep ^rootpassword: /etc/confluent/confluent.deploycfg | awk '{print $2}'|grep -v null)
|
||||
if [ -n "$ROOTPASS" ]; then
|
||||
echo root:$ROOTPASS | chpasswd -e
|
||||
echo "ubuntu:$ROOTPASS" | chpasswd -e
|
||||
else
|
||||
echo 'ubuntu:!' | chpasswd -e
|
||||
fi
|
||||
cntmp=$(mktemp -d)
|
||||
cd "$cntmp" || { echo "Failed to cd to temporary directory $cntmp"; exit 1; }
|
||||
touch /etc/confluent/confluent.deploycfg
|
||||
python3 /opt/confluent/bin/apiclient /confluent-public/os/$PROFILE/scripts/confignet > confignet
|
||||
python3 confignet
|
||||
cd -
|
||||
rm -rf "$cntmp"
|
||||
python3 /opt/confluent/bin/apiclient /confluent-public/os/$PROFILE/scripts/functions > /etc/confluent/functions
|
||||
bash /etc/confluent/functions run_remote setupssh
|
||||
for cert in /etc/ssh/ssh*-cert.pub; do
|
||||
if [ -s $cert ]; then
|
||||
echo HostCertificate $cert >> /etc/ssh/sshd_config.d/90-confluent.conf
|
||||
fi
|
||||
done
|
||||
mkdir -p /var/log/confluent
|
||||
chmod 700 /var/log/confluent
|
||||
touch /var/log/confluent/confluent-firstboot.log
|
||||
touch /var/log/confluent/confluent-post.log
|
||||
chmod 600 /var/log/confluent/confluent-post.log
|
||||
chmod 600 /var/log/confluent/confluent-firstboot.log
|
||||
exec >> /var/log/confluent/confluent-post.log
|
||||
exec 2>> /var/log/confluent/confluent-post.log
|
||||
bash /etc/confluent/functions run_remote_python syncfileclient
|
||||
bash /etc/confluent/functions run_remote_parts post.d
|
||||
bash /etc/confluent/functions run_remote_config post.d
|
||||
exec >> /var/log/confluent/confluent-firstboot.log
|
||||
exec 2>> /var/log/confluent/confluent-firstboot.log
|
||||
bash /etc/confluent/functions run_remote_parts firstboot.d
|
||||
bash /etc/confluent/functions run_remote_config firstboot.d
|
||||
python3 /opt/confluent/bin/apiclient /confluent-api/self/updatestatus -d 'status: staged'
|
||||
python3 /opt/confluent/bin/apiclient /confluent-api/self/updatestatus -d 'status: complete'
|
||||
systemctl disable confluentbootstrap
|
||||
rm /etc/systemd/system/confluentbootstrap.service
|
||||
END_OF_EMBED
|
||||
chmod +x /mnt/opt/confluent/bin/confluentbootstrap.sh
|
||||
cat > /mnt/etc/systemd/system/confluentbootstrap.service << EOS
|
||||
[Unit]
|
||||
Description=First Boot Process
|
||||
Requires=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/opt/confluent/bin/confluentbootstrap.sh
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOS
|
||||
chroot /mnt systemctl enable confluentbootstrap
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import glob
|
||||
import shutil
|
||||
import shlex
|
||||
import subprocess
|
||||
import select
|
||||
|
||||
sys.path.append('/opt/lib/confluent/python')
|
||||
|
||||
import confluent.sortutil as sortutil
|
||||
import confluent.client as client
|
||||
|
||||
|
||||
def prep_outdir(node):
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
for certfile in glob.glob('/var/lib/confluent/public/site/tls/*.pem'):
|
||||
basename = os.path.basename(certfile)
|
||||
destfile = os.path.join(tmpdir, basename)
|
||||
shutil.copy2(certfile, destfile)
|
||||
subprocess.check_call(shlex.split(f'confetty set /nodes/{node}/deployment/ident_image=create'))
|
||||
shutil.copy2(f'/var/lib/confluent/private/identity_files/{node}.json', os.path.join(tmpdir, 'identity.json'))
|
||||
return tmpdir
|
||||
|
||||
def exec_bfb_install(host, nodetorshim, bfbfile, installprocs, pipedesc, all, poller):
|
||||
remotedir = subprocess.check_output(shlex.split(f'ssh {host} mktemp -d /tmp/bfb.XXXXXX')).decode().strip()
|
||||
bfbbasename = os.path.basename(bfbfile)
|
||||
subprocess.check_call(shlex.split(f'rsync -avz --info=progress2 {bfbfile} {host}:{remotedir}/{bfbbasename}'))
|
||||
subprocess.check_call(shlex.split(f'rsync -avc --info=progress2 /opt/lib/confluent/osdeploy/bluefield/hostscripts/ {host}:{remotedir}/'))
|
||||
for node in nodetorshim:
|
||||
rshim = nodetorshim[node]
|
||||
nodeoutdir = prep_outdir(node)
|
||||
nodeprofile = subprocess.check_output(shlex.split(f'nodeattrib {node} deployment.pendingprofile')).decode().strip().split(':', 2)[2].strip()
|
||||
shutil.copy2(f'/var/lib/confluent/public/os/{nodeprofile}/bfb.cfg.template', os.path.join(nodeoutdir, 'bfb.cfg.template'))
|
||||
subprocess.check_call(shlex.split(f'rsync -avz {nodeoutdir}/ {host}:{remotedir}/{node}/'))
|
||||
shutil.rmtree(nodeoutdir)
|
||||
run_cmdv(node, shlex.split(f'ssh {host} sh /etc/confluent/functions confluentpython {remotedir}/bfb-autoinstall {node} {remotedir}/{bfbbasename} {rshim}'), all, poller, pipedesc)
|
||||
|
||||
|
||||
def run_cmdv(node, cmdv, all, poller, pipedesc):
|
||||
try:
|
||||
nopen = subprocess.Popen(
|
||||
cmdv, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
except OSError as e:
|
||||
if e.errno == 2:
|
||||
sys.stderr.write('{0}: Unable to find local executable file "{1}"\n'.format(node, cmdv[0]))
|
||||
return
|
||||
raise
|
||||
pipedesc[nopen.stdout.fileno()] = {'node': node, 'popen': nopen,
|
||||
'type': 'stdout', 'file': nopen.stdout}
|
||||
pipedesc[nopen.stderr.fileno()] = {'node': node, 'popen': nopen,
|
||||
'type': 'stderr', 'file': nopen.stderr}
|
||||
all.add(nopen.stdout)
|
||||
poller.register(nopen.stdout, select.EPOLLIN)
|
||||
all.add(nopen.stderr)
|
||||
poller.register(nopen.stderr, select.EPOLLIN)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print(f'Usage: {sys.argv[0]} <host> <bfbfile> <node1:rshim1> [<node2:rshim2> ...]')
|
||||
sys.exit(1)
|
||||
|
||||
host = sys.argv[1]
|
||||
bfbfile = sys.argv[2]
|
||||
nodetorshim = {}
|
||||
for arg in sys.argv[3:]:
|
||||
node, rshim = arg.split(':')
|
||||
nodetorshim[node] = rshim
|
||||
|
||||
installprocs = {}
|
||||
pipedesc = {}
|
||||
all = set()
|
||||
poller = select.epoll()
|
||||
|
||||
exec_bfb_install(host, nodetorshim, bfbfile, installprocs, pipedesc, all, poller)
|
||||
rdy = poller.poll(10)
|
||||
pendingexecs = []
|
||||
exitcode = 0
|
||||
while all:
|
||||
pernodeout = {}
|
||||
for r in rdy:
|
||||
r = r[0]
|
||||
desc = pipedesc[r]
|
||||
r = desc['file']
|
||||
node = desc['node']
|
||||
data = True
|
||||
singlepoller = select.epoll()
|
||||
singlepoller.register(r, select.EPOLLIN)
|
||||
while data and singlepoller.poll(0):
|
||||
data = r.readline()
|
||||
if data:
|
||||
if desc['type'] == 'stdout':
|
||||
if node not in pernodeout:
|
||||
pernodeout[node] = []
|
||||
pernodeout[node].append(data)
|
||||
else:
|
||||
data = client.stringify(data)
|
||||
sys.stderr.write('{0}: {1}'.format(node, data))
|
||||
sys.stderr.flush()
|
||||
else:
|
||||
pop = desc['popen']
|
||||
ret = pop.poll()
|
||||
if ret is not None:
|
||||
exitcode = exitcode | ret
|
||||
all.discard(r)
|
||||
poller.unregister(r)
|
||||
r.close()
|
||||
if desc['type'] == 'stdout' and pendingexecs:
|
||||
node, cmdv = pendingexecs.popleft()
|
||||
run_cmdv(node, cmdv, all, poller, pipedesc)
|
||||
singlepoller.close()
|
||||
for node in sortutil.natural_sort(pernodeout):
|
||||
for line in pernodeout[node]:
|
||||
line = client.stringify(line)
|
||||
sys.stdout.write('{0}: {1}'.format(node, line))
|
||||
sys.stdout.flush()
|
||||
if all:
|
||||
rdy = poller.poll(10)
|
||||
|
||||
|
||||
@@ -47,38 +47,113 @@ c_crypt.restype = ctypes.c_char_p
|
||||
|
||||
|
||||
def get_my_addresses():
|
||||
nlhdrsz = struct.calcsize('IHHII')
|
||||
ifaddrsz = struct.calcsize('BBBBI')
|
||||
# RTM_GETADDR = 22
|
||||
# nlmsghdr struct: u32 len, u16 type, u16 flags, u32 seq, u32 pid
|
||||
nlhdr = struct.pack('IHHII', nlhdrsz + ifaddrsz, 22, 0x301, 0, 0)
|
||||
# ifaddrmsg struct: u8 family, u8 prefixlen, u8 flags, u8 scope, u32 index
|
||||
ifaddrmsg = struct.pack('BBBBI', 0, 0, 0, 0, 0)
|
||||
s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE)
|
||||
s.bind((0, 0))
|
||||
s.sendall(nlhdr + ifaddrmsg)
|
||||
addrs = []
|
||||
while True:
|
||||
pdata = s.recv(65536)
|
||||
v = memoryview(pdata)
|
||||
if struct.unpack('H', v[4:6])[0] == 3: # netlink done message
|
||||
break
|
||||
while len(v):
|
||||
length, typ = struct.unpack('IH', v[:6])
|
||||
if typ == 20:
|
||||
fam, plen, _, scope, ridx = struct.unpack('BBBBI', v[nlhdrsz:nlhdrsz+ifaddrsz])
|
||||
if scope in (253, 0):
|
||||
rta = v[nlhdrsz+ifaddrsz:length]
|
||||
while len(rta):
|
||||
rtalen, rtatyp = struct.unpack('HH', rta[:4])
|
||||
if rtalen < 4:
|
||||
break
|
||||
if rtatyp == 1:
|
||||
addrs.append((fam, rta[4:rtalen], plen, ridx))
|
||||
rta = rta[msg_align(rtalen):]
|
||||
v = v[msg_align(length):]
|
||||
for ifa in get_ifaddrs():
|
||||
if ifa[0] == 'ip':
|
||||
addrs.append((ifa[1], ifa[2], ifa[3]))
|
||||
return addrs
|
||||
|
||||
def get_mac_addresses():
|
||||
macs = []
|
||||
for ifa in get_ifaddrs():
|
||||
if ifa[0] == 'ETHER':
|
||||
macs.append((ifa[1], ifa[2]))
|
||||
return macs
|
||||
|
||||
def get_ifaddrs():
|
||||
class sockaddr(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('sa_family', ctypes.c_uint16),
|
||||
('sa_data', ctypes.c_ubyte * 14),
|
||||
]
|
||||
|
||||
class sockaddr_in(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('sin_family', ctypes.c_uint16),
|
||||
('sin_port', ctypes.c_uint16),
|
||||
('sin_addr', ctypes.c_ubyte * 4),
|
||||
('sin_zero', ctypes.c_ubyte * 8),
|
||||
]
|
||||
|
||||
class sockaddr_in6(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('sin6_family', ctypes.c_uint16),
|
||||
('sin6_port', ctypes.c_uint16),
|
||||
('sin6_flowinfo', ctypes.c_uint32),
|
||||
('sin6_addr', ctypes.c_ubyte * 16),
|
||||
('sin6_scope_id', ctypes.c_uint32),
|
||||
]
|
||||
|
||||
class sockaddr_ll(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('sll_family', ctypes.c_uint16),
|
||||
('sll_protocol', ctypes.c_uint16),
|
||||
('sll_ifindex', ctypes.c_int32),
|
||||
('sll_hatype', ctypes.c_uint16),
|
||||
('sll_pkttype', ctypes.c_uint8),
|
||||
('sll_halen', ctypes.c_uint8),
|
||||
('sll_addr', ctypes.c_ubyte * 8),
|
||||
]
|
||||
|
||||
class ifaddrs(ctypes.Structure):
|
||||
pass
|
||||
|
||||
ifaddrs._fields_ = [
|
||||
('ifa_next', ctypes.POINTER(ifaddrs)),
|
||||
('ifa_name', ctypes.c_char_p),
|
||||
('ifa_flags', ctypes.c_uint),
|
||||
('ifa_addr', ctypes.POINTER(sockaddr)),
|
||||
('ifa_netmask', ctypes.POINTER(sockaddr)),
|
||||
('ifa_ifu', ctypes.POINTER(sockaddr)),
|
||||
('ifa_data', ctypes.c_void_p),
|
||||
]
|
||||
|
||||
libc = ctypes.CDLL(ctypes.util.find_library('c'))
|
||||
libc.getifaddrs.argtypes = [ctypes.POINTER(ctypes.POINTER(ifaddrs))]
|
||||
libc.getifaddrs.restype = ctypes.c_int
|
||||
libc.freeifaddrs.argtypes = [ctypes.POINTER(ifaddrs)]
|
||||
libc.freeifaddrs.restype = None
|
||||
ifap = ctypes.POINTER(ifaddrs)()
|
||||
result = libc.getifaddrs(ctypes.pointer(ifap))
|
||||
if result != 0:
|
||||
return []
|
||||
addresses = []
|
||||
ifa = ifap
|
||||
try:
|
||||
while ifa:
|
||||
if ifa.contents.ifa_addr:
|
||||
family = ifa.contents.ifa_addr.contents.sa_family
|
||||
name = ifa.contents.ifa_name.decode('utf-8') if ifa.contents.ifa_name else None
|
||||
if family in (socket.AF_INET, socket.AF_INET6):
|
||||
# skip loopback and non-multicast interfaces
|
||||
if ifa.contents.ifa_flags & 8 or not ifa.contents.ifa_flags & 0x1000:
|
||||
ifa = ifa.contents.ifa_next
|
||||
continue
|
||||
if family == socket.AF_INET:
|
||||
addr_ptr = ctypes.cast(ifa.contents.ifa_addr, ctypes.POINTER(sockaddr_in))
|
||||
addr_bytes = bytes(addr_ptr.contents.sin_addr)
|
||||
if_index = socket.if_nametoindex(name) if name else 0
|
||||
addresses.append(('ip', family, addr_bytes, if_index))
|
||||
elif family == socket.AF_INET6:
|
||||
addr_ptr = ctypes.cast(ifa.contents.ifa_addr, ctypes.POINTER(sockaddr_in6))
|
||||
addr_bytes = bytes(addr_ptr.contents.sin6_addr)
|
||||
scope_id = addr_ptr.contents.sin6_scope_id
|
||||
addresses.append(('ip', family, addr_bytes, scope_id))
|
||||
elif family == socket.AF_PACKET:
|
||||
addr_ptr = ctypes.cast(ifa.contents.ifa_addr, ctypes.POINTER(sockaddr_ll))
|
||||
halen = addr_ptr.contents.sll_halen
|
||||
if addr_ptr.contents.sll_hatype in (1, 32) and halen > 0: # ARPHRD_ETHER or ARPHRD_INFINIBAND
|
||||
if addr_ptr.contents.sll_hatype == 1 and addr_ptr.contents.sll_addr[0] & 2: # skip locally administered MACs
|
||||
ifa = ifa.contents.ifa_next
|
||||
continue
|
||||
mac_bytes = bytes(addr_ptr.contents.sll_addr[:halen])
|
||||
macaddr = ':'.join('{:02x}'.format(b) for b in mac_bytes)
|
||||
addresses.append(('ETHER', name, macaddr))
|
||||
ifa = ifa.contents.ifa_next
|
||||
finally:
|
||||
libc.freeifaddrs(ifap)
|
||||
|
||||
return addresses
|
||||
|
||||
def scan_confluents(confuuid=None):
|
||||
srvs = {}
|
||||
@@ -92,22 +167,24 @@ def scan_confluents(confuuid=None):
|
||||
s4.bind(('0.0.0.0', 1900))
|
||||
doneidxs = set([])
|
||||
msg = 'M-SEARCH * HTTP/1.1\r\nST: urn:xcat.org:service:confluent:'
|
||||
if not confuuid:
|
||||
if not confuuid and os.path.exists('/etc/confluent/confluent.deploycfg'):
|
||||
with open('/etc/confluent/confluent.deploycfg') as dcfg:
|
||||
for line in dcfg.read().split('\n'):
|
||||
if line.startswith('confluent_uuid:'):
|
||||
confluentuuid = line.split(': ')[1]
|
||||
msg += '/confluentuuid=' + confluentuuid
|
||||
break
|
||||
if not confuuid and os.path.exists('/confluent_uuid'):
|
||||
with open('/confluent_uuid') as cuuidin:
|
||||
confluentuuid = cuuidin.read().strip()
|
||||
msg += '/confluentuuid=' + confluentuuid
|
||||
try:
|
||||
with open('/sys/devices/virtual/dmi/id/product_uuid') as uuidin:
|
||||
msg += '/uuid=' + uuidin.read().strip()
|
||||
except Exception:
|
||||
pass
|
||||
for addrf in glob.glob('/sys/class/net/*/address'):
|
||||
with open(addrf) as addrin:
|
||||
hwaddr = addrin.read().strip()
|
||||
msg += '/mac=' + hwaddr
|
||||
for iface, hwaddr in get_mac_addresses():
|
||||
msg += '/mac=' + hwaddr
|
||||
msg = msg.encode('utf8')
|
||||
for addr in get_my_addresses():
|
||||
if addr[0] == socket.AF_INET6:
|
||||
@@ -155,7 +232,8 @@ def scan_confluents(confuuid=None):
|
||||
if currip.startswith('fe80::') and '%' not in currip:
|
||||
currip = '{0}%{1}'.format(currip, peer[-1])
|
||||
srvs[currip] = current
|
||||
srvlist.append(currip)
|
||||
if currip not in srvlist:
|
||||
srvlist.append(currip)
|
||||
r = select.select((s4, s6), (), (), 2)
|
||||
if r:
|
||||
r = r[0]
|
||||
@@ -625,4 +703,7 @@ if __name__ == '__main__':
|
||||
elif checkonly:
|
||||
sys.stdout.write(mclient.check_connections())
|
||||
else:
|
||||
sys.stdout.write(mclient.grab_url(sys.argv[1], data).decode())
|
||||
try:
|
||||
sys.stdout.buffer.write(mclient.grab_url(sys.argv[1], data))
|
||||
except AttributeError:
|
||||
sys.stdout.write(mclient.grab_url(sys.argv[1], data))
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
# serial port is not reporting DCD, then it doesn't look like a comfortable enough scenario
|
||||
|
||||
import fcntl
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
import struct
|
||||
import subprocess
|
||||
import termios
|
||||
import platform
|
||||
|
||||
|
||||
addrtoname = {
|
||||
@@ -74,9 +76,8 @@ def fixup_ubuntu_grub_serial():
|
||||
grubout.write(grubline + '\n')
|
||||
subprocess.check_call(['update-grub'])
|
||||
|
||||
def get_serial_config():
|
||||
if not os.path.exists('/sys/firmware/efi'):
|
||||
return None
|
||||
|
||||
def get_spcr_config():
|
||||
if not os.path.exists('/sys/firmware/acpi/tables/SPCR'):
|
||||
return None
|
||||
spcr = open("/sys/firmware/acpi/tables/SPCR", "rb")
|
||||
@@ -99,14 +100,39 @@ def get_serial_config():
|
||||
currattr = termios.tcgetattr(ttyf)
|
||||
currattr[4:6] = [0, termiobaud[retval['speed']]]
|
||||
termios.tcsetattr(ttyf, termios.TCSANOW, currattr)
|
||||
retval['connected'] = bool(struct.unpack('<I', fcntl.ioctl(
|
||||
ttyf, termios.TIOCMGET, '\x00\x00\x00\x00'))[0] & termios.TIOCM_CAR)
|
||||
os.close(ttyf)
|
||||
return retval
|
||||
|
||||
def get_serial_config():
|
||||
if platform.machine() != 'x86_64':
|
||||
return None # Trust non-x86 to do the right thing
|
||||
if not os.path.exists('/sys/firmware/efi'):
|
||||
return None # BIOS might fail at grub output, defer to stock OS behavior
|
||||
retval = get_spcr_config()
|
||||
if retval:
|
||||
return retval
|
||||
firstfound = None
|
||||
numpossible = 0
|
||||
numconnected = 0
|
||||
for serdev in glob.glob('/dev/ttyS*'):
|
||||
ttyf = os.open(serdev, os.O_RDWR | os.O_NOCTTY)
|
||||
try:
|
||||
statusreg = fcntl.ioctl(ttyf, termios.TIOCMGET, '\x00\x00\x00\x00')
|
||||
numpossible += 1
|
||||
if not firstfound:
|
||||
firstfound = serdev
|
||||
except Exception:
|
||||
continue
|
||||
finally:
|
||||
os.close(ttyf)
|
||||
if struct.unpack('<I', statusreg)[0] & termios.TIOCM_CAR:
|
||||
numconnected += 1
|
||||
firstfound = serdev
|
||||
if numpossible == 1 or numconnected == 1:
|
||||
return { 'tty': firstfound, 'speed': 115200 }
|
||||
|
||||
def main():
|
||||
autoconscfg = get_serial_config()
|
||||
if not autoconscfg or not autoconscfg['connected']:
|
||||
if not autoconscfg:
|
||||
return
|
||||
if os.path.exists('/etc/redhat-release'): # redhat family
|
||||
deserialize_grub_rh()
|
||||
|
||||
@@ -80,14 +80,18 @@ def await_tentative():
|
||||
time.sleep(1)
|
||||
|
||||
def map_idx_to_name():
|
||||
map = {}
|
||||
map_dict = {}
|
||||
devtype = {}
|
||||
prevdev = None
|
||||
|
||||
for line in subprocess.check_output(['ip', 'l']).decode('utf8').splitlines():
|
||||
if line.startswith(' ') and 'link/' in line:
|
||||
typ = line.split()[0].split('/')[1]
|
||||
devtype[prevdev] = typ if typ != 'ether' else 'ethernet'
|
||||
if line.startswith(' '):
|
||||
if 'link/' in line and prevdev and prevdev not in devtype:
|
||||
for word in line.split():
|
||||
if word.startswith('link/'):
|
||||
typ = word.split('/')[1]
|
||||
devtype[prevdev] = typ if typ != 'ether' else 'ethernet'
|
||||
break # Stop detection after the first type hit
|
||||
continue
|
||||
idx, iface, rst = line.split(':', 2)
|
||||
prevdev = iface.strip()
|
||||
@@ -99,9 +103,8 @@ def map_idx_to_name():
|
||||
pass
|
||||
idx = int(idx)
|
||||
iface = iface.strip()
|
||||
map[idx] = iface
|
||||
return map, devtype
|
||||
|
||||
map_dict[idx] = iface
|
||||
return map_dict, devtype
|
||||
|
||||
def get_interface_name(iname, settings):
|
||||
explicitname = settings.get('interface_names', None)
|
||||
@@ -114,8 +117,10 @@ def get_interface_name(iname, settings):
|
||||
class NetplanManager(object):
|
||||
def __init__(self, deploycfg):
|
||||
self.cfgbydev = {}
|
||||
self.cfgbybond = {}
|
||||
self.read_connections()
|
||||
self.deploycfg = deploycfg
|
||||
self.teamidx = 0
|
||||
|
||||
def read_connections(self):
|
||||
for plan in glob.glob('/etc/netplan/*.y*ml'):
|
||||
@@ -124,29 +129,61 @@ class NetplanManager(object):
|
||||
if not planinfo:
|
||||
continue
|
||||
nicinfo = planinfo.get('network', {}).get('ethernets', {})
|
||||
for devname in nicinfo:
|
||||
if devname == 'lo':
|
||||
continue
|
||||
if 'gateway4' in nicinfo[devname]:
|
||||
# normalize deprecated syntax on read in
|
||||
gw4 = nicinfo[devname]['gateway4']
|
||||
del nicinfo[devname]['gateway4']
|
||||
routeinfo = nicinfo[devname].get('routes', [])
|
||||
for ri in routeinfo:
|
||||
if ri.get('via', None) == gw4 and ri.get('to', None) in ('default', '0.0.0.0/0', '0/0'):
|
||||
break
|
||||
else:
|
||||
routeinfo.append({
|
||||
'to': 'default',
|
||||
'via': gw4
|
||||
})
|
||||
nicinfo[devname]['routes'] = routeinfo
|
||||
self.cfgbydev[devname] = nicinfo[devname]
|
||||
bondinfo = planinfo.get('network', {}).get('bonds', {})
|
||||
for currinfo in (nicinfo, bondinfo):
|
||||
currcfg = self.cfgbydev if currinfo is nicinfo else self.cfgbybond
|
||||
for devname in currinfo:
|
||||
if devname == 'lo':
|
||||
continue
|
||||
if 'gateway4' in currinfo[devname]:
|
||||
# normalize deprecated syntax on read in
|
||||
gw4 = currinfo[devname]['gateway4']
|
||||
del currinfo[devname]['gateway4']
|
||||
routeinfo = currinfo[devname].get('routes', [])
|
||||
for ri in routeinfo:
|
||||
if ri.get('via', None) == gw4 and ri.get('to', None) in ('default', '0.0.0.0/0', '0/0'):
|
||||
break
|
||||
else:
|
||||
routeinfo.append({
|
||||
'to': 'default',
|
||||
'via': gw4
|
||||
})
|
||||
currinfo[devname]['routes'] = routeinfo
|
||||
currcfg[devname] = currinfo[devname]
|
||||
|
||||
def apply_configuration(self, cfg):
|
||||
devnames = cfg['interfaces']
|
||||
if len(devnames) != 1:
|
||||
raise Exception('Multi-nic team/bonds not yet supported')
|
||||
if len(devnames) > 1:
|
||||
teammode = cfg['settings'].get('team_mode', None)
|
||||
if not teammode:
|
||||
sys.stderr.write("Warning, multiple interfaces ({0}) without a team_mode, skipping setup\n".format(','.join(devnames)))
|
||||
return
|
||||
if teammode == 'lacp':
|
||||
teammode = '802.3ad'
|
||||
elif teammode == 'activebackup':
|
||||
teammode = 'active-backup'
|
||||
for currdev in self.cfgbybond:
|
||||
for iface in self.cfgbybond[currdev].get('interfaces', []):
|
||||
if iface in devnames:
|
||||
break
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
# this bond is identified as matching
|
||||
self.cfgbybond[currdev]['interfaces'] = list(devnames)
|
||||
self.cfgbybond[currdev]['parameters']['mode'] = teammode
|
||||
devnames = [currdev]
|
||||
break
|
||||
# no current bond, make a new one
|
||||
connname = cfg['settings'].get('connection_name', None)
|
||||
if not connname:
|
||||
connname = 'bond{0}'.format(self.teamidx)
|
||||
while connname in self.cfgbybond:
|
||||
self.teamidx += 1
|
||||
connname = 'bond{0}'.format(self.teamidx)
|
||||
self.cfgbybond[connname] = {'interfaces': list(devnames), 'parameters': {'mode': teammode, 'mii-monitor-interval': 100}}
|
||||
devnames = [connname]
|
||||
stgs = cfg['settings']
|
||||
needcfgapply = False
|
||||
for devname in devnames:
|
||||
@@ -171,7 +208,7 @@ class NetplanManager(object):
|
||||
gws.append(stgs.get('ipv4_gateway', None))
|
||||
gws.append(stgs.get('ipv6_gateway', None))
|
||||
for gwaddr in gws:
|
||||
if gwaddr:
|
||||
if gwaddr and gwaddr != '0.0.0.0':
|
||||
cfgroutes = self.getcfgarrpath([devname, 'routes'])
|
||||
for rinfo in cfgroutes:
|
||||
if rinfo.get('via', None) == gwaddr:
|
||||
@@ -192,18 +229,56 @@ class NetplanManager(object):
|
||||
if dnsdomain not in currdnsdomain:
|
||||
needcfgwrite = True
|
||||
currdnsdomain.append(dnsdomain)
|
||||
prune_from_cloudinit = []
|
||||
if needcfgwrite:
|
||||
needcfgapply = True
|
||||
newcfg = {'network': {'version': 2, 'ethernets': {devname: self.cfgbydev[devname]}}}
|
||||
oumask = os.umask(0o77)
|
||||
with open('/etc/netplan/{0}-confluentcfg.yaml'.format(devname), 'w') as planout:
|
||||
if devname in self.cfgbydev:
|
||||
prune_from_cloudinit.append(devname)
|
||||
newcfg = {'network': {'version': 2, 'ethernets': {devname: self.cfgbydev[devname]}}}
|
||||
cfgfile = '/etc/netplan/10-{0}-confluentcfg.yaml'.format(devname)
|
||||
elif devname in self.cfgbybond:
|
||||
newcfg = {'network': {'version': 2, 'bonds': {devname: self.cfgbybond[devname]}}}
|
||||
for iface in newcfg['network']['bonds'][devname]['interfaces']:
|
||||
prune_from_cloudinit.append(iface)
|
||||
with open('/etc/netplan/10-{0}-confluentcfg.yaml'.format(iface), 'w') as planout:
|
||||
planout.write(yaml.dump({'network': {'version': 2, 'ethernets': {iface: {'dhcp4': False}}}}))
|
||||
cfgfile = '/etc/netplan/30-{0}-confluentcfg.yaml'.format(devname)
|
||||
with open(cfgfile, 'w') as planout:
|
||||
planout.write(yaml.dump(newcfg))
|
||||
os.umask(oumask)
|
||||
if prune_from_cloudinit:
|
||||
prunecfgs = ['/etc/netplan/00-installer-config.yaml',
|
||||
'/etc/netplan/50-cloud-init.yaml.dist-subiquity',
|
||||
'/etc/netplan/50-cloud-init.yaml']
|
||||
prunecfgs.extend(glob.glob('/etc/cloud/cloud.cfg.d/*.cfg'))
|
||||
for defcfg in prunecfgs:
|
||||
if not os.path.exists(defcfg):
|
||||
continue
|
||||
with open(defcfg, 'r') as cloudinit:
|
||||
cloudinfo = yaml.safe_load(cloudinit)
|
||||
if 'network' not in cloudinfo:
|
||||
continue
|
||||
for clouddev in list(cloudinfo.get('network', {}).get('ethernets', {})):
|
||||
if clouddev in prune_from_cloudinit:
|
||||
del cloudinfo['network']['ethernets'][clouddev]
|
||||
if not cloudinfo['network'].get('ethernets', {}):
|
||||
os.remove(defcfg)
|
||||
if '/etc/cloud/cloud.cfg.d/' in defcfg:
|
||||
# need to also change datasource, probably
|
||||
if os.path.exists('/var/lib/cloud/instances/iid-datasource-none/network-config.json'):
|
||||
os.remove('/var/lib/cloud/instances/iid-datasource-none/network-config.json')
|
||||
else:
|
||||
oumask = os.umask(0o77)
|
||||
with open(defcfg, 'w') as cloudinit:
|
||||
cloudinit.write(yaml.dump(cloudinfo))
|
||||
os.umask(oumask)
|
||||
if needcfgapply:
|
||||
subprocess.check_call(['netplan', 'generate'])
|
||||
subprocess.call(['netplan', 'apply'])
|
||||
|
||||
def getcfgarrpath(self, devpath):
|
||||
currptr = self.cfgbydev
|
||||
currptr = self.cfgbybond if devpath[0] in self.cfgbybond else self.cfgbydev
|
||||
for k in devpath[:-1]:
|
||||
if k not in currptr:
|
||||
currptr[k] = {}
|
||||
@@ -356,7 +431,7 @@ class NetworkManager(object):
|
||||
currteam = deats.get('connection.master', None)
|
||||
if currteam == team:
|
||||
return
|
||||
for stg in ('ipv4.dhcp-hostname', 'ipv4.dns', 'ipv6.dns', 'ipv6.dhcp-hostname'):
|
||||
for stg in ('ipv4.dhcp-hostname', 'ipv4.dns', 'ipv6.dns', 'ipv6.dhcp-hostname', 'ipv4.dns-search', 'ipv6.dns-search'):
|
||||
if deats.get(stg, None):
|
||||
bondcfg[stg] = deats[stg]
|
||||
if member in self.uuidbyname:
|
||||
@@ -488,15 +563,16 @@ if __name__ == '__main__':
|
||||
continue
|
||||
myname = s.getsockname()
|
||||
s.close()
|
||||
curridx = None
|
||||
if len(myname) == 4:
|
||||
curridx = myname[-1]
|
||||
else:
|
||||
myname = myname[0]
|
||||
myname = socket.inet_pton(socket.AF_INET, myname)
|
||||
for addr in myaddrs:
|
||||
if myname == addr[1].tobytes():
|
||||
if myname == addr[1]:
|
||||
curridx = addr[-1]
|
||||
if curridx in doneidxs:
|
||||
if curridx is not None and curridx in doneidxs:
|
||||
continue
|
||||
for tries in (1, 2, 3):
|
||||
try:
|
||||
|
||||
@@ -55,4 +55,9 @@ confluentpython $confapiclient /confluent-api/self/nodelist | sed -e 's/^- //' >
|
||||
cat /etc/ssh/shosts.equiv > /root/.shosts
|
||||
cd -
|
||||
rm -rf $TMPDIR
|
||||
systemctl try-restart sshd
|
||||
# ssh may be sshd or ssh, depending
|
||||
if systemctl list-unit-files | grep -q '^sshd\.service'; then
|
||||
systemctl try-restart sshd
|
||||
elif systemctl list-unit-files | grep -q '^ssh\.service'; then
|
||||
systemctl try-restart ssh
|
||||
fi
|
||||
|
||||
@@ -26,13 +26,12 @@ mkdir -p opt/confluent/bin
|
||||
mkdir -p stateless-bin
|
||||
cp -a el8bin/* .
|
||||
ln -s el8 el9
|
||||
ln -s el8 el10
|
||||
mv el10/initramfs/usr el10/initramfs/var
|
||||
cp -a el8 el10
|
||||
cp -a debian debian13
|
||||
mkdir -p debian13/initramfs/usr
|
||||
mv debian13/initramfs/lib debian13/initramfs/usr/
|
||||
mv el10/initramfs/usr el10/initramfs/var
|
||||
for os in rhvh4 el7 genesis el8 suse15 debian debian13 ubuntu20.04 ubuntu22.04 ubuntu24.04 coreos el9 el10; do
|
||||
for os in rhvh4 el7 genesis el8 suse15 debian debian13 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04 coreos el9 el10; do
|
||||
mkdir ${os}out
|
||||
cd ${os}out
|
||||
if [ -d ../${os}bin ]; then
|
||||
@@ -48,7 +47,7 @@ for os in rhvh4 el7 genesis el8 suse15 debian debian13 ubuntu20.04 ubuntu22.04 u
|
||||
mv ../addons.cpio .
|
||||
cd ..
|
||||
done
|
||||
for os in el7 el8 suse15 el9 el10 ubuntu20.04; do
|
||||
for os in el7 el8 suse15 el9 el10 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04; do
|
||||
mkdir ${os}disklessout
|
||||
cd ${os}disklessout
|
||||
if [ -d ../${os}bin ]; then
|
||||
@@ -86,7 +85,10 @@ cp -a esxi7 esxi8
|
||||
%install
|
||||
mkdir -p %{buildroot}/opt/confluent/share/licenses/confluent_osdeploy/
|
||||
#cp LICENSE %{buildroot}/opt/confluent/share/licenses/confluent_osdeploy/
|
||||
for os in rhvh4 el7 el8 el9 el10 debian debian13 genesis suse15 ubuntu20.04 ubuntu22.04 ubuntu24.04 esxi6 esxi7 esxi8 coreos; do
|
||||
mkdir -p %{buildroot}/opt/confluent/lib/osdeploy/common
|
||||
cp common/initramfs/opt/confluent/bin/apiclient %{buildroot}/opt/confluent/lib/osdeploy/common
|
||||
|
||||
for os in rhvh4 el7 el8 el9 el10 debian debian13 genesis suse15 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04 esxi6 esxi7 esxi8 coreos; do
|
||||
mkdir -p %{buildroot}/opt/confluent/lib/osdeploy/$os/initramfs/aarch64/
|
||||
cp ${os}out/addons.* %{buildroot}/opt/confluent/lib/osdeploy/$os/initramfs/aarch64/
|
||||
if [ -d ${os}disklessout ]; then
|
||||
|
||||
@@ -33,7 +33,7 @@ cp -a debian debian13
|
||||
mkdir -p debian13/initramfs/usr
|
||||
mv debian13/initramfs/lib debian13/initramfs/usr/
|
||||
mv el10/initramfs/usr el10/initramfs/var
|
||||
for os in rhvh4 el7 genesis el8 suse15 debian debian13 ubuntu18.04 ubuntu20.04 ubuntu22.04 ubuntu24.04 coreos el9 el10; do
|
||||
for os in rhvh4 el7 genesis el8 suse15 debian debian13 ubuntu18.04 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04 coreos el9 el10; do
|
||||
mkdir ${os}out
|
||||
cd ${os}out
|
||||
if [ -d ../${os}bin ]; then
|
||||
@@ -49,7 +49,7 @@ for os in rhvh4 el7 genesis el8 suse15 debian debian13 ubuntu18.04 ubuntu20.04 u
|
||||
mv ../addons.cpio .
|
||||
cd ..
|
||||
done
|
||||
for os in el7 el8 suse15 el9 el10 ubuntu20.04 ubuntu22.04 ubuntu24.04; do
|
||||
for os in el7 el8 suse15 el9 el10 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04; do
|
||||
mkdir ${os}disklessout
|
||||
cd ${os}disklessout
|
||||
if [ -d ../${os}bin ]; then
|
||||
@@ -89,7 +89,7 @@ cp -a esxi7 esxi9
|
||||
%install
|
||||
mkdir -p %{buildroot}/opt/confluent/share/licenses/confluent_osdeploy/
|
||||
cp LICENSE %{buildroot}/opt/confluent/share/licenses/confluent_osdeploy/
|
||||
for os in rhvh4 el7 el8 el9 el10 genesis suse15 ubuntu20.04 debian debian13 ubuntu18.04 ubuntu22.04 ubuntu24.04 esxi6 esxi7 esxi8 esxi9 coreos; do
|
||||
for os in rhvh4 el7 el8 el9 el10 genesis suse15 ubuntu20.04 debian debian13 ubuntu18.04 ubuntu22.04 ubuntu24.04 ubuntu26.04 esxi6 esxi7 esxi8 esxi9 coreos; do
|
||||
mkdir -p %{buildroot}/opt/confluent/lib/osdeploy/$os/initramfs
|
||||
mkdir -p %{buildroot}/opt/confluent/lib/osdeploy/$os/profiles
|
||||
cp ${os}out/addons.* %{buildroot}/opt/confluent/lib/osdeploy/$os/initramfs
|
||||
|
||||
+14
-6
@@ -110,14 +110,22 @@ else
|
||||
for nic in $(ip link | grep mtu|grep -v LOOPBACK|cut -d: -f 2|sed -e 's/ //'); do
|
||||
ip link set $nic up
|
||||
done
|
||||
for nic in $(ip link | grep mtu|grep -v LOOPBACK|grep LOWER_UP|cut -d: -f 2|sed -e 's/ //'); do
|
||||
if udhcpc -i $nic -p $dhcpid -t 2 -T 2 -n -x 93:0007 -x 97:00$dhuuid -q; then
|
||||
/opt/confluent/bin/copernicus > /etc/confluent/confluent.info
|
||||
if grep ^MANAGER:.*\\. /etc/confluent/confluent.info ; then
|
||||
break
|
||||
nic=
|
||||
while [ -z "$nic" ]; do
|
||||
for nic in $(ip link | grep mtu|grep -v LOOPBACK|grep LOWER_UP|cut -d: -f 2|sed -e 's/ //'); do
|
||||
if udhcpc -i $nic -p $dhcpid -t 2 -T 2 -n -x 93:0007 -x 97:00$dhuuid -q; then
|
||||
/opt/confluent/bin/copernicus > /etc/confluent/confluent.info
|
||||
if grep ^MANAGER:.*\\. /etc/confluent/confluent.info ; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
ip -4 address flush dev $nic
|
||||
nic=""
|
||||
done
|
||||
if [ -z "$nic" ]; then
|
||||
echo "No network interface could be detected, retrying...."
|
||||
sleep 2
|
||||
fi
|
||||
ip -4 flush dev $nic
|
||||
done
|
||||
mgr=$(grep ^MANAGER:.*\\. /etc/confluent/confluent.info|head -n 1|cut -d: -f 2|sed -e 's/ //')
|
||||
nodename=$(grep ^NODENAME: /etc/confluent/confluent.info|head -n 1|cut -d: -f 2|sed -e 's/ //')
|
||||
|
||||
@@ -3,8 +3,8 @@ confluent_whost=$confluent_mgr
|
||||
if [[ "$confluent_whost" == *:* ]] && [[ "$confluent_whost" != "["* ]]; then
|
||||
confluent_whost="[$confluent_mgr]"
|
||||
fi
|
||||
mkdir -p /mnt/remoteimg /mnt/remote /mnt/overlay
|
||||
if [ "untethered" = "$(getarg confluent_imagemethod)" ]; then
|
||||
mkdir -p /mnt/remoteimg /mnt/remote /mnt/overlay /sysroot
|
||||
if [ "untethered" = "$(getarg confluent_imagemethod)" -o "uncompressed" = "$(getarg confluent_imagemethod)" ]; then
|
||||
mount -t tmpfs untethered /mnt/remoteimg
|
||||
curl https://$confluent_whost/confluent-public/os/$confluent_profile/rootimg.sfs -o /mnt/remoteimg/rootimg.sfs
|
||||
else
|
||||
@@ -40,20 +40,53 @@ fi
|
||||
|
||||
|
||||
#mount -t tmpfs overlay /mnt/overlay
|
||||
modprobe zram
|
||||
memtot=$(grep ^MemTotal: /proc/meminfo|awk '{print $2}')
|
||||
memtot=$((memtot/2))$(grep ^MemTotal: /proc/meminfo | awk '{print $3'})
|
||||
echo $memtot > /sys/block/zram0/disksize
|
||||
mkfs.xfs /dev/zram0 > /dev/null
|
||||
mount -o discard /dev/zram0 /mnt/overlay
|
||||
if [ ! -f /tmp/mountparts.sh ]; then
|
||||
mkdir -p /mnt/overlay/upper /mnt/overlay/work
|
||||
mount -t overlay -o upperdir=/mnt/overlay/upper,workdir=/mnt/overlay/work,lowerdir=/mnt/remote disklessroot /sysroot
|
||||
if [ ! "uncompressed" = "$(getarg confluent_imagemethod)" ]; then
|
||||
modprobe zram
|
||||
memtot=$(grep ^MemTotal: /proc/meminfo|awk '{print $2}')
|
||||
memtot=$((memtot/2))$(grep ^MemTotal: /proc/meminfo | awk '{print $3'})
|
||||
echo $memtot > /sys/block/zram0/disksize
|
||||
mkfs.xfs /dev/zram0 > /dev/null
|
||||
fi
|
||||
TETHERED=0
|
||||
if [ "untethered" = "$(getarg confluent_imagemethod)" -o "uncompressed" = "$(getarg confluent_imagemethod)" ]; then
|
||||
if [ "untethered" = "$(getarg confluent_imagemethod)" ]; then
|
||||
mount -o discard /dev/zram0 /sysroot
|
||||
else
|
||||
mount -t tmpfs disklessroot /sysroot
|
||||
fi
|
||||
echo -en "Decrypting and extracting root filesystem: 0%\r"
|
||||
srcsz=$(du -sk /mnt/remote | awk '{print $1}')
|
||||
while [ -f /mnt/remoteimg/rootimg.sfs ]; do
|
||||
dstsz=$(du -sk /sysroot | awk '{print $1}')
|
||||
pct=$((dstsz * 100 / srcsz))
|
||||
if [ $pct -gt 99 ]; then
|
||||
pct=99
|
||||
fi
|
||||
echo -en "Decrypting and extracting root filesystem: $pct%\r"
|
||||
sleep 0.25
|
||||
done &
|
||||
cp -ax /mnt/remote/* /sysroot/
|
||||
umount /mnt/remote
|
||||
if [ -e /dev/mapper/cryptimg ]; then
|
||||
dmsetup remove cryptimg
|
||||
fi
|
||||
losetup -d $loopdev
|
||||
rm /mnt/remoteimg/rootimg.sfs
|
||||
umount /mnt/remoteimg
|
||||
wait
|
||||
echo -e "Decrypting and extracting root filesystem: 100%"
|
||||
else
|
||||
for srcmount in $(cat /tmp/mountparts.sh | awk '{print $3}'); do
|
||||
mkdir -p /mnt/overlay${srcmount}/upper /mnt/overlay${srcmount}/work
|
||||
mount -t overlay -o upperdir=/mnt/overlay${srcmount}/upper,workdir=/mnt/overlay${srcmount}/work,lowerdir=${srcmount} disklesspart /sysroot${srcmount#/mnt/remote}
|
||||
done
|
||||
TETHERED=1
|
||||
mount -o discard /dev/zram0 /mnt/overlay
|
||||
if [ ! -f /tmp/mountparts.sh ]; then
|
||||
mkdir -p /mnt/overlay/upper /mnt/overlay/work
|
||||
mount -t overlay -o upperdir=/mnt/overlay/upper,workdir=/mnt/overlay/work,lowerdir=/mnt/remote disklessroot /sysroot
|
||||
else
|
||||
for srcmount in $(cat /tmp/mountparts.sh | awk '{print $3}'); do
|
||||
mkdir -p /mnt/overlay${srcmount}/upper /mnt/overlay${srcmount}/work
|
||||
mount -t overlay -o upperdir=/mnt/overlay${srcmount}/upper,workdir=/mnt/overlay${srcmount}/work,lowerdir=${srcmount} disklesspart /sysroot${srcmount#/mnt/remote}
|
||||
done
|
||||
fi
|
||||
fi
|
||||
mkdir -p /sysroot/etc/ssh
|
||||
mkdir -p /sysroot/etc/confluent
|
||||
@@ -109,7 +142,7 @@ echo ' EnableSSHKeysign yes' >> $sshconf
|
||||
echo ' HostbasedKeyTypes *ed25519*' >> $sshconf
|
||||
curl -sf -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" https://$confluent_whost/confluent-api/self/nodelist > /sysroot/etc/ssh/shosts.equiv
|
||||
cp /sysroot/etc/ssh/shosts.equiv /sysroot/root/.shosts
|
||||
chmod 640 /sysroot/etc/ssh/*_key
|
||||
chmod 600 /sysroot/etc/ssh/*_key
|
||||
cp /tls/*.pem /sysroot/etc/pki/ca-trust/source/anchors/
|
||||
chroot /sysroot/ update-ca-trust
|
||||
curl -sf https://$confluent_whost/confluent-public/os/$confluent_profile/scripts/onboot.service > /sysroot/etc/systemd/system/onboot.service
|
||||
@@ -129,10 +162,25 @@ ln -s /sysroot/lib/modules/$(uname -r) /lib/modules/
|
||||
mv /lib/firmware /lib/firmware-ramfs
|
||||
ln -s /sysroot/lib/firmware /lib/firmware
|
||||
kill $(grep -l ^/usr/lib/systemd/systemd-udevd /proc/*/cmdline|cut -d/ -f 3)
|
||||
if [ $TETHERED -eq 1 ]; then
|
||||
(
|
||||
sleep 86400 &
|
||||
ONBOOTPID=$!
|
||||
mkdir -p /run/confluent
|
||||
echo $ONBOOTPID > /run/confluent/onboot_sleep.pid
|
||||
wait $ONBOOTPID
|
||||
dd if=/mnt/remoteimg/rootimg.sfs iflag=nocache count=0 >& /dev/null
|
||||
rm -rf /lib/modules/$(uname -r) /lib/modules/$(uname -r)-ramfs /lib/firmware-ramfs /usr/lib64/libcrypto.so* /usr/lib64/systemd/ /kernel/ /usr/bin/ /usr/sbin/ /usr/libexec/
|
||||
) &
|
||||
while [ ! -f /run/confluent/onboot_sleep.pid ]; do
|
||||
sleep 0.1
|
||||
done
|
||||
else
|
||||
rm -rf /lib/modules/$(uname -r) /lib/modules/$(uname -r)-ramfs /lib/firmware-ramfs /usr/lib64/libcrypto.so* /usr/lib64/systemd/ /kernel/ /usr/bin/ /usr/sbin/ /usr/libexec/
|
||||
fi
|
||||
if grep debugssh /proc/cmdline >& /dev/null; then
|
||||
exec /opt/confluent/bin/start_root
|
||||
else
|
||||
rm -rf /lib/modules/$(uname -r) /lib/modules/$(uname -r)-ramfs /lib/firmware-ramfs /usr/lib64/libcrypto.so* /usr/lib64/systemd/ /kernel/ /usr/bin/ /usr/sbin/ /usr/libexec/
|
||||
exec /opt/confluent/bin/start_root -s # share mount namespace, keep kernel callbacks intact
|
||||
fi
|
||||
|
||||
|
||||
@@ -21,17 +21,10 @@ if grep ^ntpservers: /etc/confluent/confluent.deploycfg > /dev/null; then
|
||||
fi
|
||||
|
||||
if [ -f /tmp/timeservers ]; then
|
||||
|
||||
ntpsrvs=$(cat /tmp/timeservers)
|
||||
|
||||
sed -i "1,/^pool * /c\\
|
||||
|
||||
${ntpsrvs//$'\n'/\\$'\n'}" /etc/chrony.conf
|
||||
|
||||
|
||||
systemctl restart chronyd
|
||||
|
||||
rm -f /tmp/timeservers
|
||||
sed -i '/^[[:space:]]*\(pool\|server\)[[:space:]]/d' /etc/chrony.conf
|
||||
cat /tmp/timeservers >> /etc/chrony.conf
|
||||
systemctl restart chronyd
|
||||
rm -f /tmp/timeservers
|
||||
fi
|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ if [[ "$confluent_whost" == *:* ]] && [[ "$confluent_whost" != "["* ]]; then
|
||||
confluent_whost="[$confluent_mgr]"
|
||||
fi
|
||||
mkdir -p /mnt/remoteimg /mnt/remote /mnt/overlay
|
||||
TETHERED=0
|
||||
if [ "untethered" = "$(getarg confluent_imagemethod)" ]; then
|
||||
mount -t tmpfs untethered /mnt/remoteimg
|
||||
curl https://$confluent_whost/confluent-public/os/$confluent_profile/rootimg.sfs -o /mnt/remoteimg/rootimg.sfs
|
||||
else
|
||||
TETHERED=1
|
||||
confluent_urls="$confluent_urls https://$confluent_whost/confluent-public/os/$confluent_profile/rootimg.sfs"
|
||||
/opt/confluent/bin/urlmount $confluent_urls /mnt/remoteimg
|
||||
fi
|
||||
@@ -130,4 +132,17 @@ ln -s /sysroot/lib/modules/$(uname -r) /lib/modules/
|
||||
mv /lib/firmware /lib/firmware-ramfs
|
||||
ln -s /sysroot/lib/firmware /lib/firmware
|
||||
kill $(grep -l ^/usr/lib/systemd/systemd-udevd /proc/*/cmdline|cut -d/ -f 3)
|
||||
if [ $TETHERED -eq 1 ]; then
|
||||
(
|
||||
sleep 86400 &
|
||||
ONBOOTPID=$!
|
||||
mkdir -p /run/confluent
|
||||
echo $ONBOOTPID > /run/confluent/onboot_sleep.pid
|
||||
wait $ONBOOTPID
|
||||
dd if=/mnt/remoteimg/rootimg.sfs iflag=nocache count=0 >& /dev/null
|
||||
) &
|
||||
while [ ! -f /run/confluent/onboot_sleep.pid ]; do
|
||||
sleep 0.1
|
||||
done
|
||||
fi
|
||||
exec /opt/confluent/bin/start_root
|
||||
|
||||
@@ -27,17 +27,10 @@ if grep ^ntpservers: /etc/confluent/confluent.deploycfg > /dev/null; then
|
||||
fi
|
||||
|
||||
if [ -f /tmp/timeservers ]; then
|
||||
|
||||
ntpsrvs=$(cat /tmp/timeservers)
|
||||
|
||||
sed -i "1,/^pool * /c\\
|
||||
${ntpsrvs//$'\n'/\\$'\n'}" /etc/chrony.conf
|
||||
|
||||
|
||||
systemctl restart chronyd
|
||||
|
||||
rm -f /tmp/timeservers
|
||||
|
||||
sed -i '/^[[:space:]]*\(pool\|server\)[[:space:]]/d' /etc/chrony.conf
|
||||
cat /tmp/timeservers >> /etc/chrony.conf
|
||||
systemctl restart chronyd
|
||||
rm -f /tmp/timeservers
|
||||
fi
|
||||
|
||||
|
||||
@@ -68,5 +61,14 @@ run_remote_parts onboot.d
|
||||
# Induce execution of remote configuration, e.g. ansible plays in ansible/onboot.d/
|
||||
run_remote_config onboot.d
|
||||
|
||||
if [ -f /run/confluent/onboot_sleep.pid ]; then
|
||||
loopdev=$(losetup -j /mnt/remoteimg/rootimg.sfs|cut -d: -f 1)
|
||||
if [ -n "$loopdev" ]; then
|
||||
losetup "$loopdev" --direct-io=on
|
||||
fi
|
||||
sleeppid=$(cat /run/confluent/onboot_sleep.pid)
|
||||
kill "$sleeppid"
|
||||
rm -f /run/confluent/onboot_sleep.pid
|
||||
fi
|
||||
#curl -X POST -d 'status: booted' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $confluent_apikey" https://$confluent_mgr/confluent-api/self/updatestatus
|
||||
kill $logshowpid
|
||||
|
||||
@@ -277,7 +277,10 @@ def synchronize():
|
||||
try:
|
||||
uid = pwd.getpwnam(opts[fname][opt]['name']).pw_uid
|
||||
except KeyError:
|
||||
uid = opts[fname][opt]['id']
|
||||
try:
|
||||
uid = opts[fname][opt]['id']
|
||||
except KeyError:
|
||||
raise Exception(f"Unable to map owner of {fname}")
|
||||
elif opt == 'group':
|
||||
try:
|
||||
gid = grp.getgrnam(opts[fname][opt]['name']).gr_gid
|
||||
|
||||
@@ -3,7 +3,7 @@ sed -i 's/centos/CentOS/; s/rhel/Red Hat Enterprise Linux/; s/oraclelinux/Oracle
|
||||
if grep Fedora $2/profile.yaml > /dev/null; then
|
||||
sed -i 's/@^minimal-environment/#/' $2/packagelist
|
||||
fi
|
||||
if grep ^label: $2/profile.yaml | grep 10 > /dev/null; then
|
||||
if grep ^label: $2/profile.yaml | grep ' 10' > /dev/null; then
|
||||
echo 'echo openssh-keysign >> /tmp/addonpackages' > $2/scripts/pre.d/enablekeysign
|
||||
chmod 644 $2/scripts/pre.d/enablekeysign
|
||||
fi
|
||||
|
||||
@@ -467,7 +467,7 @@ def install_to_disk(imgpath):
|
||||
subprocess.check_call(['pvcreate', '-ff', '-y', lvmpart])
|
||||
subprocess.check_call(['vgcreate', vgname, lvmpart])
|
||||
vgroupmap = {}
|
||||
if yaml and vgmap:
|
||||
if yaml and vgmap and os.path.exists('/tmp/volumegroupmap.yml'):
|
||||
with open('/tmp/volumegroupmap.yml') as mapin:
|
||||
vgroupmap = yaml.safe_load(mapin)
|
||||
donedisks = {}
|
||||
|
||||
@@ -3,8 +3,8 @@ confluent_whost=$confluent_mgr
|
||||
if [[ "$confluent_whost" == *:* ]] && [[ "$confluent_whost" != "["* ]]; then
|
||||
confluent_whost="[$confluent_mgr]"
|
||||
fi
|
||||
mkdir -p /mnt/remoteimg /mnt/remote /mnt/overlay
|
||||
if [ "untethered" = "$(getarg confluent_imagemethod)" ]; then
|
||||
mkdir -p /mnt/remoteimg /mnt/remote /mnt/overlay /sysroot
|
||||
if [ "untethered" = "$(getarg confluent_imagemethod)" -o "uncompressed" = "$(getarg confluent_imagemethod)" ]; then
|
||||
mount -t tmpfs untethered /mnt/remoteimg
|
||||
curl https://$confluent_whost/confluent-public/os/$confluent_profile/rootimg.sfs -o /mnt/remoteimg/rootimg.sfs
|
||||
else
|
||||
@@ -40,20 +40,53 @@ fi
|
||||
|
||||
|
||||
#mount -t tmpfs overlay /mnt/overlay
|
||||
modprobe zram
|
||||
memtot=$(grep ^MemTotal: /proc/meminfo|awk '{print $2}')
|
||||
memtot=$((memtot/2))$(grep ^MemTotal: /proc/meminfo | awk '{print $3'})
|
||||
echo $memtot > /sys/block/zram0/disksize
|
||||
mkfs.xfs /dev/zram0 > /dev/null
|
||||
mount -o discard /dev/zram0 /mnt/overlay
|
||||
if [ ! -f /tmp/mountparts.sh ]; then
|
||||
mkdir -p /mnt/overlay/upper /mnt/overlay/work
|
||||
mount -t overlay -o upperdir=/mnt/overlay/upper,workdir=/mnt/overlay/work,lowerdir=/mnt/remote disklessroot /sysroot
|
||||
if [ ! "uncompressed" = "$(getarg confluent_imagemethod)" ]; then
|
||||
modprobe zram
|
||||
memtot=$(grep ^MemTotal: /proc/meminfo|awk '{print $2}')
|
||||
memtot=$((memtot/2))$(grep ^MemTotal: /proc/meminfo | awk '{print $3'})
|
||||
echo $memtot > /sys/block/zram0/disksize
|
||||
mkfs.xfs /dev/zram0 > /dev/null
|
||||
fi
|
||||
TETHERED=0
|
||||
if [ "untethered" = "$(getarg confluent_imagemethod)" -o "uncompressed" = "$(getarg confluent_imagemethod)" ]; then
|
||||
if [ "untethered" = "$(getarg confluent_imagemethod)" ]; then
|
||||
mount -o discard /dev/zram0 /sysroot
|
||||
else
|
||||
mount -t tmpfs disklessroot /sysroot
|
||||
fi
|
||||
echo -en "Decrypting and extracting root filesystem: 0%\r"
|
||||
srcsz=$(du -sk /mnt/remote | awk '{print $1}')
|
||||
while [ -f /mnt/remoteimg/rootimg.sfs ]; do
|
||||
dstsz=$(du -sk /sysroot | awk '{print $1}')
|
||||
pct=$((dstsz * 100 / srcsz))
|
||||
if [ $pct -gt 99 ]; then
|
||||
pct=99
|
||||
fi
|
||||
echo -en "Decrypting and extracting root filesystem: $pct%\r"
|
||||
sleep 0.25
|
||||
done &
|
||||
cp -ax /mnt/remote/* /sysroot/
|
||||
umount /mnt/remote
|
||||
if [ -e /dev/mapper/cryptimg ]; then
|
||||
dmsetup remove cryptimg
|
||||
fi
|
||||
losetup -d $loopdev
|
||||
rm /mnt/remoteimg/rootimg.sfs
|
||||
umount /mnt/remoteimg
|
||||
wait
|
||||
echo -e "Decrypting and extracting root filesystem: 100%"
|
||||
else
|
||||
for srcmount in $(cat /tmp/mountparts.sh | awk '{print $3}'); do
|
||||
mkdir -p /mnt/overlay${srcmount}/upper /mnt/overlay${srcmount}/work
|
||||
mount -t overlay -o upperdir=/mnt/overlay${srcmount}/upper,workdir=/mnt/overlay${srcmount}/work,lowerdir=${srcmount} disklesspart /sysroot${srcmount#/mnt/remote}
|
||||
done
|
||||
TETHERED=1
|
||||
mount -o discard /dev/zram0 /mnt/overlay
|
||||
if [ ! -f /tmp/mountparts.sh ]; then
|
||||
mkdir -p /mnt/overlay/upper /mnt/overlay/work
|
||||
mount -t overlay -o upperdir=/mnt/overlay/upper,workdir=/mnt/overlay/work,lowerdir=/mnt/remote disklessroot /sysroot
|
||||
else
|
||||
for srcmount in $(cat /tmp/mountparts.sh | awk '{print $3}'); do
|
||||
mkdir -p /mnt/overlay${srcmount}/upper /mnt/overlay${srcmount}/work
|
||||
mount -t overlay -o upperdir=/mnt/overlay${srcmount}/upper,workdir=/mnt/overlay${srcmount}/work,lowerdir=${srcmount} disklesspart /sysroot${srcmount#/mnt/remote}
|
||||
done
|
||||
fi
|
||||
fi
|
||||
mkdir -p /sysroot/etc/ssh
|
||||
mkdir -p /sysroot/etc/confluent
|
||||
@@ -109,8 +142,10 @@ echo ' EnableSSHKeysign yes' >> $sshconf
|
||||
echo ' HostbasedKeyTypes *ed25519*' >> $sshconf
|
||||
curl -sf -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" https://$confluent_whost/confluent-api/self/nodelist > /sysroot/etc/ssh/shosts.equiv
|
||||
cp /sysroot/etc/ssh/shosts.equiv /sysroot/root/.shosts
|
||||
chmod 640 /sysroot/etc/ssh/*_key
|
||||
chroot /sysroot chgrp ssh_keys /etc/ssh/*_key
|
||||
if grep ^ssh_keys: /etc/group > /dev/null; then
|
||||
chmod 640 /sysroot/etc/ssh/*_key
|
||||
chroot /sysroot chgrp ssh_keys /etc/ssh/*_key
|
||||
fi
|
||||
cp /tls/*.pem /sysroot/etc/pki/ca-trust/source/anchors/
|
||||
chroot /sysroot/ update-ca-trust
|
||||
curl -sf https://$confluent_whost/confluent-public/os/$confluent_profile/scripts/onboot.service > /sysroot/etc/systemd/system/onboot.service
|
||||
@@ -131,9 +166,35 @@ mv /lib/firmware /lib/firmware-ramfs
|
||||
ln -s /sysroot/lib/firmware /lib/firmware
|
||||
rm -f /sysroot/etc/dracut.conf.d/diskless.conf # remove diskless dracut from runtime, to make kdump happier
|
||||
kill $(grep -l ^/usr/lib/systemd/systemd-udevd /proc/*/cmdline|cut -d/ -f 3)
|
||||
if grep debugssh /proc/cmdline >& /dev/null; then
|
||||
debugssh=1
|
||||
else
|
||||
debugssh=0
|
||||
fi
|
||||
if [ $TETHERED -eq 1 ]; then
|
||||
# In tethered mode, the double-caching is useful to get through tricky part of
|
||||
# onboot with confignet. After that, it's excessive cache usage.
|
||||
# Give the onboot script a hook to have us come in and enable directio to the
|
||||
# squashfs and drop the cache of the rootimg so far
|
||||
(
|
||||
sleep 86400 &
|
||||
ONBOOTPID=$!
|
||||
mkdir -p /run/confluent
|
||||
echo $ONBOOTPID > /run/confluent/onboot_sleep.pid
|
||||
wait $ONBOOTPID
|
||||
dd if=/mnt/remoteimg/rootimg.sfs iflag=nocache count=0 >& /dev/null
|
||||
if [ $debugssh -eq 0 ]; then
|
||||
rm -rf /lib/modules/$(uname -r) /lib/modules/$(uname -r)-ramfs /lib/firmware-ramfs /usr/lib64/libcrypto.so* /usr/lib64/systemd/ /kernel/ /usr/bin/ /usr/sbin/ /usr/libexec/
|
||||
fi
|
||||
) &
|
||||
while [ ! -f /run/confluent/onboot_sleep.pid ]; do
|
||||
sleep 0.1
|
||||
done
|
||||
elif [ $debugssh -eq 0 ]; then
|
||||
rm -rf /lib/modules/$(uname -r) /lib/modules/$(uname -r)-ramfs /lib/firmware-ramfs /usr/lib64/libcrypto.so* /usr/lib64/systemd/ /kernel/ /usr/bin/ /usr/sbin/ /usr/libexec/
|
||||
fi
|
||||
if grep debugssh /proc/cmdline >& /dev/null; then
|
||||
exec /opt/confluent/bin/start_root
|
||||
else
|
||||
rm -rf /lib/modules/$(uname -r) /lib/modules/$(uname -r)-ramfs /lib/firmware-ramfs /usr/lib64/libcrypto.so* /usr/lib64/systemd/ /kernel/ /usr/bin/ /usr/sbin/ /usr/libexec/
|
||||
exec /opt/confluent/bin/start_root -s # share mount namespace, keep kernel callbacks intact
|
||||
fi
|
||||
|
||||
@@ -26,17 +26,10 @@ if grep ^ntpservers: /etc/confluent/confluent.deploycfg > /dev/null; then
|
||||
fi
|
||||
|
||||
if [ -f /tmp/timeservers ]; then
|
||||
|
||||
ntpsrvs=$(cat /tmp/timeservers)
|
||||
|
||||
sed -i "1,/^pool * /c\\
|
||||
|
||||
${ntpsrvs//$'\n'/\\$'\n'}" /etc/chrony.conf
|
||||
|
||||
|
||||
systemctl restart chronyd
|
||||
|
||||
rm -f /tmp/timeservers
|
||||
sed -i '/^[[:space:]]*\(pool\|server\)[[:space:]]/d' /etc/chrony.conf
|
||||
cat /tmp/timeservers >> /etc/chrony.conf
|
||||
systemctl restart chronyd
|
||||
rm -f /tmp/timeservers
|
||||
fi
|
||||
|
||||
export nodename confluent_mgr confluent_profile
|
||||
@@ -62,5 +55,15 @@ run_remote_parts onboot.d
|
||||
# Induce execution of remote configuration, e.g. ansible plays in ansible/onboot.d/
|
||||
run_remote_config onboot.d
|
||||
|
||||
if [ -f /run/confluent/onboot_sleep.pid ]; then
|
||||
loopdev=$(losetup -j /mnt/remoteimg/rootimg.sfs|cut -d: -f 1)
|
||||
if [ -n "$loopdev" ]; then
|
||||
losetup "$loopdev" --direct-io=on
|
||||
fi
|
||||
sleeppid=$(cat /run/confluent/onboot_sleep.pid)
|
||||
kill "$sleeppid"
|
||||
rm -f /run/confluent/onboot_sleep.pid
|
||||
fi
|
||||
|
||||
#curl -X POST -d 'status: booted' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $confluent_apikey" https://$confluent_mgr/confluent-api/self/updatestatus
|
||||
kill $logshowpid
|
||||
|
||||
@@ -56,7 +56,7 @@ cat /tls/*.pem > /etc/pki/tls/certs/ca-bundle.crt
|
||||
TRIES=0
|
||||
touch /etc/confluent/confluent.info
|
||||
TRIES=5
|
||||
echo -n "Waitiing for disks..."
|
||||
echo -n "Waiting for disks..."
|
||||
while [ ! -e /dev/disk/by-label ] && [ $TRIES -gt 0 ]; do
|
||||
sleep 1
|
||||
TRIES=$((TRIES - 1))
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# It is advised to avoid /var/lib/confluent/public as a source for syncing. /var/lib/confluent/public
|
||||
# is served without authentication and thus any sensitive content would be a risk. If wanting to host
|
||||
# syncfiles on a common share, it is suggested to have /var/lib/confluent be the share and use some other
|
||||
# subdirectory other than public.
|
||||
#
|
||||
# Syncing is performed as the 'confluent' user, so all source files must be accessible by the confluent user.
|
||||
#
|
||||
# This file lists files to synchronize or merge to the deployed systems from the deployment server
|
||||
# To specify taking /some/path/hosts on the deployment server and duplicating it to /etc/hosts:
|
||||
# Note particularly the use of '->' to delineate source from target.
|
||||
# /some/path/hosts -> /etc/hosts
|
||||
|
||||
# If wanting to simply use the same path for source and destinaiton, the -> may be skipped:
|
||||
# /etc/hosts
|
||||
|
||||
# More function is available, for example to limit the entry to run only on n1 through n8, and to set
|
||||
# owner, group, and permissions in octal notation:
|
||||
# /example/source -> n1-n8:/etc/target (owner=root,group=root,permissions=600)
|
||||
|
||||
# Entries under APPENDONCE: will be added to specified target, only if the target does not already
|
||||
# contain the data in the source already in its entirety. This allows append in a fashion that
|
||||
# is friendly to being run repeatedly
|
||||
|
||||
# Entries under MERGE: will attempt to be intelligently merged. This supports /etc/group and /etc/passwd
|
||||
# Any supporting entries in /etc/shadow or /etc/gshadow are added automatically, with password disabled
|
||||
# It also will not inject 'system' ids (under 1,000 usually) as those tend to be local and rpm managed.
|
||||
MERGE:
|
||||
# /etc/passwd
|
||||
# /etc/group
|
||||
@@ -8,7 +8,9 @@ for addr in $(grep ^MANAGER: /etc/confluent/confluent.info|awk '{print $2}'|sed
|
||||
fi
|
||||
done
|
||||
mkdir -p /mnt/remoteimg /mnt/remote /mnt/overlay
|
||||
if grep confluent_imagemethod=untethered /proc/cmdline > /dev/null; then
|
||||
TETHERED=1
|
||||
if grep -q confluent_imagemethod=untethered /proc/cmdline || grep -q confluent_imagemethod=uncompressed /proc/cmdline; then
|
||||
TETHERED=0
|
||||
mount -t tmpfs untethered /mnt/remoteimg
|
||||
curl https://$confluent_mgr/confluent-public/os/$confluent_profile/rootimg.sfs -o /mnt/remoteimg/rootimg.sfs
|
||||
else
|
||||
@@ -44,15 +46,46 @@ fi
|
||||
|
||||
|
||||
#mount -t tmpfs overlay /mnt/overlay
|
||||
modprobe zram
|
||||
memtot=$(grep ^MemTotal: /proc/meminfo|awk '{print $2}')
|
||||
memtot=$((memtot/2))$(grep ^MemTotal: /proc/meminfo | awk '{print $3'})
|
||||
echo $memtot > /sys/block/zram0/disksize
|
||||
modprobe xfs
|
||||
mkdir /sysroot
|
||||
mkfs.xfs /dev/zram0 > /dev/null
|
||||
mount -o discard /dev/zram0 /mnt/overlay
|
||||
if [ ! -f /tmp/mountparts.sh ]; then
|
||||
if ! grep -q confluent_imagemethod=uncompressed /proc/cmdline; then
|
||||
modprobe zram
|
||||
memtot=$(grep ^MemTotal: /proc/meminfo|awk '{print $2}')
|
||||
memtot=$((memtot/2))$(grep ^MemTotal: /proc/meminfo | awk '{print $3'})
|
||||
echo $memtot > /sys/block/zram0/disksize
|
||||
modprobe xfs
|
||||
mkfs.xfs /dev/zram0 > /dev/null
|
||||
if [ "$TETHERED" = 1 ]; then
|
||||
mount -o discard /dev/zram0 /mnt/overlay
|
||||
else
|
||||
mount -o discard /dev/zram0 /sysroot
|
||||
fi
|
||||
mount -o discard /dev/zram0 /mnt/overlay
|
||||
elif grep -q confluent_imagemethod=uncompressed /proc/cmdline; then
|
||||
mount -t tmpfs disklessroot /sysroot
|
||||
fi
|
||||
if [ "$TETHERED" = 0 ]; then
|
||||
echo -en "Decrypting and extracting root filesystem: 0%\r"
|
||||
srcsz=$(du -sk /mnt/remote | awk '{print $1}')
|
||||
while [ -f /mnt/remoteimg/rootimg.sfs ]; do
|
||||
dstsz=$(du -sk /sysroot | awk '{print $1}')
|
||||
pct=$((dstsz * 100 / srcsz))
|
||||
if [ $pct -gt 99 ]; then
|
||||
pct=99
|
||||
fi
|
||||
echo -en "Decrypting and extracting root filesystem: $pct%\r"
|
||||
sleep 0.25
|
||||
done &
|
||||
cp -a /mnt/remote/* /sysroot/
|
||||
umount /mnt/remote
|
||||
if [ -e /dev/mapper/cryptimg ]; then
|
||||
dmsetup remove cryptimg
|
||||
fi
|
||||
losetup -d $loopdev
|
||||
rm /mnt/remoteimg/rootimg.sfs
|
||||
umount /mnt/remoteimg
|
||||
wait
|
||||
echo -e "Decrypting and extracting root filesystem: 100%"
|
||||
elif [ ! -f /tmp/mountparts.sh ]; then
|
||||
mkdir -p /mnt/overlay/upper /mnt/overlay/work
|
||||
mount -t overlay -o upperdir=/mnt/overlay/upper,workdir=/mnt/overlay/work,lowerdir=/mnt/remote disklessroot /sysroot
|
||||
else
|
||||
@@ -68,6 +101,7 @@ cp /root/.ssh/* /sysroot/root/.ssh
|
||||
chmod 700 /sysroot/root/.ssh
|
||||
cp /etc/confluent/* /sysroot/etc/confluent/
|
||||
cp /etc/ssh/*key* /sysroot/etc/ssh/
|
||||
cp /tls/* /sysroot/etc/ssl/certs
|
||||
for pubkey in /etc/ssh/ssh_host*key.pub; do
|
||||
certfile=${pubkey/.pub/-cert.pub}
|
||||
privfile=${pubkey%.pub}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# It is advised to avoid /var/lib/confluent/public as a source for syncing. /var/lib/confluent/public
|
||||
# is served without authentication and thus any sensitive content would be a risk. If wanting to host
|
||||
# syncfiles on a common share, it is suggested to have /var/lib/confluent be the share and use some other
|
||||
# subdirectory other than public.
|
||||
#
|
||||
# Syncing is performed as the 'confluent' user, so all source files must be accessible by the confluent user.
|
||||
#
|
||||
# This file lists files to synchronize or merge to the deployed systems from the deployment server
|
||||
# To specify taking /some/path/hosts on the deployment server and duplicating it to /etc/hosts:
|
||||
# Note particularly the use of '->' to delineate source from target.
|
||||
# /some/path/hosts -> /etc/hosts
|
||||
|
||||
# If wanting to simply use the same path for source and destinaiton, the -> may be skipped:
|
||||
# /etc/hosts
|
||||
|
||||
# More function is available, for example to limit the entry to run only on n1 through n8, and to set
|
||||
# owner, group, and permissions in octal notation:
|
||||
# /example/source -> n1-n8:/etc/target (owner=root,group=root,permissions=600)
|
||||
|
||||
# Entries under APPENDONCE: will be added to specified target, only if the target does not already
|
||||
# contain the data in the source already in its entirety. This allows append in a fashion that
|
||||
# is friendly to being run repeatedly
|
||||
|
||||
# Entries under MERGE: will attempt to be intelligently merged. This supports /etc/group and /etc/passwd
|
||||
# Any supporting entries in /etc/shadow or /etc/gshadow are added automatically, with password disabled
|
||||
# It also will not inject 'system' ids (under 1,000 usually) as those tend to be local and rpm managed.
|
||||
MERGE:
|
||||
# /etc/passwd
|
||||
# /etc/group
|
||||
@@ -7,7 +7,7 @@ exec >> /target/var/log/confluent/confluent-firstboot.log
|
||||
exec 2>> /target/var/log/confluent/confluent-firstboot.log
|
||||
chmod 600 /target/var/log/confluent/confluent-firstboot.log
|
||||
cp -a /etc/confluent/ssh/* /etc/ssh/
|
||||
systemctl restart sshd
|
||||
systemctl restart ssh
|
||||
rootpw=$(grep ^rootpassword: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
if [ ! -z "$rootpw" -a "$rootpw" != "null" ]; then
|
||||
echo root:$rootpw | chpasswd -e
|
||||
|
||||
@@ -33,7 +33,7 @@ done
|
||||
echo HostbasedAuthentication yes >> /etc/ssh/sshd_config.d/confluent.conf
|
||||
echo HostbasedUsesNameFromPacketOnly yes >> /etc/ssh/sshd_config.d/confluent.conf
|
||||
echo IgnoreRhosts no >> /etc/ssh/sshd_config.d/confluent.conf
|
||||
systemctl restart sshd
|
||||
systemctl restart ssh
|
||||
mkdir -p /etc/confluent
|
||||
export nodename confluent_profile confluent_mgr
|
||||
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/functions > /etc/confluent/functions
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# It is advised to avoid /var/lib/confluent/public as a source for syncing. /var/lib/confluent/public
|
||||
# is served without authentication and thus any sensitive content would be a risk. If wanting to host
|
||||
# syncfiles on a common share, it is suggested to have /var/lib/confluent be the share and use some other
|
||||
# subdirectory other than public.
|
||||
#
|
||||
# Syncing is performed as the 'confluent' user, so all source files must be accessible by the confluent user.
|
||||
#
|
||||
# This file lists files to synchronize or merge to the deployed systems from the deployment server
|
||||
# To specify taking /some/path/hosts on the deployment server and duplicating it to /etc/hosts:
|
||||
# Note particularly the use of '->' to delineate source from target.
|
||||
# /some/path/hosts -> /etc/hosts
|
||||
|
||||
# If wanting to simply use the same path for source and destinaiton, the -> may be skipped:
|
||||
# /etc/hosts
|
||||
|
||||
# More function is available, for example to limit the entry to run only on n1 through n8, and to set
|
||||
# owner, group, and permissions in octal notation:
|
||||
# /example/source -> n1-n8:/etc/target (owner=root,group=root,permissions=600)
|
||||
|
||||
# Entries under APPENDONCE: will be added to specified target, only if the target does not already
|
||||
# contain the data in the source already in its entirety. This allows append in a fashion that
|
||||
# is friendly to being run repeatedly
|
||||
|
||||
# Entries under MERGE: will attempt to be intelligently merged. This supports /etc/group and /etc/passwd
|
||||
# Any supporting entries in /etc/shadow or /etc/gshadow are added automatically, with password disabled
|
||||
# It also will not inject 'system' ids (under 1,000 usually) as those tend to be local and rpm managed.
|
||||
MERGE:
|
||||
# /etc/passwd
|
||||
# /etc/group
|
||||
@@ -37,10 +37,20 @@ else
|
||||
chroot . custom-installation/confluent/bin/clortho $NODENAME $MGR > /root/custom-installation/confluent/confluent.apikey
|
||||
APIKEY=$(cat /root/custom-installation/confluent/confluent.apikey)
|
||||
done
|
||||
MGR=[$MGR]
|
||||
nic=$(grep ^MANAGER /custom-installation/confluent/confluent.info|grep fe80::|sed -e s/.*%//|head -n 1)
|
||||
nic=$(ip link |grep ^$nic:|awk '{print $2}')
|
||||
DEVICE=${nic%:}
|
||||
if echo "$MGR" | grep -q ':'; then
|
||||
# IPv6 manager: wrap address in brackets and resolve interface from scoped manager entry.
|
||||
MGR=[$MGR]
|
||||
nic=$(grep ^MANAGER /custom-installation/confluent/confluent.info|grep fe80::|sed -e s/.*%//|head -n 1)
|
||||
nic=$(ip link |grep ^$nic:|awk '{print $2}')
|
||||
DEVICE=${nic%:}
|
||||
else
|
||||
# IPv4 routed deployment: use previously detected NIC, fallback to route lookup.
|
||||
if [ -f /tmp/autodetectnic ]; then
|
||||
DEVICE=$(cat /tmp/autodetectnic)
|
||||
else
|
||||
DEVICE=$(ip route get ${MGR} 2>/dev/null | head -1 | sed -n 's/.*dev \([^ ]*\).*/\1/p')
|
||||
fi
|
||||
fi
|
||||
IP=done
|
||||
fi
|
||||
if [ -z "$MGTIFACE" ]; then
|
||||
|
||||
@@ -97,6 +97,62 @@ while ! grep NODENAME /custom-installation/confluent/confluent.info; do
|
||||
echo -n $(grep ^apitoken: cnflnt.yml|awk '{print $2}') > $hmackeyfile
|
||||
cd -
|
||||
umount $tmnt
|
||||
elif confluentsrv=$(sed -n 's/.*confluent=\([^ ]*\).*/\1/p' /proc/cmdline); [ ! -z "$confluentsrv" ]; then
|
||||
echo "confluent= kernel arg found: $confluentsrv" > /dev/console 2>&1
|
||||
. /scripts/functions
|
||||
|
||||
rmmod cdc_ether 2> /dev/null
|
||||
rm -rf /run/net* /run/dhcpcd /var/lib/dhcpcd
|
||||
for dev in $(ip a|grep MULTICAST|awk '{print $2}'|sed -e s/://); do
|
||||
ip a flush $dev
|
||||
echo 1 > /proc/sys/net/ipv6/conf/$dev/addr_gen_mode 2>/dev/null
|
||||
echo 0 > /proc/sys/net/ipv6/conf/$dev/addr_gen_mode 2>/dev/null
|
||||
done
|
||||
unset DEVICE DEVICE6 IP IP6 dev
|
||||
|
||||
echo "Starting DHCP configure_networking..." > /dev/console 2>&1
|
||||
configure_networking
|
||||
echo "DHCP done, DEVICE=$DEVICE" > /dev/console 2>&1
|
||||
echo $DEVICE > /tmp/autodetectnic
|
||||
|
||||
RETRIES=0
|
||||
while [ $RETRIES -lt 5 ]; do
|
||||
if openssl s_client -connect $confluentsrv:443 </dev/null > /dev/null 2>&1; then
|
||||
echo "TLS connectivity to $confluentsrv OK" > /dev/console 2>&1
|
||||
break
|
||||
fi
|
||||
RETRIES=$((RETRIES + 1))
|
||||
echo "Cannot reach $confluentsrv:443, retry $RETRIES/5..." > /dev/console 2>&1
|
||||
sleep 3
|
||||
done
|
||||
|
||||
if [ $RETRIES -ge 5 ]; then
|
||||
echo "Failed to reach $confluentsrv after 5 retries, falling back to copernicus" > /dev/console 2>&1
|
||||
/opt/confluent/bin/copernicus -t > /custom-installation/confluent/confluent.info
|
||||
continue
|
||||
fi
|
||||
|
||||
myids="uuid=$(cat /sys/devices/virtual/dmi/id/product_uuid)"
|
||||
for mac in $(ip link | grep 'link/ether' | awk '{print $2}'); do
|
||||
myids="$myids/mac=$mac"
|
||||
done
|
||||
echo "Calling whoami with IDs: $myids" > /dev/console 2>&1
|
||||
|
||||
myname=$( (printf "GET /confluent-api/self/whoami HTTP/1.0\r\nHost: $confluentsrv\r\nCONFLUENT_IDS: $myids\r\n\r\n"; sleep 3) \
|
||||
| openssl s_client -connect $confluentsrv:443 -quiet 2>/dev/null \
|
||||
| tail -1 | tr -d '\r\n')
|
||||
|
||||
echo "whoami returned: '$myname'" > /dev/console 2>&1
|
||||
|
||||
if [ ! -z "$myname" ]; then
|
||||
MGR=$confluentsrv
|
||||
echo "NODENAME: $myname" > /custom-installation/confluent/confluent.info
|
||||
echo "MANAGER: $confluentsrv" >> /custom-installation/confluent/confluent.info
|
||||
echo "EXTMGRINFO: $confluentsrv||1" >> /custom-installation/confluent/confluent.info
|
||||
else
|
||||
echo "whoami returned empty, retrying in 10s..." > /dev/console 2>&1
|
||||
sleep 10
|
||||
fi
|
||||
else
|
||||
/opt/confluent/bin/copernicus -t > /custom-installation/confluent/confluent.info
|
||||
fi
|
||||
|
||||
@@ -23,6 +23,7 @@ touch /etc/cloud/cloud-init.disabled
|
||||
source /etc/confluent/functions
|
||||
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg|awk '{print $2}')
|
||||
export confluent_mgr confluent_profile
|
||||
run_remote_python confignet
|
||||
run_remote_parts firstboot.d
|
||||
run_remote_config firstboot.d
|
||||
curl --capath /etc/confluent/tls -f -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $confluent_apikey" -X POST -d "status: complete" https://$confluent_mgr/confluent-api/self/updatestatus
|
||||
|
||||
@@ -89,7 +89,6 @@ chroot /target update-ca-certificates
|
||||
chroot /target bash -c "source /etc/confluent/functions; run_remote_python autoconsole"
|
||||
|
||||
chroot /target bash -c "source /etc/confluent/functions; run_remote_python syncfileclient"
|
||||
chroot /target bash -c "source /etc/confluent/functions; run_remote_python confignet"
|
||||
chroot /target bash -c "source /etc/confluent/functions; run_remote_parts post.d"
|
||||
source /target/etc/confluent/functions
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ done
|
||||
echo HostbasedAuthentication yes >> /etc/ssh/sshd_config.d/confluent.conf
|
||||
echo HostbasedUsesNameFromPacketOnly yes >> /etc/ssh/sshd_config.d/confluent.conf
|
||||
echo IgnoreRhosts no >> /etc/ssh/sshd_config.d/confluent.conf
|
||||
systemctl restart sshd
|
||||
systemctl restart ssh
|
||||
mkdir -p /etc/confluent
|
||||
export nodename confluent_profile confluent_mgr
|
||||
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/functions > /etc/confluent/functions
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# It is advised to avoid /var/lib/confluent/public as a source for syncing. /var/lib/confluent/public
|
||||
# is served without authentication and thus any sensitive content would be a risk. If wanting to host
|
||||
# syncfiles on a common share, it is suggested to have /var/lib/confluent be the share and use some other
|
||||
# subdirectory other than public.
|
||||
#
|
||||
# Syncing is performed as the 'confluent' user, so all source files must be accessible by the confluent user.
|
||||
#
|
||||
# This file lists files to synchronize or merge to the deployed systems from the deployment server
|
||||
# To specify taking /some/path/hosts on the deployment server and duplicating it to /etc/hosts:
|
||||
# Note particularly the use of '->' to delineate source from target.
|
||||
# /some/path/hosts -> /etc/hosts
|
||||
|
||||
# If wanting to simply use the same path for source and destinaiton, the -> may be skipped:
|
||||
# /etc/hosts
|
||||
|
||||
# More function is available, for example to limit the entry to run only on n1 through n8, and to set
|
||||
# owner, group, and permissions in octal notation:
|
||||
# /example/source -> n1-n8:/etc/target (owner=root,group=root,permissions=600)
|
||||
|
||||
# Entries under APPENDONCE: will be added to specified target, only if the target does not already
|
||||
# contain the data in the source already in its entirety. This allows append in a fashion that
|
||||
# is friendly to being run repeatedly
|
||||
|
||||
# Entries under MERGE: will attempt to be intelligently merged. This supports /etc/group and /etc/passwd
|
||||
# Any supporting entries in /etc/shadow or /etc/gshadow are added automatically, with password disabled
|
||||
# It also will not inject 'system' ids (under 1,000 usually) as those tend to be local and rpm managed.
|
||||
MERGE:
|
||||
# /etc/passwd
|
||||
# /etc/group
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
ubuntu22.04/
|
||||
+1
@@ -0,0 +1 @@
|
||||
ubuntu20.04-diskless/
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <dirent.h>
|
||||
|
||||
#define COM1 0x3f8
|
||||
#define COM2 0x2f8
|
||||
@@ -19,6 +20,137 @@
|
||||
#define SPEED57600 6
|
||||
#define SPEED115200 7
|
||||
|
||||
typedef struct {
|
||||
char devnode[32];
|
||||
speed_t speed;
|
||||
int valid;
|
||||
} serial_port_t;
|
||||
|
||||
serial_port_t process_spcr() {
|
||||
serial_port_t result = {0};
|
||||
char buff[128];
|
||||
int fd;
|
||||
uint64_t address;
|
||||
int currspeed;
|
||||
|
||||
result.valid = 0;
|
||||
|
||||
fd = open("/sys/firmware/acpi/tables/SPCR", O_RDONLY);
|
||||
if (fd < 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (read(fd, buff, 80) < 80) {
|
||||
close(fd);
|
||||
return result;
|
||||
}
|
||||
close(fd);
|
||||
|
||||
if (buff[8] != 2) return result; // revision 2
|
||||
if (buff[36] != 0) return result; // 16550 only
|
||||
if (buff[40] != 1) return result; // IO only
|
||||
|
||||
address = *(uint64_t *)(buff + 44);
|
||||
currspeed = buff[58];
|
||||
|
||||
if (address == COM1) {
|
||||
strncpy(result.devnode, "/dev/ttyS0", sizeof(result.devnode));
|
||||
} else if (address == COM2) {
|
||||
strncpy(result.devnode, "/dev/ttyS1", sizeof(result.devnode));
|
||||
} else if (address == COM3) {
|
||||
strncpy(result.devnode, "/dev/ttyS2", sizeof(result.devnode));
|
||||
} else if (address == COM4) {
|
||||
strncpy(result.devnode, "/dev/ttyS3", sizeof(result.devnode));
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (currspeed == SPEED9600) {
|
||||
result.speed = B9600;
|
||||
} else if (currspeed == SPEED19200) {
|
||||
result.speed = B19200;
|
||||
} else if (currspeed == SPEED57600) {
|
||||
result.speed = B57600;
|
||||
} else if (currspeed == SPEED115200) {
|
||||
result.speed = B115200;
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
|
||||
result.valid = 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
serial_port_t identify_by_sys_vendor() {
|
||||
serial_port_t result = {0};
|
||||
char buff[128];
|
||||
FILE *f;
|
||||
|
||||
f = fopen("/sys/devices/virtual/dmi/id/sys_vendor", "r");
|
||||
if (f) {
|
||||
if (fgets(buff, sizeof(buff), f)) {
|
||||
if (strstr(buff, "Supermicro")) {
|
||||
strncpy(result.devnode, "/dev/ttyS1", sizeof(result.devnode));
|
||||
result.speed = B115200;
|
||||
result.valid = 1;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
serial_port_t search_serial_ports() {
|
||||
serial_port_t result = {0};
|
||||
DIR *dir;
|
||||
struct dirent *entry;
|
||||
int fd;
|
||||
int status;
|
||||
int numfound= 0;
|
||||
int numpossible = 0;
|
||||
|
||||
dir = opendir("/dev");
|
||||
if (!dir) {
|
||||
return result;
|
||||
}
|
||||
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (strncmp(entry->d_name, "ttyS", 4) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char devpath[64];
|
||||
snprintf(devpath, sizeof(devpath), "/dev/%s", entry->d_name);
|
||||
|
||||
fd = open(devpath, O_RDWR | O_NOCTTY | O_NONBLOCK);
|
||||
if (fd < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ioctl(fd, TIOCMGET, &status) == 0) {
|
||||
numpossible++;
|
||||
if (numfound < 1) {
|
||||
strncpy(result.devnode, devpath, sizeof(result.devnode));
|
||||
result.speed = B115200;
|
||||
}
|
||||
if (status & TIOCM_CAR) {
|
||||
strncpy(result.devnode, devpath, sizeof(result.devnode));
|
||||
numfound++;
|
||||
result.speed = B115200;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
close(fd);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
if (numfound == 1 || numpossible == 1) {
|
||||
result.valid = 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
struct termios tty;
|
||||
struct termios tty2;
|
||||
@@ -36,46 +168,38 @@ int main(int argc, char* argv[]) {
|
||||
char* offset;
|
||||
uint64_t address;
|
||||
bufflen = 0;
|
||||
tmpi = open("/sys/firmware/acpi/tables/SPCR", O_RDONLY);
|
||||
if (tmpi < 0) {
|
||||
exit(0);
|
||||
}
|
||||
if (read(tmpi, buff, 80) < 80) {
|
||||
exit(0);
|
||||
}
|
||||
close(tmpi);
|
||||
if (buff[8] != 2) exit(0); //revision 2
|
||||
if (buff[36] != 0) exit(0); //16550 only
|
||||
if (buff[40] != 1) exit(0); //IO only
|
||||
address = *(uint64_t *)(buff + 44);
|
||||
currspeed = buff[58];
|
||||
offset = buff + 10;
|
||||
if (address == COM1) {
|
||||
strncpy(buff, "/dev/ttyS0", 128);
|
||||
} else if (address == COM2) {
|
||||
strncpy(buff, "/dev/ttyS1", 128);
|
||||
} else if (address == COM3) {
|
||||
strncpy(buff, "/dev/ttyS2", 128);
|
||||
} else if (address == COM4) {
|
||||
strncpy(buff, "/dev/ttyS3", 128);
|
||||
} else {
|
||||
#ifndef __x86_64__
|
||||
// Only x86 needs autoconsole, other platforms have reasonable default serial console
|
||||
exit(0);
|
||||
#endif
|
||||
serial_port_t spcr = process_spcr();
|
||||
if (!spcr.valid) {
|
||||
spcr = search_serial_ports();
|
||||
}
|
||||
if (!spcr.valid) {
|
||||
spcr = identify_by_sys_vendor();
|
||||
}
|
||||
if (!spcr.valid) {
|
||||
exit(0);
|
||||
}
|
||||
strncpy(buff, spcr.devnode, sizeof(buff));
|
||||
offset = strchr(buff, 0);
|
||||
currspeed = spcr.speed;
|
||||
ttyf = open(buff, O_RDWR | O_NOCTTY);
|
||||
if (ttyf < 0) {
|
||||
fprintf(stderr, "Unable to open tty\n");
|
||||
exit(1);
|
||||
}
|
||||
if (currspeed == SPEED9600) {
|
||||
if (currspeed == B9600) {
|
||||
cspeed = B9600;
|
||||
strncpy(offset, ",9600", 6);
|
||||
} else if (currspeed == SPEED19200) {
|
||||
} else if (currspeed == B19200) {
|
||||
cspeed = B19200;
|
||||
strncpy(offset, ",19200", 7);
|
||||
} else if (currspeed == SPEED57600) {
|
||||
} else if (currspeed == B57600) {
|
||||
cspeed = B57600;
|
||||
strncpy(offset, ",57600", 7);
|
||||
} else if (currspeed == SPEED115200) {
|
||||
} else if (currspeed == B115200) {
|
||||
cspeed = B115200;
|
||||
strncpy(offset, ",115200", 8);
|
||||
} else {
|
||||
|
||||
@@ -213,97 +213,101 @@ int main(int argc, char* argv[]) {
|
||||
memset(msg, 0, 1024);
|
||||
/* Deny packet access to the last 24 bytes to assure null */
|
||||
recvfrom(n4, msg, 1000, 0, (struct sockaddr *)&dst4, &dst4size);
|
||||
if (nodenameidx = strstr(msg, "NODENAME: ")) {
|
||||
if (strstr(msg, "HTTP/1.1 200 OK")) {
|
||||
if (nodenameidx = strstr(msg, "NODENAME: ")) {
|
||||
nodenameidx += 10;
|
||||
strncpy(nodename, nodenameidx, 1024);
|
||||
nodenameidx = strstr(nodename, "\r");
|
||||
if (nodenameidx) { nodenameidx[0] = 0; }
|
||||
if (strncmp(lastnodename, nodename, 1024) != 0) {
|
||||
printf("NODENAME: %s\n", nodename);
|
||||
strncpy(lastnodename, nodename, 1024);
|
||||
}
|
||||
}
|
||||
if (nodenameidx = strstr(msg, "CURRTIME: ")) {
|
||||
nodenameidx += 10;
|
||||
strncpy(nodename, nodenameidx, 1024);
|
||||
nodenameidx = strstr(nodename, "\r");
|
||||
if (nodenameidx) { nodenameidx[0] = 0; }
|
||||
if (strncmp(lastnodename, nodename, 1024) != 0) {
|
||||
printf("NODENAME: %s\n", nodename);
|
||||
strncpy(lastnodename, nodename, 1024);
|
||||
if (nodenameidx = strstr(nodename, "\r")) {
|
||||
nodenameidx[0] = 0;
|
||||
}
|
||||
}
|
||||
if (nodenameidx = strstr(msg, "CURRTIME: ")) {
|
||||
nodenameidx += 10;
|
||||
strncpy(nodename, nodenameidx, 1024);
|
||||
if (nodenameidx = strstr(nodename, "\r")) {
|
||||
nodenameidx[0] = 0;
|
||||
settime = strtol(nodename, NULL, 10);
|
||||
}
|
||||
settime = strtol(nodename, NULL, 10);
|
||||
}
|
||||
if (nodenameidx = strstr(msg, "CURRMSECS: ")) {
|
||||
nodenameidx += 10;
|
||||
strncpy(nodename, nodenameidx, 1024);
|
||||
if (nodenameidx = strstr(nodename, "\r")) {
|
||||
nodenameidx[0] = 0;
|
||||
if (nodenameidx = strstr(msg, "CURRMSECS: ")) {
|
||||
nodenameidx += 10;
|
||||
strncpy(nodename, nodenameidx, 1024);
|
||||
if (nodenameidx = strstr(nodename, "\r")) {
|
||||
nodenameidx[0] = 0;
|
||||
}
|
||||
setusec = strtol(nodename, NULL, 10) * 1000;
|
||||
}
|
||||
memset(msg, 0, 1024);
|
||||
inet_ntop(dst4.sin_family, &dst4.sin_addr, msg, dst4size);
|
||||
/* Take measure from printing out the same ip twice in a row */
|
||||
if (strncmp(lastmsg, msg, 1024) != 0) {
|
||||
sendto(n4, "PING", 4, 0, (const struct sockaddr *)&dst4, dst4size);
|
||||
printf("MANAGER: %s\n", msg);
|
||||
strncpy(lastmsg, msg, 1024);
|
||||
}
|
||||
setusec = strtol(nodename, NULL, 10) * 1000;
|
||||
}
|
||||
memset(msg, 0, 1024);
|
||||
inet_ntop(dst4.sin_family, &dst4.sin_addr, msg, dst4size);
|
||||
/* Take measure from printing out the same ip twice in a row */
|
||||
if (strncmp(lastmsg, msg, 1024) != 0) {
|
||||
sendto(n4, "PING", 4, 0, (const struct sockaddr *)&dst4, dst4size);
|
||||
printf("MANAGER: %s\n", msg);
|
||||
strncpy(lastmsg, msg, 1024);
|
||||
}
|
||||
}
|
||||
if (FD_ISSET(ns, &rfds)) {
|
||||
memset(msg, 0, 1024);
|
||||
/* Deny packet access to the last 24 bytes to assure null */
|
||||
recvfrom(ns, msg, 1000, 0, (struct sockaddr *)&dst, &dstsize);
|
||||
if (nodenameidx = strstr(msg, "NODENAME: ")) {
|
||||
if (strstr(msg, "HTTP/1.1 200 OK")) {
|
||||
if (nodenameidx = strstr(msg, "NODENAME: ")) {
|
||||
nodenameidx += 10;
|
||||
strncpy(nodename, nodenameidx, 1024);
|
||||
nodenameidx = strstr(nodename, "\r");
|
||||
if (nodenameidx) { nodenameidx[0] = 0; }
|
||||
if (strncmp(lastnodename, nodename, 1024) != 0) {
|
||||
printf("NODENAME: %s\n", nodename);
|
||||
strncpy(lastnodename, nodename, 1024);
|
||||
}
|
||||
}
|
||||
if (nodenameidx = strstr(msg, "CURRTIME: ")) {
|
||||
nodenameidx += 10;
|
||||
strncpy(nodename, nodenameidx, 1024);
|
||||
nodenameidx = strstr(nodename, "\r");
|
||||
if (nodenameidx) { nodenameidx[0] = 0; }
|
||||
if (strncmp(lastnodename, nodename, 1024) != 0) {
|
||||
printf("NODENAME: %s\n", nodename);
|
||||
strncpy(lastnodename, nodename, 1024);
|
||||
if (nodenameidx = strstr(nodename, "\r")) {
|
||||
nodenameidx[0] = 0;
|
||||
}
|
||||
}
|
||||
if (nodenameidx = strstr(msg, "CURRTIME: ")) {
|
||||
nodenameidx += 10;
|
||||
strncpy(nodename, nodenameidx, 1024);
|
||||
if (nodenameidx = strstr(nodename, "\r")) {
|
||||
nodenameidx[0] = 0;
|
||||
settime = strtol(nodename, NULL, 10);
|
||||
}
|
||||
settime = strtol(nodename, NULL, 10);
|
||||
}
|
||||
if (nodenameidx = strstr(msg, "DEFAULTNET: 1")) {
|
||||
isdefault = 1;
|
||||
}
|
||||
if (nodenameidx = strstr(msg, "MGTIFACE: ")) {
|
||||
nodenameidx += 10;
|
||||
strncpy(mgtifname, nodenameidx, 1024);
|
||||
if (nodenameidx = strstr(mgtifname, "\r")) {
|
||||
nodenameidx[0] = 0;
|
||||
if (nodenameidx = strstr(msg, "DEFAULTNET: 1")) {
|
||||
isdefault = 1;
|
||||
}
|
||||
}
|
||||
if (nodenameidx = strstr(msg, "CURRMSECS: ")) {
|
||||
nodenameidx += 10;
|
||||
strncpy(nodename, nodenameidx, 1024);
|
||||
if (nodenameidx = strstr(nodename, "\r")) {
|
||||
nodenameidx[0] = 0;
|
||||
if (nodenameidx = strstr(msg, "MGTIFACE: ")) {
|
||||
nodenameidx += 10;
|
||||
strncpy(mgtifname, nodenameidx, 1024);
|
||||
if (nodenameidx = strstr(mgtifname, "\r")) {
|
||||
nodenameidx[0] = 0;
|
||||
}
|
||||
}
|
||||
setusec = strtol(nodename, NULL, 10) * 1000;
|
||||
}
|
||||
memset(msg, 0, 1024);
|
||||
inet_ntop(dst.sin6_family, &dst.sin6_addr, msg, dstsize);
|
||||
if (strncmp(last6msg, msg, 1024) != 0 || lastidx != dst.sin6_scope_id) {
|
||||
lastidx = dst.sin6_scope_id;
|
||||
sendto(ns, "PING", 4, 0, (const struct sockaddr *)&dst, dstsize);
|
||||
printf("MANAGER: %s", msg);
|
||||
if (strncmp(msg, "fe80::", 6) == 0) {
|
||||
printf("%%%u", dst.sin6_scope_id);
|
||||
if (nodenameidx = strstr(msg, "CURRMSECS: ")) {
|
||||
nodenameidx += 10;
|
||||
strncpy(nodename, nodenameidx, 1024);
|
||||
if (nodenameidx = strstr(nodename, "\r")) {
|
||||
nodenameidx[0] = 0;
|
||||
}
|
||||
setusec = strtol(nodename, NULL, 10) * 1000;
|
||||
}
|
||||
printf("\n");
|
||||
printf("EXTMGRINFO: %s", msg);
|
||||
if (strncmp(msg, "fe80::", 6) == 0) {
|
||||
printf("%%%u", dst.sin6_scope_id);
|
||||
memset(msg, 0, 1024);
|
||||
inet_ntop(dst.sin6_family, &dst.sin6_addr, msg, dstsize);
|
||||
if (strncmp(last6msg, msg, 1024) != 0 || lastidx != dst.sin6_scope_id) {
|
||||
lastidx = dst.sin6_scope_id;
|
||||
sendto(ns, "PING", 4, 0, (const struct sockaddr *)&dst, dstsize);
|
||||
printf("MANAGER: %s", msg);
|
||||
if (strncmp(msg, "fe80::", 6) == 0) {
|
||||
printf("%%%u", dst.sin6_scope_id);
|
||||
}
|
||||
printf("\n");
|
||||
printf("EXTMGRINFO: %s", msg);
|
||||
if (strncmp(msg, "fe80::", 6) == 0) {
|
||||
printf("%%%u", dst.sin6_scope_id);
|
||||
}
|
||||
printf("|%s|%d\n", mgtifname, isdefault);
|
||||
strncpy(last6msg, msg, 1024);
|
||||
}
|
||||
printf("|%s|%d\n", mgtifname, isdefault);
|
||||
strncpy(last6msg, msg, 1024);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,8 +132,8 @@ static int http_read(const char *path, char *buf, size_t size, off_t offset,
|
||||
|
||||
if (strcmp(path, filename) != 0) return -ENOENT;
|
||||
memset(headbuffer, 0, 512);
|
||||
if (offset >= filesize) return 0;
|
||||
if (offset + size - 1 >= filesize) size = filesize - offset - 1;
|
||||
if (offset >= filesize || size == 0) return 0;
|
||||
if (offset + size > filesize) size = filesize - offset;
|
||||
snprintf(headbuffer, 512, "%ld-%ld", offset, offset + size - 1);
|
||||
if (curl_easy_setopt(curl, CURLOPT_RANGE, headbuffer) != CURLE_OK) {
|
||||
fprintf(stderr, "Error setting range\n");
|
||||
|
||||
@@ -49,6 +49,7 @@ def main(args):
|
||||
wiz.add_argument('-p', help='Copy in TFTP contents required for PXE support', action='store_true')
|
||||
wiz.add_argument('-i', help='Interactively prompt for behaviors', action='store_true')
|
||||
wiz.add_argument('-l', help='Set up local management node to allow login from managed nodes', action='store_true')
|
||||
wiz.add_argument('-r', help='Repack site contents if present', action='store_true')
|
||||
osip = sp.add_parser('importcheck', help='Check import of an OS image from an ISO image')
|
||||
osip.add_argument('imagefile', help='File to use for source of importing')
|
||||
osip = sp.add_parser('import', help='Import an OS image from an ISO image')
|
||||
@@ -367,6 +368,8 @@ def initialize(cmdset):
|
||||
rc = initialize_genesis()
|
||||
if rc != 0:
|
||||
sys.exit(rc)
|
||||
if cmdset.r:
|
||||
didsomething = True
|
||||
if not didsomething and (cmdset.k or cmdset.l or cmdset.g or cmdset.p):
|
||||
if cmdset.g:
|
||||
updateboot('genesis-x86_64')
|
||||
|
||||
@@ -94,6 +94,7 @@ _allowedbyrole = {
|
||||
'/node*/power/state',
|
||||
'/node*/sensors/*',
|
||||
'/node*/attributes/current',
|
||||
'/node*/attributes/all',
|
||||
'/node*/description',
|
||||
'/noderange/*/nodes/',
|
||||
'/nodes/',
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import os
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
import confluent.collective.manager as collective
|
||||
import confluent.util as util
|
||||
from os.path import exists
|
||||
import datetime
|
||||
import shutil
|
||||
import socket
|
||||
import eventlet.green.subprocess as subprocess
|
||||
import tempfile
|
||||
try:
|
||||
import cryptography.x509 as x509
|
||||
except ImportError:
|
||||
x509 = None
|
||||
|
||||
def mkdirp(targ):
|
||||
try:
|
||||
@@ -178,6 +189,17 @@ def assure_tls_ca():
|
||||
os.symlink(certname, hashname)
|
||||
finally:
|
||||
os.seteuid(ouid)
|
||||
return certout
|
||||
|
||||
#def is_self_signed(pem):
|
||||
# cert = ssl.PEM_cert_to_DER_cert(pem)
|
||||
# return cert.get('subjectAltName', []) == cert.get('issuer', [])
|
||||
# x509 certificate issuer subject comparison..
|
||||
#>>> b.issuer
|
||||
#<Name(C=US,ST=NC,L=RTP,O=Lenovo,CN=XCC-7D9D-J102MM2T)>
|
||||
#>>> b.subject
|
||||
#<Name(C=US,ST=NC,L=RTP,O=Lenovo,CN=XCC-7D9D-J102MM2T)>
|
||||
|
||||
|
||||
def substitute_cfg(setting, key, val, newval, cfgfile, line):
|
||||
if key.strip() == setting:
|
||||
@@ -222,7 +244,7 @@ def create_full_ca(certout):
|
||||
cfgfile.write(line.strip() + '\n')
|
||||
continue
|
||||
cfgfile.write(line.strip() + '\n')
|
||||
cfgfile.write('\n[CACert]\nbasicConstraints = CA:true\n\n[ca_confluent]\n')
|
||||
cfgfile.write('\n[CACert]\nbasicConstraints = critical,CA:true\nkeyUsage = critical,keyCertSign,cRLSign\n[ca_confluent]\n')
|
||||
subprocess.check_call(
|
||||
['openssl', 'ecparam', '-name', 'secp384r1', '-genkey', '-out',
|
||||
keyout])
|
||||
@@ -231,7 +253,7 @@ def create_full_ca(certout):
|
||||
subprocess.check_call(
|
||||
['openssl', 'ca', '-config', newcfg, '-batch', '-selfsign',
|
||||
'-extensions', 'CACert', '-extfile', newcfg,
|
||||
'-notext', '-startdate',
|
||||
'-notext', '-md', 'sha384', '-startdate',
|
||||
'19700101010101Z', '-enddate', '21000101010101Z', '-keyfile',
|
||||
keyout, '-out', '/etc/confluent/tls/ca/cacert.pem', '-in', csrout]
|
||||
)
|
||||
@@ -257,7 +279,7 @@ def create_simple_ca(keyout, certout):
|
||||
if len(subj) > 68:
|
||||
subj = subj[:68]
|
||||
with open(tmpconfig, 'a') as cfgfile:
|
||||
cfgfile.write('\n[CACert]\nbasicConstraints = CA:true\n')
|
||||
cfgfile.write('\n[CACert]\nbasicConstraints = critical,CA:true\n')
|
||||
subprocess.check_call([
|
||||
'openssl', 'req', '-new', '-x509', '-key', keyout, '-days',
|
||||
'27300', '-out', certout, '-subj', subj,
|
||||
@@ -266,70 +288,112 @@ def create_simple_ca(keyout, certout):
|
||||
finally:
|
||||
os.remove(tmpconfig)
|
||||
|
||||
def create_certificate(keyout=None, certout=None, csrout=None):
|
||||
if not keyout:
|
||||
def create_certificate(keyout=None, certout=None, csrfile=None, subj=None, san=None, backdate=True, days=None):
|
||||
now_utc = datetime.datetime.now(datetime.timezone.utc)
|
||||
if backdate:
|
||||
# To deal with wildly off clocks, we backdate certificates.
|
||||
startdate = '20000101010101Z'
|
||||
else:
|
||||
# apply a mild backdate anyway, even if these are supposed to be for more accurate clocks
|
||||
startdate = (now_utc - datetime.timedelta(hours=24)).strftime('%Y%m%d%H%M%SZ')
|
||||
if days is None:
|
||||
enddate = '21000101010101Z'
|
||||
else:
|
||||
enddate = (now_utc + datetime.timedelta(days=days)).strftime('%Y%m%d%H%M%SZ')
|
||||
tlsmateriallocation = {}
|
||||
if not certout:
|
||||
tlsmateriallocation = get_certificate_paths()
|
||||
keyout = tlsmateriallocation.get('keys', [None])[0]
|
||||
certout = tlsmateriallocation.get('certs', [None])[0]
|
||||
if not certout:
|
||||
certout = tlsmateriallocation.get('bundles', [None])[0]
|
||||
if not keyout or not certout:
|
||||
if (not keyout and not csrfile) or not certout:
|
||||
raise Exception('Unable to locate TLS certificate path automatically')
|
||||
assure_tls_ca()
|
||||
shortname = socket.gethostname().split('.')[0]
|
||||
longname = shortname # socket.getfqdn()
|
||||
if not csrout:
|
||||
cacertname = assure_tls_ca()
|
||||
if not subj:
|
||||
shortname = socket.gethostname().split('.')[0]
|
||||
longname = shortname # socket.getfqdn()
|
||||
subj = '/CN={0}'.format(longname)
|
||||
elif '/CN=' not in subj:
|
||||
subj = '/CN={0}'.format(subj)
|
||||
if not csrfile:
|
||||
subprocess.check_call(
|
||||
['openssl', 'ecparam', '-name', 'secp384r1', '-genkey', '-out',
|
||||
keyout])
|
||||
ipaddrs = list(get_ip_addresses())
|
||||
san = ['IP:{0}'.format(x) for x in ipaddrs]
|
||||
# It is incorrect to put IP addresses as DNS type. However
|
||||
# there exists non-compliant clients that fail with them as IP
|
||||
# san.extend(['DNS:{0}'.format(x) for x in ipaddrs])
|
||||
dnsnames = set(ipaddrs)
|
||||
dnsnames.add(shortname)
|
||||
for currip in ipaddrs:
|
||||
dnsnames.add(socket.getnameinfo((currip, 0), 0)[0])
|
||||
for currname in dnsnames:
|
||||
san.append('DNS:{0}'.format(currname))
|
||||
#san.append('DNS:{0}'.format(longname))
|
||||
san = ','.join(san)
|
||||
permitdomains = []
|
||||
if x509:
|
||||
# check if this CA has name constraints, and avoid violating them
|
||||
with open(cacertname, 'rb') as f:
|
||||
cer = x509.load_pem_x509_certificate(f.read())
|
||||
for extension in cer.extensions:
|
||||
if extension.oid == x509.ExtensionOID.NAME_CONSTRAINTS:
|
||||
nc = extension.value
|
||||
for pname in nc.permitted_subtrees:
|
||||
permitdomains.append(pname.value)
|
||||
if not san:
|
||||
ipaddrs = list(get_ip_addresses())
|
||||
if not permitdomains:
|
||||
san = ['IP:{0}'.format(x) for x in ipaddrs]
|
||||
# It is incorrect to put IP addresses as DNS type. However
|
||||
# there exists non-compliant clients that fail with them as IP
|
||||
# san.extend(['DNS:{0}'.format(x) for x in ipaddrs])
|
||||
dnsnames = set(ipaddrs)
|
||||
dnsnames.add(shortname)
|
||||
dnsnames.add(longname)
|
||||
else:
|
||||
# nameconstraints preclude IP and shortname
|
||||
san = []
|
||||
dnsnames = set()
|
||||
for suffix in permitdomains:
|
||||
if longname.endswith(suffix):
|
||||
dnsnames.add(longname)
|
||||
break
|
||||
for currip in ipaddrs:
|
||||
currname = socket.getnameinfo((currip, 0), 0)[0]
|
||||
for suffix in permitdomains:
|
||||
if currname.endswith(suffix):
|
||||
dnsnames.add(currname)
|
||||
break
|
||||
if not permitdomains:
|
||||
dnsnames.add(currname)
|
||||
for currname in dnsnames:
|
||||
san.append('DNS:{0}'.format(currname))
|
||||
#san.append('DNS:{0}'.format(longname))
|
||||
san = ','.join(san)
|
||||
sslcfg = get_openssl_conf_location()
|
||||
tmphdl, tmpconfig = tempfile.mkstemp()
|
||||
os.close(tmphdl)
|
||||
tmphdl, extconfig = tempfile.mkstemp()
|
||||
os.close(tmphdl)
|
||||
needcsr = False
|
||||
if csrout is None:
|
||||
if csrfile is None:
|
||||
needcsr = True
|
||||
tmphdl, csrout = tempfile.mkstemp()
|
||||
tmphdl, csrfile = tempfile.mkstemp()
|
||||
os.close(tmphdl)
|
||||
shutil.copy2(sslcfg, tmpconfig)
|
||||
try:
|
||||
with open(extconfig, 'a') as cfgfile:
|
||||
cfgfile.write('\nbasicConstraints=critical,CA:false\nkeyUsage=critical,digitalSignature\nextendedKeyUsage=serverAuth,clientAuth\nsubjectAltName={0}'.format(san))
|
||||
if needcsr:
|
||||
with open(tmpconfig, 'a') as cfgfile:
|
||||
cfgfile.write('\n[SAN]\nsubjectAltName={0}'.format(san))
|
||||
with open(extconfig, 'a') as cfgfile:
|
||||
cfgfile.write('\nbasicConstraints=CA:false\nsubjectAltName={0}'.format(san))
|
||||
subprocess.check_call([
|
||||
'openssl', 'req', '-new', '-key', keyout, '-out', csrout, '-subj',
|
||||
'/CN={0}'.format(longname),
|
||||
'-extensions', 'SAN', '-config', tmpconfig
|
||||
'openssl', 'req', '-new', '-key', keyout, '-out', csrfile, '-subj',
|
||||
subj, '-extensions', 'SAN', '-config', tmpconfig
|
||||
])
|
||||
else:
|
||||
# when used manually, allow the csr SAN to stand
|
||||
# may add explicit subj/SAN argument, in which case we would skip copy
|
||||
with open(tmpconfig, 'a') as cfgfile:
|
||||
cfgfile.write('\ncopy_extensions=copy\n')
|
||||
with open(extconfig, 'a') as cfgfile:
|
||||
cfgfile.write('\nbasicConstraints=CA:false\n')
|
||||
#else:
|
||||
# # when used manually, allow the csr SAN to stand
|
||||
# # may add explicit subj/SAN argument, in which case we would skip copy
|
||||
# #with open(tmpconfig, 'a') as cfgfile:
|
||||
# # cfgfile.write('\ncopy_extensions=copy\n')
|
||||
# with open(extconfig, 'a') as cfgfile:
|
||||
# cfgfile.write('\nbasicConstraints=CA:false\n')
|
||||
if os.path.exists('/etc/confluent/tls/cakey.pem'):
|
||||
# simple style CA in effect, make a random serial number and
|
||||
# hope for the best, and accept inability to backdate the cert
|
||||
serialnum = '0x' + ''.join(['{:02x}'.format(x) for x in bytearray(os.urandom(20))])
|
||||
subprocess.check_call([
|
||||
'openssl', 'x509', '-req', '-in', csrout,
|
||||
'openssl', 'x509', '-req', '-in', csrfile,
|
||||
'-CA', '/etc/confluent/tls/cacert.pem',
|
||||
'-CAkey', '/etc/confluent/tls/cakey.pem',
|
||||
'-set_serial', serialnum, '-out', certout, '-days', '27300',
|
||||
@@ -348,12 +412,11 @@ def create_certificate(keyout=None, certout=None, csrout=None):
|
||||
shutil.copy2(cacfgfile, tmpcafile)
|
||||
os.close(tmphdl)
|
||||
cacfgfile = tmpcafile
|
||||
# with realcalock: # if we put it in server, we must lock it
|
||||
subprocess.check_call([
|
||||
'openssl', 'ca', '-config', cacfgfile,
|
||||
'-in', csrout, '-out', certout, '-batch', '-notext',
|
||||
'-startdate', '19700101010101Z', '-enddate', '21000101010101Z',
|
||||
'-extfile', extconfig
|
||||
'openssl', 'ca', '-config', cacfgfile, '-rand_serial',
|
||||
'-in', csrfile, '-out', certout, '-batch', '-notext',
|
||||
'-startdate', startdate, '-enddate', enddate, '-md', 'sha384',
|
||||
'-extfile', extconfig, '-subj', subj
|
||||
])
|
||||
for keycopy in tlsmateriallocation.get('keys', []):
|
||||
if keycopy != keyout:
|
||||
@@ -381,18 +444,43 @@ def create_certificate(keyout=None, certout=None, csrout=None):
|
||||
finally:
|
||||
os.remove(tmpconfig)
|
||||
if needcsr:
|
||||
os.remove(csrout)
|
||||
print(extconfig) # os.remove(extconfig)
|
||||
os.remove(csrfile)
|
||||
os.remove(extconfig)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
import ipaddress
|
||||
outdir = os.getcwd()
|
||||
keyout = os.path.join(outdir, 'key.pem')
|
||||
certout = os.path.join(outdir, sys.argv[2] + 'cert.pem')
|
||||
certout = os.path.join(outdir, 'cert.pem')
|
||||
csrout = None
|
||||
subj, san = (None, None)
|
||||
try:
|
||||
bindex = sys.argv.index('-b')
|
||||
bmcnode = sys.argv.pop(bindex + 1) # Remove bmcnode argument
|
||||
sys.argv.pop(bindex) # Remove -b flag
|
||||
import confluent.config.configmanager as cfm
|
||||
c = cfm.ConfigManager(None)
|
||||
subj, san = util.get_bmc_subject_san(c, bmcnode)
|
||||
except ValueError:
|
||||
bindex = None
|
||||
if subj is None:
|
||||
try:
|
||||
sans = set()
|
||||
sindex = sys.argv.index('-s')
|
||||
subj = sys.argv.pop(sindex + 1) # Remove subject argument
|
||||
sys.argv.pop(sindex) # Remove -s flag
|
||||
try:
|
||||
ipaddress.ip_address(subj)
|
||||
sans.add('IP:{0}'.format(subj))
|
||||
except ValueError:
|
||||
sans.add('DNS:{0}'.format(subj))
|
||||
san = ','.join(sans) if sans else None
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
csrout = sys.argv[1]
|
||||
except IndexError:
|
||||
csrout = None
|
||||
create_certificate(keyout, certout, csrout)
|
||||
create_certificate(keyout, certout, csrout, subj, san, backdate=False, days=3650)
|
||||
|
||||
@@ -716,6 +716,7 @@ def become_leader(connection):
|
||||
if reassimilate is not None:
|
||||
reassimilate.kill()
|
||||
reassimilate = eventlet.spawn(reassimilate_missing)
|
||||
cfm._init_indexes()
|
||||
cfm._ready = True
|
||||
if _assimilate_missing(skipaddr):
|
||||
schedule_rebalance()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
# Copyright 2015-2019 Lenovo
|
||||
# Copyright 2015-2025 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -379,7 +379,7 @@ node = {
|
||||
'the managed node. If not specified, then console '
|
||||
'is disabled. "ipmi" should be specified for most '
|
||||
'systems if console is desired.'),
|
||||
'validvalues': ('ssh', 'ipmi', 'openbmc', 'tsmsol', 'vcenter', 'proxmox'),
|
||||
'validvalues': ('ssh', 'ipmi', 'openbmc', 'megasol', 'tsmsol', 'vcenter', 'proxmox'),
|
||||
},
|
||||
# 'virtualization.host': {
|
||||
# 'description': ('Hypervisor where this node does/should reside'),
|
||||
@@ -408,6 +408,12 @@ node = {
|
||||
'include /<prefixlen> CIDR suffix to indicate subnet length, which is '
|
||||
'autodetected by default where possible.',
|
||||
},
|
||||
'hardwaremanagement.manager_tls_name': {
|
||||
'description': 'A name to use in lieu of the value in hardwaremanagement.manager for '
|
||||
'TLS certificate verification purposes. Some strategies involve a non-IP, '
|
||||
'non-resolvable name, or this can be used to access by IP while using name-based '
|
||||
'validation',
|
||||
},
|
||||
'hardwaremanagement.method': {
|
||||
'description': 'The method used to perform operations such as power '
|
||||
'control, get sensor data, get inventory, and so on. '
|
||||
@@ -444,6 +450,9 @@ node = {
|
||||
#IBM Flex)''',
|
||||
# 'appliesto': ['system'],
|
||||
# },
|
||||
'id.index': {
|
||||
'description': 'Confluent generated numeric index for the node.',
|
||||
},
|
||||
'id.model': {
|
||||
'description': 'The model number of a node. In scenarios where there '
|
||||
'is both a name and a model number, it is generally '
|
||||
@@ -469,17 +478,17 @@ node = {
|
||||
'the discovery process to decide where to place the mac address of a detected PXE nic.',
|
||||
},
|
||||
'net.connection_name': {
|
||||
'description': 'Name to use when specifiying a name for connection and/or interface name for a team. This may be the name of a team interface, '
|
||||
'description': 'Name to use when specifiying a name for connection and/or interface name for a team/bond. This may be the name of a team/bond interface, '
|
||||
'the connection name in network manager for the interface, or may be installed as an altname '
|
||||
'as supported by the respective OS deployment profiles. Default is to accept default name for '
|
||||
'a team consistent with the respective OS, or to use the matching original port name as connection name.'
|
||||
'a team/bond consistent with the respective OS, or to use the matching original port name as connection name.'
|
||||
},
|
||||
'net.interface_names': {
|
||||
'description': 'Interface name or comma delimited list of names to match for this interface. It is generally recommended '
|
||||
'to leave this blank unless needing to set up interfaces that are not on a common subnet with a confluent server, '
|
||||
'as confluent servers provide autodetection for matching the correct network definition to an interface. '
|
||||
'This would be the default name per the deployed OS and can be a comma delimited list to denote members of '
|
||||
'a team or a single interface for VLAN/PKEY connections.'
|
||||
'a team/bond or a single interface for VLAN/PKEY connections.'
|
||||
},
|
||||
'net.mtu': {
|
||||
'description': 'MTU to apply to this connection',
|
||||
@@ -565,7 +574,7 @@ node = {
|
||||
'operating system',
|
||||
},
|
||||
'net.team_mode': {
|
||||
'description': 'Indicates that this interface should be a team and what mode or runner to use when teamed. '
|
||||
'description': 'Indicates that this interface should be a team/bond and what mode or runner to use when teamed or bonded. '
|
||||
'If this covers a deployment interface, one of the member interfaces may be brought up as '
|
||||
'a standalone interface until deployment is complete, as supported by the OS deployment profile. '
|
||||
'To support this scenario, the switch should be set up to allow independent operation of member ports (e.g. lacp bypass mode or fallback mode).',
|
||||
@@ -599,6 +608,10 @@ node = {
|
||||
'description': ('SNMPv1 community string, it is highly recommended to'
|
||||
'step up to SNMPv3'),
|
||||
},
|
||||
'snmp.privacyprotocol': {
|
||||
'description': 'The privacy protocol to use for SNMPv3',
|
||||
'valid_values': ('aes', 'des'),
|
||||
},
|
||||
# 'secret.snmplocalizedkey': {
|
||||
# 'description': ("SNMPv3 key localized to this node's SNMP Engine id"
|
||||
# 'This can be used in lieu of snmppassphrase to avoid'
|
||||
|
||||
@@ -164,6 +164,45 @@ def _mkpath(pathname):
|
||||
raise
|
||||
|
||||
|
||||
def _count_freeindexes(freeindexes):
|
||||
count = 0
|
||||
for idx in freeindexes:
|
||||
if isinstance(idx, list):
|
||||
for subidx in range(idx[0], idx[1] + 1):
|
||||
count += 1
|
||||
else:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def _is_free_index(freeindexes, idx):
|
||||
for freeidx in freeindexes:
|
||||
if isinstance(freeidx, list):
|
||||
if freeidx[0] <= idx <= freeidx[1]:
|
||||
return True
|
||||
else:
|
||||
if freeidx == idx:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _remove_free_index(freeindexes, idx):
|
||||
for i, freeidx in enumerate(freeindexes):
|
||||
if isinstance(freeidx, list):
|
||||
if freeidx[0] <= idx <= freeidx[1]:
|
||||
if freeidx[0] == freeidx[1]:
|
||||
del freeindexes[i]
|
||||
elif freeidx[0] == idx:
|
||||
freeindexes[i][0] += 1
|
||||
elif freeidx[1] == idx:
|
||||
freeindexes[i][1] -= 1
|
||||
else:
|
||||
freeindexes.insert(i + 1, [idx + 1, freeidx[1]])
|
||||
freeindexes[i][1] = idx - 1
|
||||
return
|
||||
else:
|
||||
if freeidx == idx:
|
||||
del freeindexes[i]
|
||||
return
|
||||
|
||||
def _derive_keys(password, salt):
|
||||
#implement our specific combination of pbkdf2 transforms to get at
|
||||
#key. We bump the iterations up because we can afford to
|
||||
@@ -334,7 +373,7 @@ def _rpc_rename_nodes(tenant, renamemap):
|
||||
|
||||
|
||||
def _rpc_rename_nodegroups(tenant, renamemap):
|
||||
ConfigManager(tenant)._true_rename_nodegroups(renamemap)
|
||||
ConfigManager(tenant)._true_rename_groups(renamemap)
|
||||
|
||||
|
||||
def _rpc_set_group_attributes(tenant, attribmap, autocreate):
|
||||
@@ -974,6 +1013,14 @@ def del_collective_member(name):
|
||||
if cfgstreams:
|
||||
exec_on_followers_unconditional('_true_del_collective_member', name)
|
||||
_true_del_collective_member(name)
|
||||
if cfgstreams:
|
||||
_hasquorum = has_quorum()
|
||||
pushes = eventlet.GreenPool()
|
||||
payload = msgpack.packb({'quorum': _hasquorum}, use_bin_type=False)
|
||||
for _ in pushes.starmap(
|
||||
_push_rpc,
|
||||
[(cfgstreams[s]['stream'], payload) for s in cfgstreams]):
|
||||
pass
|
||||
|
||||
def _true_del_collective_member(name, sync=True):
|
||||
global cfgleader
|
||||
@@ -1142,7 +1189,10 @@ class _ExpressionFormat(string.Formatter):
|
||||
# such as 'net.pxe.hwaddr'
|
||||
key = '.' + left.attr + key
|
||||
left = left.value
|
||||
key = left.id + key
|
||||
if isinstance(left, ast.Name):
|
||||
key = left.id + key
|
||||
else:
|
||||
raise ValueError("Invalid AST structure: expected ast.Name at end of attribute chain")
|
||||
if (not key.startswith('custom.') and
|
||||
_get_valid_attrname(key) not in allattributes.node):
|
||||
raise ValueError(
|
||||
@@ -1203,6 +1253,32 @@ class _ExpressionFormat(string.Formatter):
|
||||
return strval[index]
|
||||
elif isinstance(node, ast.Constant):
|
||||
return node.value
|
||||
elif isinstance(node, ast.Call):
|
||||
key = ''
|
||||
baseval = ''
|
||||
if isinstance(node.func, ast.Attribute):
|
||||
fun_name = node.func.attr
|
||||
baseval = self._handle_ast_node(node.func.value)
|
||||
else:
|
||||
raise ValueError("Invalid function call syntax in expression")
|
||||
if fun_name == 'replace':
|
||||
if len(node.args) != 2:
|
||||
raise ValueError("Invalid number of arguments to replace")
|
||||
arg1 = self._handle_ast_node(node.args[0])
|
||||
arg2 = self._handle_ast_node(node.args[1])
|
||||
return baseval.replace(arg1, arg2)
|
||||
elif fun_name == 'upper':
|
||||
return baseval.upper()
|
||||
elif fun_name == 'lower':
|
||||
return baseval.lower()
|
||||
elif fun_name == 'block_number':
|
||||
chunk_size = self._handle_ast_node(node.args[0])
|
||||
return (int(baseval) - 1) // chunk_size + 1
|
||||
elif fun_name == 'block_offset':
|
||||
chunk_size = self._handle_ast_node(node.args[0])
|
||||
return (int(baseval) - 1) % chunk_size + 1
|
||||
else:
|
||||
raise ValueError("Unsupported function in expression")
|
||||
else:
|
||||
raise ValueError("Unrecognized expression syntax")
|
||||
|
||||
@@ -1306,7 +1382,7 @@ class ConfigManager(object):
|
||||
return _cfgstore['main']
|
||||
return _cfgstore['tenant'][self.tenant]
|
||||
|
||||
def __init__(self, tenant, decrypt=False, username=None):
|
||||
def __init__(self, tenant, decrypt=False, username=None, create_tenant=False):
|
||||
self.clientfiles = {}
|
||||
global _cfgstore
|
||||
self.inrestore = False
|
||||
@@ -1328,12 +1404,14 @@ class ConfigManager(object):
|
||||
self._cfgstore['nodes'] = {}
|
||||
self._bg_sync_to_file()
|
||||
return
|
||||
elif 'tenant' not in _cfgstore:
|
||||
elif 'tenant' not in _cfgstore and create_tenant:
|
||||
_cfgstore['tenant'] = {tenant: {}}
|
||||
self._bg_sync_to_file()
|
||||
elif tenant not in _cfgstore['tenant']:
|
||||
elif tenant not in _cfgstore['tenant'] and create_tenant:
|
||||
_cfgstore['tenant'][tenant] = {}
|
||||
self._bg_sync_to_file()
|
||||
elif tenant and tenant not in _cfgstore.get('tenant', {}):
|
||||
raise ValueError("Tenant {0} does not exist".format(tenant))
|
||||
self.tenant = tenant
|
||||
if 'nodegroups' not in self._cfgstore:
|
||||
self._cfgstore['nodegroups'] = {'everything': {}}
|
||||
@@ -1344,6 +1422,9 @@ class ConfigManager(object):
|
||||
self.wait_for_sync()
|
||||
|
||||
def add_client_file(self, clientfile):
|
||||
filename = os.path.normpath(clientfile.filename)
|
||||
if filename.startswith('../') or filename.startswith('..\\'):
|
||||
raise ValueError("Invalid filename: {0}".format(clientfile.filename))
|
||||
self.clientfiles[clientfile.filename] = clientfile.fileobject
|
||||
|
||||
def close_client_files(self):
|
||||
@@ -2206,7 +2287,7 @@ class ConfigManager(object):
|
||||
watcher = self._nodecollwatchers[self.tenant][watcher]
|
||||
watcher(added=(), deleting=nodes, renamed=(), configmanager=self)
|
||||
changeset = {}
|
||||
for node in nodes:
|
||||
for node in confluent.util.natural_sort(nodes):
|
||||
# set a reserved attribute for the sake of the change notification
|
||||
# framework to trigger on
|
||||
changeset[node] = {'_nodedeleted': 1}
|
||||
@@ -2214,6 +2295,29 @@ class ConfigManager(object):
|
||||
if node in self._cfgstore['nodes']:
|
||||
self._sync_groups_to_node(node=node, groups=[],
|
||||
changeset=changeset)
|
||||
nidx = self._cfgstore['nodes'][node].get('id.index', {}).get('value', None)
|
||||
if nidx is not None:
|
||||
currmaxidx = get_global('max_node_index')
|
||||
freeindexes = get_global('free_node_indexes')
|
||||
if not freeindexes:
|
||||
freeindexes = []
|
||||
if nidx == currmaxidx - 1:
|
||||
currmaxidx = currmaxidx - 1
|
||||
while _is_free_index(freeindexes, currmaxidx - 1):
|
||||
_remove_free_index(freeindexes, currmaxidx - 1)
|
||||
currmaxidx = currmaxidx - 1
|
||||
set_global('max_node_index', currmaxidx)
|
||||
else:
|
||||
lastindex = freeindexes[-1] if freeindexes else [-2, -2]
|
||||
if not isinstance(lastindex, list):
|
||||
lastindex = [lastindex, lastindex]
|
||||
if nidx == lastindex[1] + 1:
|
||||
lastindex[1] = nidx
|
||||
if freeindexes:
|
||||
freeindexes[-1] = lastindex
|
||||
else:
|
||||
freeindexes.append(nidx)
|
||||
set_global('free_node_indexes', freeindexes)
|
||||
del self._cfgstore['nodes'][node]
|
||||
_mark_dirtykey('nodes', node, self.tenant)
|
||||
self._notif_attribwatchers(changeset)
|
||||
@@ -2491,12 +2595,29 @@ class ConfigManager(object):
|
||||
attrname, node)
|
||||
raise ValueError(errstr)
|
||||
attribmap[node][attrname] = attrval
|
||||
for node in attribmap:
|
||||
for node in confluent.util.natural_sort(attribmap):
|
||||
node = confluent.util.stringify(node)
|
||||
exprmgr = None
|
||||
if node not in self._cfgstore['nodes']:
|
||||
newnodes.append(node)
|
||||
self._cfgstore['nodes'][node] = {}
|
||||
freeindexes = get_global('free_node_indexes')
|
||||
if not freeindexes:
|
||||
freeindexes = []
|
||||
if _count_freeindexes(freeindexes) > 128: # tend to leave freed indexes disused until a lot have accumulated
|
||||
if isinstance(freeindexes[0], list):
|
||||
nidx = freeindexes[0][0]
|
||||
freeindexes[0][0] = nidx + 1
|
||||
if freeindexes[0][0] == freeindexes[0][1]:
|
||||
freeindexes[0] = freeindexes[0][0]
|
||||
else:
|
||||
nidx = freeindexes.pop(0)
|
||||
set_global('free_node_indexes', freeindexes)
|
||||
else:
|
||||
nidx = get_global('max_node_index')
|
||||
if nidx is None:
|
||||
nidx = 1
|
||||
set_global('max_node_index', nidx + 1)
|
||||
self._cfgstore['nodes'][node] = {'id.index': {'value': nidx}}
|
||||
cfgobj = self._cfgstore['nodes'][node]
|
||||
recalcexpressions = False
|
||||
for attrname in attribmap[node]:
|
||||
@@ -3129,6 +3250,29 @@ def get_globals():
|
||||
bkupglobals[globvar] = _cfgstore['globals'][globvar]
|
||||
return bkupglobals
|
||||
|
||||
def _init_indexes():
|
||||
maxidx = get_global('max_node_index')
|
||||
if maxidx is not None or 'main' not in _cfgstore:
|
||||
return
|
||||
maxidx = 1
|
||||
maincfgstore = _cfgstore['main']
|
||||
nodes_without_index = []
|
||||
for node in confluent.util.natural_sort(maincfgstore.get('nodes', {})):
|
||||
nidx = maincfgstore['nodes'][node].get('id.index', {}).get('value', None)
|
||||
if nidx is not None:
|
||||
if nidx >= maxidx:
|
||||
maxidx = nidx + 1
|
||||
else:
|
||||
nodes_without_index.append(node)
|
||||
for node in nodes_without_index:
|
||||
maincfgstore['nodes'][node]['id.index'] = {'value': maxidx}
|
||||
maxidx += 1
|
||||
_mark_dirtykey('nodes', node, None)
|
||||
set_global('max_node_index', maxidx)
|
||||
set_global('free_node_indexes', [])
|
||||
ConfigManager._bg_sync_to_file()
|
||||
|
||||
|
||||
def init(stateless=False):
|
||||
global _cfgstore
|
||||
global _ready
|
||||
@@ -3141,6 +3285,7 @@ def init(stateless=False):
|
||||
_cfgstore = {}
|
||||
members = list(list_collective())
|
||||
if len(members) < 2:
|
||||
_init_indexes()
|
||||
_ready = True
|
||||
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ def chunk_output(output, n):
|
||||
yield output[i:i + n]
|
||||
|
||||
def get_buffer_output(nodename):
|
||||
if _bufferdaemon is None:
|
||||
eventlet.spawn(run_buffer_daemon)
|
||||
out = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
out.setsockopt(socket.SOL_SOCKET, socket.SO_PASSCRED, 1)
|
||||
out.connect("\x00confluent-vtbuffer")
|
||||
@@ -85,6 +87,8 @@ def get_buffer_output(nodename):
|
||||
def send_output(nodename, output):
|
||||
if not isinstance(nodename, bytes):
|
||||
nodename = nodename.encode('utf8')
|
||||
if _bufferdaemon is None:
|
||||
eventlet.spawn(run_buffer_daemon)
|
||||
out = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
out.setsockopt(socket.SOL_SOCKET, socket.SO_PASSCRED, 1)
|
||||
out.connect("\x00confluent-vtbuffer")
|
||||
@@ -597,14 +601,25 @@ def _start_tenant_sessions(cfm):
|
||||
event=log.Events.stacktrace)
|
||||
cfm.watch_nodecollection(_nodechange)
|
||||
|
||||
running = True
|
||||
|
||||
def run_buffer_daemon():
|
||||
global _bufferdaemon
|
||||
while running:
|
||||
minrestartdeadline = time.time() + 30 # Do not restart more than once every 30 seconds
|
||||
_bufferdaemon = subprocess.Popen(
|
||||
['/opt/confluent/bin/vtbufferd', 'confluent-vtbuffer'], bufsize=0, stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL)
|
||||
_bufferdaemon.wait()
|
||||
# Ensure we do not restart more than once every 30 seconds
|
||||
sleep_time = minrestartdeadline - time.time()
|
||||
if sleep_time > 0:
|
||||
eventlet.sleep(sleep_time)
|
||||
|
||||
def initialize():
|
||||
global _tracelog
|
||||
global _bufferdaemon
|
||||
_tracelog = log.Logger('trace')
|
||||
_bufferdaemon = subprocess.Popen(
|
||||
['/opt/confluent/bin/vtbufferd', 'confluent-vtbuffer'], bufsize=0, stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL)
|
||||
|
||||
def start_console_sessions():
|
||||
configmodule.hook_new_configmanagers(_start_tenant_sessions)
|
||||
|
||||
@@ -76,7 +76,7 @@ import shutil
|
||||
|
||||
vinz = None
|
||||
pluginmap = {}
|
||||
dispatch_plugins = (b'ipmi', u'ipmi', b'redfish', u'redfish', b'tsmsol', u'tsmsol', b'geist', u'geist', b'deltapdu', u'deltapdu', b'eatonpdu', u'eatonpdu', b'affluent', u'affluent', b'cnos', u'cnos', b'enos', u'enos')
|
||||
dispatch_plugins = (b'remoteconfig', b'ipmi', u'ipmi', b'redfish', u'redfish', b'tsmsol', u'tsmsol', b'geist', u'geist', b'deltapdu', u'deltapdu', b'eatonpdu', u'eatonpdu', b'affluent', u'affluent', b'cnos', u'cnos', b'enos', u'enos')
|
||||
|
||||
PluginCollection = plugin.PluginCollection
|
||||
|
||||
@@ -300,6 +300,20 @@ def _init_core():
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
'certificate': {
|
||||
'sign': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'generate_csr': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'install': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
'certificate_authorities': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
@@ -462,7 +476,15 @@ def _init_core():
|
||||
}),
|
||||
'ident_image': PluginRoute({
|
||||
'handler': 'identimage'
|
||||
})
|
||||
}),
|
||||
'remote_config': {
|
||||
'run': PluginRoute({
|
||||
'handler': 'remoteconfig'
|
||||
}),
|
||||
'active': PluginCollection({
|
||||
'handler': 'remoteconfig'
|
||||
}),
|
||||
},
|
||||
},
|
||||
'events': {
|
||||
'hardware': {
|
||||
@@ -1368,17 +1390,16 @@ def handle_staging(pathcomponents, operation, configmanager, inputdata):
|
||||
'''
|
||||
if operation == 'create':
|
||||
if len(pathcomponents) == 1:
|
||||
stage = Staging(inputdata['user'],str(uuid.uuid1()))
|
||||
if stage.create_directory():
|
||||
if 'filename' in inputdata:
|
||||
data_file = stage.storage_folder + '/filename.txt'
|
||||
with open(data_file, 'w') as f:
|
||||
f.write(inputdata['filename'])
|
||||
else:
|
||||
raise Exception('Error: Missing filename arg')
|
||||
push_url = stage.get_push_url()
|
||||
yield msg.CreatedResource(push_url)
|
||||
|
||||
if 'filename' not in inputdata:
|
||||
raise Exception('Error: Missing filename parameter')
|
||||
inputdata['filename'] = os.path.normpath(inputdata['filename'])
|
||||
if '/' in inputdata['filename'] or '\\' in inputdata['filename']:
|
||||
raise Exception('Error: Invalid filename parameter, must not contain path separators')
|
||||
stage = Staging(inputdata['user'],str(uuid.uuid4()))
|
||||
if stage.create_directory():
|
||||
data_file = stage.storage_folder + '/filename.txt'
|
||||
push_url = stage.get_push_url()
|
||||
yield msg.CreatedResource(push_url)
|
||||
elif len(pathcomponents) == 3:
|
||||
stage = Staging(pathcomponents[1], pathcomponents[2])
|
||||
file = stage.get_file_name()
|
||||
|
||||
@@ -98,6 +98,7 @@ import eventlet
|
||||
import eventlet.greenpool
|
||||
import eventlet.semaphore
|
||||
|
||||
|
||||
autosensors = set()
|
||||
scanner = None
|
||||
|
||||
@@ -106,6 +107,11 @@ try:
|
||||
except NameError:
|
||||
unicode = str
|
||||
|
||||
try:
|
||||
import cryptography.x509.verification as verification
|
||||
except ImportError:
|
||||
verification = None
|
||||
|
||||
class nesteddict(dict):
|
||||
|
||||
def __missing__(self, key):
|
||||
@@ -1059,8 +1065,12 @@ def get_nodename_sysdisco(cfg, handler, info):
|
||||
return currnode
|
||||
else:
|
||||
baynum = info['bay']
|
||||
nl = cfg.filter_node_attributes('enclosure.manager=' + currnode)
|
||||
nl = list(cfg.filter_node_attributes('enclosure.bay={0}'.format(baynum), nl))
|
||||
alphabaynum = '{}{}'.format((int(baynum) + 1) // 2, 'ab'[(int(baynum) - 1) % 2])
|
||||
onl = cfg.filter_node_attributes('enclosure.manager=' + currnode)
|
||||
nl = list(cfg.filter_node_attributes('enclosure.bay={}'.format(baynum), onl))
|
||||
if len(nl) == 1:
|
||||
return nl[0]
|
||||
nl = list(cfg.filter_node_attributes('enclosure.bay={}'.format(alphabaynum), onl))
|
||||
if len(nl) == 1:
|
||||
return nl[0]
|
||||
|
||||
@@ -1182,14 +1192,21 @@ def get_nodename_from_enclosures(cfg, info):
|
||||
encl = nodes_by_uuid[cuuid]
|
||||
bay = info.get('enclosure.bay', None)
|
||||
if bay:
|
||||
tnl = cfg.filter_node_attributes('enclosure.manager=' + encl)
|
||||
otnl = cfg.filter_node_attributes('enclosure.manager=' + encl)
|
||||
tnl = list(
|
||||
cfg.filter_node_attributes('enclosure.bay={0}'.format(bay),
|
||||
tnl))
|
||||
cfg.filter_node_attributes('enclosure.bay={}'.format(bay), otnl))
|
||||
if len(tnl) == 1:
|
||||
# This is not a secure assurance, because it's by
|
||||
# uuid instead of a key
|
||||
nodename = tnl[0]
|
||||
else:
|
||||
alphabay = '{}{}'.format((int(bay) + 1) // 2, 'ab'[(int(bay) - 1) % 2])
|
||||
tnl = list(
|
||||
cfg.filter_node_attributes('enclosure.bay={}'.format(alphabay), otnl))
|
||||
if len(tnl) == 1:
|
||||
# Fallback alpha-bay mapping resolved to a single node
|
||||
nodename = tnl[0]
|
||||
|
||||
return nodename
|
||||
|
||||
|
||||
@@ -1215,10 +1232,15 @@ def search_smms_by_cert(currsmm, cert, cfg):
|
||||
port = neigh.get('port', None)
|
||||
if port is not None:
|
||||
bay = port + 1
|
||||
nl = list(
|
||||
onl = list(
|
||||
cfg.filter_node_attributes('enclosure.manager=' + currsmm))
|
||||
nl = list(
|
||||
cfg.filter_node_attributes('enclosure.bay={}'.format(bay), nl))
|
||||
cfg.filter_node_attributes('enclosure.bay={}'.format(bay), onl))
|
||||
if len(nl) == 1:
|
||||
return currsmm, bay, nl[0]
|
||||
alphabay = '{}{}'.format((bay + 1) // 2, 'ab'[(bay - 1) % 2])
|
||||
nl = list(
|
||||
cfg.filter_node_attributes('enclosure.bay={}'.format(alphabay), onl))
|
||||
if len(nl) == 1:
|
||||
return currsmm, bay, nl[0]
|
||||
return currsmm, bay, None
|
||||
@@ -1311,8 +1333,15 @@ def eval_node(cfg, handler, info, nodename, manual=False):
|
||||
return
|
||||
# search for nodes fitting our description using filters
|
||||
# lead with the most specific to have a small second pass
|
||||
nl = list(cfg.filter_node_attributes(
|
||||
'enclosure.bay={0}'.format(info['enclosure.bay']), nl))
|
||||
baynum = info.get('enclosure.bay', None)
|
||||
if baynum:
|
||||
nnl = list(cfg.filter_node_attributes(
|
||||
'enclosure.bay={}'.format(baynum), nl))
|
||||
if len(nnl) == 0:
|
||||
alphabaynum = '{}{}'.format((int(baynum) + 1) // 2, 'ab'[(int(baynum) - 1) % 2])
|
||||
nnl = list(cfg.filter_node_attributes(
|
||||
'enclosure.bay={}'.format(alphabaynum), nl))
|
||||
nl = nnl
|
||||
if len(nl) != 1:
|
||||
info['discofailure'] = 'ambigconfig'
|
||||
if len(nl):
|
||||
@@ -1323,7 +1352,7 @@ def eval_node(cfg, handler, info, nodename, manual=False):
|
||||
errorstr = 'The {0} in enclosure {1} bay {2} does not ' \
|
||||
'seem to be a defined node ({3})'.format(
|
||||
handler.devname, nodename,
|
||||
info['enclosure.bay'],
|
||||
baynum,
|
||||
handler.ipaddr,
|
||||
)
|
||||
if manual:
|
||||
@@ -1472,7 +1501,7 @@ def discover_node(cfg, handler, info, nodename, manual):
|
||||
break
|
||||
log.log({'info': 'Discovered {0} ({1})'.format(nodename,
|
||||
handler.devname)})
|
||||
if nodeconfig:
|
||||
if nodeconfig or handler.current_cert_self_signed():
|
||||
bmcaddr = cfg.get_node_attributes(nodename, 'hardwaremanagement.manager')
|
||||
bmcaddr = bmcaddr.get(nodename, {}).get('hardwaremanagement.manager', {}).get('value', '')
|
||||
if not bmcaddr:
|
||||
@@ -1481,9 +1510,12 @@ def discover_node(cfg, handler, info, nodename, manual):
|
||||
bmcaddr = bmcaddr.split('/', 1)[0]
|
||||
wait_for_connection(bmcaddr)
|
||||
socket.getaddrinfo(bmcaddr, 443)
|
||||
if nodeconfig:
|
||||
subprocess.check_call(['/opt/confluent/bin/nodeconfig', nodename] + nodeconfig)
|
||||
log.log({'info': 'Configured {0} ({1})'.format(nodename,
|
||||
handler.devname)})
|
||||
if verification and handler.current_cert_self_signed():
|
||||
handler.autosign_certificate()
|
||||
|
||||
info['discostatus'] = 'discovered'
|
||||
for i in pending_by_uuid.get(curruuid, []):
|
||||
|
||||
@@ -17,6 +17,10 @@ import errno
|
||||
import eventlet
|
||||
import socket
|
||||
webclient = eventlet.import_patched('pyghmi.util.webclient')
|
||||
try:
|
||||
import cryptography.x509 as x509
|
||||
except ImportError:
|
||||
x509 = None
|
||||
|
||||
class NodeHandler(object):
|
||||
https_supported = True
|
||||
@@ -59,6 +63,40 @@ class NodeHandler(object):
|
||||
# may occur against the target in a short while
|
||||
return True
|
||||
|
||||
def current_cert_self_signed(self):
|
||||
if not x509:
|
||||
return
|
||||
if not self._ipaddr:
|
||||
return
|
||||
try:
|
||||
wc = webclient.SecureHTTPConnection(self._ipaddr, verifycallback=self._savecert, port=443)
|
||||
wc.connect()
|
||||
wc.close()
|
||||
if not self._fp:
|
||||
return False
|
||||
# Check if certificate is self-signed by comparing issuer and subject
|
||||
cert = self._fp
|
||||
certobj = x509.load_der_x509_certificate(cert)
|
||||
skid = None
|
||||
akid = None
|
||||
for ext in certobj.extensions:
|
||||
if ext.oid == x509.ExtensionOID.SUBJECT_KEY_IDENTIFIER:
|
||||
skid = ext.value
|
||||
elif ext.oid == x509.ExtensionOID.AUTHORITY_KEY_IDENTIFIER:
|
||||
akid = ext.value
|
||||
if akid:
|
||||
if skid.digest == akid.key_identifier:
|
||||
return True
|
||||
elif certobj.issuer == certobj.subject:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
def autosign_certificate(self):
|
||||
# A no-op by default
|
||||
return
|
||||
|
||||
def scan(self):
|
||||
# Do completely passive things to enhance data.
|
||||
# Probe is permitted to for example attempt a login
|
||||
|
||||
@@ -23,6 +23,7 @@ try:
|
||||
from urllib import urlencode
|
||||
except ImportError:
|
||||
from urllib.parse import urlencode
|
||||
import eventlet.green.subprocess as subprocess
|
||||
|
||||
getaddrinfo = eventlet.support.greendns.getaddrinfo
|
||||
|
||||
@@ -326,6 +327,14 @@ class NodeHandler(generic.NodeHandler):
|
||||
raise exc.TargetEndpointUnreachable(
|
||||
'hardwaremanagement.manager must be set to desired address (No IPv6 Link Local detected)')
|
||||
|
||||
def autosign_certificate(self):
|
||||
nodename = self.nodename
|
||||
hwmgt_method = self.configmanager.get_node_attributes(
|
||||
nodename, 'hardwaremanagement.method').get(
|
||||
nodename, {}).get('hardwaremanagement.method', {}).get('value', 'ipmi')
|
||||
if hwmgt_method != 'redfish':
|
||||
return
|
||||
subprocess.check_call(['/opt/confluent/bin/nodecertutil', nodename, 'signbmccert', '--days', '47'])
|
||||
|
||||
def remote_nodecfg(nodename, cfm):
|
||||
cfg = cfm.get_node_attributes(
|
||||
|
||||
@@ -29,6 +29,7 @@ import eventlet.green.socket as socket
|
||||
webclient = eventlet.import_patched('pyghmi.util.webclient')
|
||||
import struct
|
||||
getaddrinfo = eventlet.support.greendns.getaddrinfo
|
||||
import eventlet.green.subprocess as subprocess
|
||||
|
||||
|
||||
def fixuuid(baduuid):
|
||||
@@ -704,6 +705,15 @@ class NodeHandler(immhandler.NodeHandler):
|
||||
if em:
|
||||
self.configmanager.set_node_attributes(
|
||||
{em: {'id.uuid': enclosureuuid}})
|
||||
def autosign_certificate(self):
|
||||
nodename = self.nodename
|
||||
hwmgt_method = self.configmanager.get_node_attributes(
|
||||
nodename, 'hardwaremanagement.method').get(
|
||||
nodename, {}).get('hardwaremanagement.method', {}).get('value', 'ipmi')
|
||||
if hwmgt_method != 'redfish':
|
||||
return
|
||||
subprocess.check_call(['/opt/confluent/bin/nodecertutil', nodename, 'signbmccert', '--days', '47'])
|
||||
|
||||
|
||||
def remote_nodecfg(nodename, cfm):
|
||||
cfg = cfm.get_node_attributes(
|
||||
|
||||
@@ -174,6 +174,7 @@ pxearchs = {
|
||||
b'\x00\x09': 'uefi-x64',
|
||||
b'\x00\x0b': 'uefi-aarch64',
|
||||
b'\x00\x10': 'uefi-httpboot',
|
||||
b'\x00\x13': 'uefi-httpboot', # arm httpboot
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -441,7 +441,7 @@ def _find_service(service, target):
|
||||
continue
|
||||
else:
|
||||
for targurl in peerdata[nid]['urls']:
|
||||
if '/eth' in targurl and targurl.endswith('.xml'):
|
||||
if targurl and targurl.endswith('.xml'):
|
||||
pooltargs.append(('/redfish/v1/', peerdata[nid], 'megarac-bmc'))
|
||||
# For now, don't interrogate generic redfish bmcs
|
||||
# This is due to a need to deduplicate from some supported SLP
|
||||
@@ -491,7 +491,7 @@ def check_fish(urldata, port=443, verifycallback=None):
|
||||
data['services'] = ['lenovo-xcc'] if 'xcc-variant' not in peerinfo else ['lenovo-xcc' + peerinfo['xcc-variant']]
|
||||
return data
|
||||
except (IndexError, KeyError):
|
||||
if 'type' in peerinfo and peerinfo['type'].lower() == 'lenovo-smm3':
|
||||
if 'type' in peerinfo and peerinfo['type'].lower() in ('lenovo-smm3', 'smm3'):
|
||||
del peerinfo['xcc-variant']
|
||||
data['uuid'] = peerinfo['enclosure-uuid']
|
||||
data['services'] = ['lenovo-smm3']
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# This SCGI server provides a http wrap to confluent api
|
||||
# It additionally manages httprequest console sessions
|
||||
import base64
|
||||
import shutil
|
||||
try:
|
||||
import Cookie
|
||||
except ModuleNotFoundError:
|
||||
@@ -360,11 +361,12 @@ def _authorize_request(env, operation, reqbody):
|
||||
return {'code': 401}
|
||||
sessid = _establish_http_session(env, authdata, name, cookie)
|
||||
if authdata and element and element.startswith('/sessions/current/webauthn/validate/'):
|
||||
if webauthn:
|
||||
for rsp in webauthn.handle_api_request(element, env, None, authdata[2], authdata[1], None, reqbody, None):
|
||||
if rsp['verified']:
|
||||
sessid = _establish_http_session(env, authdata, name, cookie)
|
||||
break
|
||||
if not webauthn:
|
||||
return {'code': 501}
|
||||
for rsp in webauthn.handle_api_request(element, env, None, authdata[2], authdata[1], None, reqbody, None):
|
||||
if rsp['verified']:
|
||||
sessid = _establish_http_session(env, authdata, name, cookie)
|
||||
break
|
||||
skiplog = _should_skip_authlog(env)
|
||||
if authdata:
|
||||
auditmsg = {
|
||||
@@ -846,7 +848,7 @@ def resourcehandler_backend(env, start_response):
|
||||
yield 'Our princess is in another castle!'
|
||||
return
|
||||
elif (operation == 'create' and ('/console/session' in env['PATH_INFO'] or
|
||||
'/shell/sessions/' in env['PATH_INFO'])):
|
||||
'/shell/sessions/' in env['PATH_INFO']) and env['PATH_INFO'].startswith(('/nodes/', '/noderange/'))):
|
||||
#hard bake JSON into this path, do not support other incarnations
|
||||
if '/console/session' in env['PATH_INFO']:
|
||||
prefix, _, _ = env['PATH_INFO'].partition('/console/session')
|
||||
@@ -984,9 +986,7 @@ def resourcehandler_backend(env, start_response):
|
||||
start_response('200 OK', headers)
|
||||
yield rsp
|
||||
return
|
||||
|
||||
|
||||
elif (operation == 'create' and ('/firmware/updates/active' in env['PATH_INFO'])):
|
||||
elif (operation == 'create' and ('/firmware/updates/active' in env['PATH_INFO']) and env['PATH_INFO'].startswith(('/nodes/', '/noderange/'))):
|
||||
url = env['PATH_INFO']
|
||||
if 'application/json' in reqtype:
|
||||
if not isinstance(reqbody, str):
|
||||
@@ -1007,8 +1007,7 @@ def resourcehandler_backend(env, start_response):
|
||||
yield json.dumps({'data': nodeurls})
|
||||
start_response('200 OK', headers)
|
||||
return
|
||||
|
||||
elif (operation == 'create' and ('/staging' in env['PATH_INFO'])):
|
||||
elif (operation == 'create' and (env['PATH_INFO'].startswith('/staging'))):
|
||||
url = env['PATH_INFO']
|
||||
args_dict = {}
|
||||
content_length = int(env.get('CONTENT_LENGTH', 0))
|
||||
@@ -1217,7 +1216,7 @@ def _assemble_json(responses, resource=None, url=None, extension=None):
|
||||
rspdata, sort_keys=True, indent=4, ensure_ascii=False).encode('utf-8'))
|
||||
|
||||
|
||||
def serve(bind_host, bind_port):
|
||||
def serve(bind_host, bind_port, bind_group=None, bind_perms=None):
|
||||
# TODO(jbjohnso): move to unix socket and explore
|
||||
# either making apache deal with it
|
||||
# or just supporting nginx or lighthttpd
|
||||
@@ -1232,13 +1231,17 @@ def serve(bind_host, bind_port):
|
||||
while not sock:
|
||||
try:
|
||||
if '/' in bind_host:
|
||||
oldumask = os.umask(0o777 - bind_perms)
|
||||
try:
|
||||
os.remove(bind_host)
|
||||
except Exception:
|
||||
pass
|
||||
sock = eventlet.listen(
|
||||
bind_host, family=socket.AF_UNIX)
|
||||
os.chmod(bind_host, 0o666)
|
||||
os.umask(oldumask)
|
||||
os.chmod(bind_host, bind_perms)
|
||||
if bind_group:
|
||||
shutil.chown(bind_host, group=bind_group)
|
||||
else:
|
||||
addrinfo = socket.getaddrinfo(bind_host, bind_port)[0]
|
||||
sock = eventlet.listen(
|
||||
@@ -1264,17 +1267,22 @@ def serve(bind_host, bind_port):
|
||||
|
||||
|
||||
class HttpApi(object):
|
||||
def __init__(self, bind_host=None, bind_port=None):
|
||||
def __init__(self, bind_host=None, bind_port=None, bind_group=None, bind_perms=None):
|
||||
self.server = None
|
||||
self.bind_host = bind_host or '127.0.0.1'
|
||||
self.bind_port = bind_port or 4005
|
||||
# Ultimately, a unix socket is being used in lieu of a TCP socket,
|
||||
# so open permissions make sense as the security is not based solely on socket access
|
||||
# however, steering it to webserver group can be done for extra confidence
|
||||
self.bind_group = bind_group
|
||||
self.bind_perms = bind_perms or 0o666
|
||||
|
||||
def start(self):
|
||||
global auditlog
|
||||
global tracelog
|
||||
tracelog = log.Logger('trace')
|
||||
auditlog = log.Logger('audit')
|
||||
self.server = eventlet.spawn(serve, self.bind_host, self.bind_port)
|
||||
self.server = eventlet.spawn(serve, self.bind_host, self.bind_port, self.bind_group, self.bind_perms)
|
||||
|
||||
|
||||
_cleaner = eventlet.spawn(_sessioncleaner)
|
||||
|
||||
@@ -314,14 +314,14 @@ def run(args):
|
||||
auth.check_for_yaml()
|
||||
collective.startup()
|
||||
consoleserver.initialize()
|
||||
http_bind_host, http_bind_port = _get_connector_config('http')
|
||||
sock_bind_host, sock_bind_port = _get_connector_config('socket')
|
||||
http_bind_host, http_bind_port, http_bind_group, http_bind_perms = _get_connector_config('http')
|
||||
sock_bind_host, sock_bind_port, sock_bind_group, sock_bind_perms = _get_connector_config('socket')
|
||||
try:
|
||||
sockservice = sockapi.SockApi(sock_bind_host, sock_bind_port)
|
||||
sockservice = sockapi.SockApi(sock_bind_host, sock_bind_port, sock_bind_group, sock_bind_perms)
|
||||
sockservice.start()
|
||||
except NameError:
|
||||
pass
|
||||
webservice = httpapi.HttpApi(http_bind_host, http_bind_port)
|
||||
webservice = httpapi.HttpApi(http_bind_host, http_bind_port, http_bind_group, http_bind_perms)
|
||||
webservice.start()
|
||||
while len(list(configmanager.list_collective())) >= 2:
|
||||
# If in a collective, stall automatic startup activity
|
||||
@@ -340,7 +340,24 @@ def run(args):
|
||||
def _get_connector_config(session):
|
||||
host = conf.get_option(session, 'bindhost')
|
||||
port = conf.get_int_option(session, 'bindport')
|
||||
return (host, port)
|
||||
group = conf.get_option(session, 'bindgroup')
|
||||
perms = conf.get_option(session, 'bindperms')
|
||||
if perms:
|
||||
if perms.startswith('0'):
|
||||
perms = int(perms, 8)
|
||||
else:
|
||||
# Parse rw-rw-rw- format (user, group, other)
|
||||
perms_value = 0
|
||||
perm_map = {'r': 4, 'w': 2, 'x': 1}
|
||||
for i, section in enumerate([perms[0:3], perms[3:6], perms[6:9]]):
|
||||
for char in section:
|
||||
if char in perm_map:
|
||||
perms_value += perm_map[char] * (8 ** (2 - i))
|
||||
perms = perms_value
|
||||
else:
|
||||
perms = None
|
||||
|
||||
return (host, port, group, perms)
|
||||
|
||||
def _get_logdirectory():
|
||||
return conf.get_option('globals', 'logdirectory')
|
||||
@@ -19,6 +19,7 @@
|
||||
# Things are defined here to 'encourage' developers to coordinate information
|
||||
# format. This is also how different data formats are supported
|
||||
import base64
|
||||
import os
|
||||
import confluent.exceptions as exc
|
||||
import confluent.config.configmanager as cfm
|
||||
import confluent.config.conf as cfgfile
|
||||
@@ -27,6 +28,7 @@ from datetime import datetime
|
||||
import confluent.util as util
|
||||
import msgpack
|
||||
import json
|
||||
import pwd
|
||||
|
||||
try:
|
||||
unicode
|
||||
@@ -519,6 +521,8 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
|
||||
return InputAlertDestination(path, nodes, inputdata, multinode)
|
||||
elif len(path) == 3 and path[:3] == ['configuration', 'management_controller', 'certificate_authorities'] and operation not in ('retrieve', 'delete'):
|
||||
return InputCertificateAuthority(path, nodes, inputdata)
|
||||
elif len(path) == 4 and path[:4] == ['configuration', 'management_controller', 'certificate', 'sign'] and operation not in ('retrieve', 'delete'):
|
||||
return InputSigningParameters(path, inputdata, nodes, configmanager)
|
||||
elif path == ['identify'] and operation != 'retrieve':
|
||||
return InputIdentifyMessage(path, nodes, inputdata)
|
||||
elif path == ['events', 'hardware', 'decode']:
|
||||
@@ -567,6 +571,8 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
|
||||
return InputPowerMessage(path, nodes, inputdata)
|
||||
elif '/'.join(path).startswith('media/detach'):
|
||||
return DetachMedia(path, nodes, inputdata)
|
||||
elif '/'.join(path).startswith('media/attach') and inputdata:
|
||||
return InputMediaUrl(path, nodes, inputdata, configmanager)
|
||||
elif '/'.join(path).startswith('media/') and inputdata:
|
||||
return InputMedia(path, nodes, inputdata, configmanager)
|
||||
elif '/'.join(path).startswith('support/servicedata') and inputdata:
|
||||
@@ -578,6 +584,8 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
|
||||
return InputLicense(path, nodes, inputdata, configmanager)
|
||||
elif path == ['deployment', 'lock'] and inputdata:
|
||||
return InputDeploymentLock(path, nodes, inputdata)
|
||||
elif path == ['deployment', 'remote_config', 'run'] and inputdata:
|
||||
return InputRemoteConfig(path, nodes, inputdata)
|
||||
elif path == ['deployment', 'ident_image']:
|
||||
return InputIdentImage(path, nodes, inputdata)
|
||||
elif path == ['console', 'ikvm']:
|
||||
@@ -586,19 +594,75 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
|
||||
raise exc.InvalidArgumentException(
|
||||
'No known input handler for request')
|
||||
|
||||
class InputFirmwareUpdate(ConfluentMessage):
|
||||
|
||||
def checkaccess(user, filename, pwent):
|
||||
"""Check if a user has read access to a file.
|
||||
|
||||
This function checks if the specified user has read access to the given
|
||||
filename. It returns True if the user has read access, and False otherwise.
|
||||
"""
|
||||
child = os.fork()
|
||||
if child == 0:
|
||||
os.setgroups(os.getgrouplist(user, pwent.pw_gid))
|
||||
os.setgid(pwent.pw_gid)
|
||||
os.setuid(pwent.pw_uid)
|
||||
if os.access(filename, os.R_OK):
|
||||
os._exit(0)
|
||||
os._exit(1)
|
||||
else:
|
||||
pid, status = os.waitpid(child, 0)
|
||||
if os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
def isurl(value):
|
||||
prefix, value = value.split('://', 1) if '://' in value else ('', value)
|
||||
if '/' in prefix:
|
||||
return False
|
||||
return True if prefix else False
|
||||
|
||||
class InputFirmwareUpdate(ConfluentMessage):
|
||||
urlsupported = False
|
||||
def __init__(self, path, nodes, inputdata, configmanager):
|
||||
self._filename = inputdata.get('filename', inputdata.get('url', inputdata.get('dirname', None)))
|
||||
self.bank = inputdata.get('bank', None)
|
||||
self.parameterdata = inputdata.get('parameterdata', None)
|
||||
self.nodes = nodes
|
||||
self.filebynode = {}
|
||||
self._complexname = False
|
||||
curruser = configmanager.current_user if configmanager else None
|
||||
# for configmanager filehandles, those are already opened by client, so
|
||||
# no need to check server side access
|
||||
checkedfiles = set(list(configmanager.clientfiles))
|
||||
for expanded in configmanager.expand_attrib_expression(
|
||||
nodes, self._filename):
|
||||
node, value = expanded
|
||||
if value != self._filename:
|
||||
self._complexname = True
|
||||
if self.urlsupported and isurl(value):
|
||||
self.filebynode[node] = value
|
||||
continue
|
||||
value = os.path.normpath(value)
|
||||
if value not in checkedfiles:
|
||||
if value.startswith('../'):
|
||||
raise Exception('File transfer with ../ is not supported')
|
||||
if value.startswith('/etc/confluent'):
|
||||
raise Exception(
|
||||
'File transfer with /etc/confluent is not supported')
|
||||
if value.startswith('/var/log/confluent'):
|
||||
raise Exception(
|
||||
'File transfer with /var/log/confluent is not supported')
|
||||
if curruser and not value.startswith('/var/lib/confluent/client_assets/'):
|
||||
try:
|
||||
pwent = pwd.getpwnam(curruser)
|
||||
if not checkaccess(curruser, value, pwent):
|
||||
errstr = '{0} is not readable by {1}, check the file and parent directory ownership and permissions'.format(
|
||||
value, curruser)
|
||||
raise Exception(errstr)
|
||||
except KeyError:
|
||||
pass # We can't check ownership for confluent users without system users, as is the case in a prominent container usage,
|
||||
# We must rely upon the banned paths to mitigate risk instead
|
||||
checkedfiles.add(value)
|
||||
self.filebynode[node] = value
|
||||
|
||||
@property
|
||||
@@ -628,6 +692,11 @@ class InputMedia(InputFirmwareUpdate):
|
||||
# Use InputFirmwareUpdate
|
||||
pass
|
||||
|
||||
class InputMediaUrl(InputFirmwareUpdate):
|
||||
# Use InputFirmwareUpdate for URL-based media attachment
|
||||
urlsupported = True
|
||||
pass
|
||||
|
||||
class InputLicense(InputFirmwareUpdate):
|
||||
pass
|
||||
|
||||
@@ -721,6 +790,9 @@ class InputConfigChangeSet(InputExpression):
|
||||
endattrs = {}
|
||||
for attr in attrs:
|
||||
origval = attrs[attr]
|
||||
if isinstance(origval, int):
|
||||
endattrs[attr] = origval
|
||||
continue
|
||||
if isinstance(origval, bytes) or isinstance(origval, unicode):
|
||||
origval = {'expression': origval}
|
||||
if 'expression' not in origval:
|
||||
@@ -956,6 +1028,20 @@ class ConfluentInputMessage(ConfluentMessage):
|
||||
def is_valid_key(self, key):
|
||||
return key in self.valid_values
|
||||
|
||||
class InputSigningParameters(InputConfigChangeSet):
|
||||
|
||||
def get_days(self, node):
|
||||
attribs = self.get_attributes(node)
|
||||
return int(attribs['days'])
|
||||
|
||||
def get_added_names(self, node):
|
||||
attribs = self.get_attributes(node)
|
||||
addnames = []
|
||||
for subj in (attribs.get('added_names') or '').split(','):
|
||||
if subj:
|
||||
addnames.append(subj.strip())
|
||||
return addnames
|
||||
|
||||
|
||||
class InputCertificateAuthority(ConfluentInputMessage):
|
||||
keyname = 'pem'
|
||||
@@ -975,6 +1061,10 @@ class InputDeploymentLock(ConfluentInputMessage):
|
||||
keyname = 'lock'
|
||||
valid_values = ['autolock', 'unlocked', 'locked']
|
||||
|
||||
class InputRemoteConfig(ConfluentInputMessage):
|
||||
keyname = 'category'
|
||||
valid_values = ['post.d', 'firstboot.d', 'onboot.d']
|
||||
|
||||
class DeploymentLock(ConfluentChoiceMessage):
|
||||
valid_values = set([
|
||||
'autolock',
|
||||
@@ -1257,6 +1347,7 @@ class BootDevice(ConfluentChoiceMessage):
|
||||
'cd',
|
||||
'floppy',
|
||||
'usb',
|
||||
'http',
|
||||
])
|
||||
|
||||
valid_bootmodes = set([
|
||||
@@ -1699,12 +1790,18 @@ class Disk(ConfluentMessage):
|
||||
'rebuilding',
|
||||
'online',
|
||||
'offline',
|
||||
'failed',
|
||||
'foreign',
|
||||
])
|
||||
state_aliases = {
|
||||
'unconfigured bad': 'fault',
|
||||
'unconfigured good': 'unconfigured',
|
||||
'(foreign) unconfigured good': 'foreign',
|
||||
'unconfiguredgood': 'unconfigured',
|
||||
'global hot spare': 'hotspare',
|
||||
'globalhotspare': 'hotspare',
|
||||
'dedicated hot spare': 'hotspare',
|
||||
'dedicatedhotspare': 'hotspare',
|
||||
}
|
||||
|
||||
def _normalize_state(self, instate):
|
||||
|
||||
@@ -35,6 +35,7 @@ if __name__ == '__main__':
|
||||
import confluent.config.configmanager as cfm
|
||||
import base64
|
||||
import confluent.networking.nxapi as nxapi
|
||||
import confluent.networking.srlinux as srlinux
|
||||
import confluent.exceptions as exc
|
||||
import confluent.log as log
|
||||
import confluent.messages as msg
|
||||
@@ -189,6 +190,10 @@ def detect_backend(switch, verifier):
|
||||
apicheck, retcode = wc.grab_json_response_with_status('/api/')
|
||||
if retcode == 400 and apicheck.startswith(b'{"imdata":['):
|
||||
_fastbackends[switch] = 'nxapi'
|
||||
else:
|
||||
rsp = wc.grab_json_response_with_status('/jsonrpc', {'dummy': 'data'}, returnheaders=True)
|
||||
if rsp[1] == 401 and rsp[2].get('WWW-Authenticate', '').startswith('Basic realm="SRLinux"'):
|
||||
_fastbackends[switch] = 'srlinux'
|
||||
return _fastbackends.get(switch, None)
|
||||
|
||||
def _extract_neighbor_data_https(switch, user, password, cfm, lldpdata):
|
||||
@@ -203,6 +208,8 @@ def _extract_neighbor_data_https(switch, user, password, cfm, lldpdata):
|
||||
return _extract_neighbor_data_affluent(switch, user, password, cfm, lldpdata, wc)
|
||||
elif backend == 'nxapi':
|
||||
return _extract_neighbor_data_nxapi(switch, user, password, cfm, lldpdata, wc)
|
||||
elif backend == 'srlinux':
|
||||
return _extract_neighbor_data_srlinux(switch, user, password, cfm, lldpdata, wc)
|
||||
|
||||
|
||||
|
||||
@@ -217,10 +224,28 @@ def _extract_neighbor_data_nxapi(switch, user, password, cfm, lldpdata, wc):
|
||||
)
|
||||
portdata['peerid'] = peerid
|
||||
_extract_extended_desc(portdata, portdata['peerdescription'], True)
|
||||
portdata['switch'] = switch
|
||||
_neighbypeerid[peerid] = portdata
|
||||
lldpdata[port] = portdata
|
||||
_neighdata[switch] = lldpdata
|
||||
|
||||
def _extract_neighbor_data_srlinux(switch, user, password, cfm, lldpdata, wc):
|
||||
cli = srlinux.SRLinuxClient(switch, user, password, cfm)
|
||||
lldpinfo = cli.get_lldp()
|
||||
for port in lldpinfo:
|
||||
portdata = lldpinfo[port]
|
||||
peerid = '{0}.{1}'.format(
|
||||
portdata.get('peerchassisid', '').replace(':', '-').replace('/', '-'),
|
||||
portdata.get('peerportid', '').replace(':', '-').replace('/', '-'),
|
||||
)
|
||||
portdata['peerid'] = peerid
|
||||
_extract_extended_desc(portdata, portdata['peerdescription'], True)
|
||||
portdata['switch'] = switch
|
||||
_neighbypeerid[peerid] = portdata
|
||||
lldpdata[port] = portdata
|
||||
_neighdata[switch] = lldpdata
|
||||
|
||||
|
||||
def _extract_neighbor_data_affluent(switch, user, password, cfm, lldpdata, wc):
|
||||
wc.set_basic_credentials(user, password)
|
||||
neighdata = wc.grab_json_response('/affluent/lldp/all')
|
||||
@@ -255,7 +280,13 @@ def _extract_neighbor_data_b(args):
|
||||
|
||||
args are carried as a tuple, because of eventlet convenience
|
||||
"""
|
||||
switch, password, user, cfm, force = args[:5]
|
||||
# Safely unpack args with defaults to avoid IndexError
|
||||
switch = args[0] if len(args) > 0 else None
|
||||
password = args[1] if len(args) > 1 else None
|
||||
user = args[2] if len(args) > 2 else None
|
||||
cfm = args[3] if len(args) > 3 else None
|
||||
privproto = args[4] if len(args) > 4 else None
|
||||
force = args[5] if len(args) > 5 else False
|
||||
vintage = _neighdata.get(switch, {}).get('!!vintage', 0)
|
||||
now = util.monotonic_time()
|
||||
if vintage > (now - 60) and not force:
|
||||
@@ -265,7 +296,7 @@ def _extract_neighbor_data_b(args):
|
||||
return _extract_neighbor_data_https(switch, user, password, cfm, lldpdata)
|
||||
except Exception as e:
|
||||
pass
|
||||
conn = snmp.Session(switch, password, user)
|
||||
conn = snmp.Session(switch, password, user, privacy_protocol=privproto)
|
||||
sid = None
|
||||
for sysid in conn.walk('1.3.6.1.2.1.1.2'):
|
||||
sid = str(sysid[1][6:])
|
||||
@@ -364,8 +395,8 @@ def _extract_neighbor_data(args):
|
||||
return _extract_neighbor_data_b(args)
|
||||
except Exception as e:
|
||||
yieldexc = False
|
||||
if len(args) >= 6:
|
||||
yieldexc = args[5]
|
||||
if len(args) >= 7:
|
||||
yieldexc = args[6]
|
||||
if yieldexc:
|
||||
return e
|
||||
else:
|
||||
|
||||
@@ -55,6 +55,7 @@ import confluent.log as log
|
||||
import confluent.messages as msg
|
||||
import confluent.noderange as noderange
|
||||
import confluent.networking.nxapi as nxapi
|
||||
import confluent.networking.srlinux as srlinux
|
||||
import confluent.util as util
|
||||
from eventlet.greenpool import GreenPool
|
||||
import eventlet.green.subprocess as subprocess
|
||||
@@ -153,7 +154,7 @@ def _nodelookup(switch, ifname):
|
||||
return None
|
||||
|
||||
def _fast_map_switch(args):
|
||||
switch, password, user, cfgm = args
|
||||
switch, password, user, cfgm = args[:4]
|
||||
macdata = None
|
||||
kv = util.TLSCertVerifier(cfgm, switch,
|
||||
'pubkeys.tls_hardwaremanager').verify_cert
|
||||
@@ -162,8 +163,16 @@ def _fast_map_switch(args):
|
||||
return _affluent_map_switch(switch, password, user, cfgm, macdata)
|
||||
elif backend == 'nxapi':
|
||||
return _nxapi_map_switch(switch, password, user, cfgm)
|
||||
elif backend == 'srlinux':
|
||||
return _srlinux_map_switch(switch, password, user, cfgm)
|
||||
raise Exception("No fast backend match")
|
||||
|
||||
def _srlinux_map_switch(switch, password, user, cfgm):
|
||||
cli = srlinux.SRLinuxClient(switch, user, password, cfgm)
|
||||
mt = cli.get_mac_table()
|
||||
_macsbyswitch[switch] = mt
|
||||
_fast_backend_fixup(mt, switch)
|
||||
|
||||
def _nxapi_map_switch(switch, password, user, cfgm):
|
||||
cli = nxapi.NxApiClient(switch, user, password, cfgm)
|
||||
mt = cli.get_mac_table()
|
||||
@@ -213,14 +222,14 @@ def _fast_backend_fixup(macs, switch):
|
||||
else:
|
||||
_nodesbymac[mac] = (nodename, nummacs)
|
||||
|
||||
def _offload_map_switch(switch, password, user):
|
||||
def _offload_map_switch(switch, password, user, privprotocol=None):
|
||||
if _offloader is None:
|
||||
_start_offloader()
|
||||
evtid = random.randint(0, 4294967295)
|
||||
while evtid in _offloadevts:
|
||||
evtid = random.randint(0, 4294967295)
|
||||
_offloadevts[evtid] = eventlet.Event()
|
||||
_offloader.stdin.write(msgpack.packb((evtid, switch, password, user),
|
||||
_offloader.stdin.write(msgpack.packb((evtid, switch, password, user, privprotocol),
|
||||
use_bin_type=True))
|
||||
_offloader.stdin.flush()
|
||||
result = _offloadevts[evtid].wait()
|
||||
@@ -280,12 +289,11 @@ def _map_switch_backend(args):
|
||||
# fallback if ifName is empty
|
||||
#
|
||||
global _macmap
|
||||
if len(args) == 4:
|
||||
switch, password, user, _ = args # 4th arg is for affluent only
|
||||
if not user:
|
||||
user = None
|
||||
else:
|
||||
switch, password = args
|
||||
switch = args[0] if len(args) > 0 else None
|
||||
password = args[1] if len(args) > 1 else None
|
||||
user = args[2] if len(args) > 2 else None
|
||||
privprotocol = args[4] if len(args) > 4 else None
|
||||
if not user: # make '' be treated as None
|
||||
user = None
|
||||
if switch not in noaffluent:
|
||||
try:
|
||||
@@ -298,7 +306,7 @@ def _map_switch_backend(args):
|
||||
except Exception as e:
|
||||
pass
|
||||
mactobridge, ifnamemap, bridgetoifmap = _offload_map_switch(
|
||||
switch, password, user)
|
||||
switch, password, user, privprotocol)
|
||||
maccounts = {}
|
||||
bridgetoifvalid = False
|
||||
for mac in mactobridge:
|
||||
@@ -367,9 +375,9 @@ def _map_switch_backend(args):
|
||||
_nodesbymac[mac] = (nodename, maccounts[ifname])
|
||||
_macsbyswitch[switch] = newmacs
|
||||
|
||||
def _snmp_map_switch_relay(rqid, switch, password, user):
|
||||
def _snmp_map_switch_relay(rqid, switch, password, user, privprotocol=None):
|
||||
try:
|
||||
res = _snmp_map_switch(switch, password, user)
|
||||
res = _snmp_map_switch(switch, password, user, privprotocol)
|
||||
payload = msgpack.packb((rqid,) + res, use_bin_type=True)
|
||||
try:
|
||||
sys.stdout.buffer.write(payload)
|
||||
@@ -391,10 +399,10 @@ def _snmp_map_switch_relay(rqid, switch, password, user):
|
||||
finally:
|
||||
sys.stdout.flush()
|
||||
|
||||
def _snmp_map_switch(switch, password, user):
|
||||
def _snmp_map_switch(switch, password, user, privprotocol=None):
|
||||
haveqbridge = False
|
||||
mactobridge = {}
|
||||
conn = snmp.Session(switch, password, user)
|
||||
conn = snmp.Session(switch, password, user, privacy_protocol=privprotocol)
|
||||
ifnamemap = get_portnamemap(conn)
|
||||
for vb in conn.walk('1.3.6.1.2.1.17.7.1.2.2.1.2'):
|
||||
haveqbridge = True
|
||||
|
||||
@@ -21,7 +21,7 @@ import confluent.collective.manager as collective
|
||||
def get_switchcreds(configmanager, switches):
|
||||
switchcfg = configmanager.get_node_attributes(
|
||||
switches, ('secret.hardwaremanagementuser', 'secret.snmpcommunity',
|
||||
'secret.hardwaremanagementpassword',
|
||||
'secret.hardwaremanagementpassword', 'snmp.privacyprotocol',
|
||||
'collective.managercandidates'), decrypt=True)
|
||||
switchauth = []
|
||||
for switch in switches:
|
||||
@@ -39,6 +39,7 @@ def get_switchcreds(configmanager, switches):
|
||||
user = None
|
||||
password = switchparms.get(
|
||||
'secret.snmpcommunity', {}).get('value', None)
|
||||
privacy_protocol = None
|
||||
if not password:
|
||||
password = switchparms.get(
|
||||
'secret.hardwaremanagementpassword', {}).get('value',
|
||||
@@ -47,7 +48,9 @@ def get_switchcreds(configmanager, switches):
|
||||
'secret.hardwaremanagementuser', {}).get('value', None)
|
||||
if not user:
|
||||
user = None
|
||||
switchauth.append((switch, password, user, configmanager))
|
||||
privacy_protocol = switchparms.get(
|
||||
'snmp.privacyprotocol', {}).get('value', None)
|
||||
switchauth.append((switch, password, user, configmanager, privacy_protocol))
|
||||
return switchauth
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
import confluent.util as util
|
||||
import eventlet
|
||||
webclient = eventlet.import_patched('pyghmi.util.webclient')
|
||||
|
||||
|
||||
|
||||
class SRLinuxClient:
|
||||
def __init__(self, switch, user, password, configmanager):
|
||||
self.cachedurls = {}
|
||||
self.switch = switch
|
||||
if configmanager:
|
||||
cv = util.TLSCertVerifier(
|
||||
configmanager, switch, 'pubkeys.tls_hardwaremanager'
|
||||
).verify_cert
|
||||
else:
|
||||
cv = lambda x: True
|
||||
self.user = user
|
||||
self.password = password
|
||||
try:
|
||||
self.user = self.user.decode()
|
||||
self.password = self.password.decode()
|
||||
except Exception:
|
||||
pass
|
||||
self.wc = webclient.SecureHTTPConnection(switch, port=443, verifycallback=cv)
|
||||
self.wc.set_basic_credentials(self.user, self.password)
|
||||
self.rpc_id = 1
|
||||
self.login()
|
||||
|
||||
def login(self):
|
||||
# Just a quick query to validate that credentials are correct and device is reachable and TLS works out however it is supposed to
|
||||
self._get_state('/system/information')
|
||||
|
||||
|
||||
|
||||
|
||||
def _rpc_call(self, method, params=None):
|
||||
"""Make a JSON-RPC call to SR-Linux"""
|
||||
payload = {
|
||||
'jsonrpc': '2.0',
|
||||
'id': self.rpc_id,
|
||||
'method': method,
|
||||
}
|
||||
if params:
|
||||
payload['params'] = params
|
||||
|
||||
self.rpc_id += 1
|
||||
|
||||
rsp = self.wc.grab_json_response_with_status('/jsonrpc', payload)
|
||||
if rsp[1] != 200:
|
||||
raise Exception(f"Failed RPC call: {method}, status: {rsp[1]}")
|
||||
|
||||
result = rsp[0]
|
||||
if 'error' in result:
|
||||
raise Exception(f"RPC error: {result['error']}")
|
||||
|
||||
return result.get('result', {})
|
||||
|
||||
def _get_state(self, path, datastore='state'):
|
||||
"""Get state data from SR-Linux using JSON-RPC get method"""
|
||||
params = {
|
||||
'commands': [
|
||||
{
|
||||
'path': path,
|
||||
'datastore': datastore
|
||||
}
|
||||
]
|
||||
}
|
||||
result = self._rpc_call('get', params)
|
||||
return result
|
||||
|
||||
def get_firmware(self):
|
||||
"""Get firmware/software version information"""
|
||||
firmdata = {}
|
||||
result = self._get_state('/system/information')
|
||||
for item in result:
|
||||
if 'version' in item:
|
||||
firmdata['SR-Linux'] = {'version': item['version']}
|
||||
if 'build-date' in item:
|
||||
if 'SR-Linux' in firmdata:
|
||||
firmdata['SR-Linux']['date'] = item['build-date']
|
||||
return firmdata
|
||||
|
||||
def get_sensors(self):
|
||||
"""Get sensor readings from the device"""
|
||||
sensedata = []
|
||||
result = self._get_state('/platform/control/temperature')
|
||||
for item in result:
|
||||
for pcc in item:
|
||||
currreading = {}
|
||||
for reading in item[pcc]:
|
||||
if reading.get('temperature', {}).get('alarm-status', False):
|
||||
currreading['health'] = 'critical'
|
||||
else:
|
||||
currreading['health'] = 'ok'
|
||||
states = []
|
||||
if reading.get('oper-state', 'up',) != 'up':
|
||||
states = [reading.get('oper-reason', 'unknown')]
|
||||
currreading['states'] = states
|
||||
currreading['name'] = 'Slot {} Temperature'.format(reading.get('slot', 'Unknown'))
|
||||
currreading['value'] = reading.get('temperature', {}).get('instant', 'Unknown')
|
||||
currreading['units'] = '°C'
|
||||
sensedata.append(currreading)
|
||||
|
||||
result = self._get_state('/platform/fan-tray')
|
||||
for item in result:
|
||||
for pft in item:
|
||||
currreading = {}
|
||||
for reading in item[pft]:
|
||||
if reading.get('srl_nokia-platform-healthz:healthz', {}).get('status', 'healthy') != 'healthy':
|
||||
currreading['health'] = 'critical'
|
||||
else:
|
||||
currreading['health'] = 'ok'
|
||||
states = []
|
||||
if reading.get('oper-state', 'up',) != 'up':
|
||||
states = [reading.get('oper-reason', 'unknown')]
|
||||
currreading['states'] = states
|
||||
currreading['name'] = 'Fan Tray {}'.format(reading.get('id', 'Unknown'))
|
||||
currreading['value'] = reading.get('fan', {}).get('speed', 'Unknown')
|
||||
currreading['units'] = '%'
|
||||
sensedata.append(currreading)
|
||||
|
||||
result = self._get_state('/platform/power-supply')
|
||||
for item in result:
|
||||
for pps in item:
|
||||
for reading in item[pps]:
|
||||
currreading = {}
|
||||
if reading.get('srl_nokia-platform-healthz:healthz', {}).get('status', 'healthy') != 'healthy':
|
||||
currreading['health'] = 'critical'
|
||||
else:
|
||||
currreading['health'] = 'ok'
|
||||
states = []
|
||||
if reading.get('oper-state', 'up',) != 'up':
|
||||
states = [reading.get('oper-reason', 'unknown')]
|
||||
currreading['states'] = states
|
||||
currreading['name'] = 'Power Supply {} Health'.format(reading.get('id', 'Unknown'))
|
||||
sensedata.append(currreading)
|
||||
tempreading = {'health': 'ok'}
|
||||
tempreading['name'] = 'Power Supply {} Temperature'.format(reading.get('id', 'Unknown'))
|
||||
tempreading['value'] = reading.get('temperature', {}).get('instant', 'Unknown')
|
||||
tempreading['units'] = '°C'
|
||||
sensedata.append(tempreading)
|
||||
for powstat in 'current', 'power', 'voltage':
|
||||
powreading = {'health': 'ok'}
|
||||
powreading['name'] = 'Power Supply {} {}'.format(reading.get('id', 'Unknown'), powstat.capitalize())
|
||||
powreading['value'] = reading.get('input', {}).get(powstat, 'Unknown')
|
||||
if powstat == 'current':
|
||||
powreading['units'] = 'A'
|
||||
elif powstat == 'power':
|
||||
powreading['units'] = 'W'
|
||||
elif powstat == 'voltage':
|
||||
powreading['units'] = 'V'
|
||||
sensedata.append(powreading)
|
||||
return sensedata
|
||||
|
||||
|
||||
|
||||
def get_health(self):
|
||||
healthdata = {'health': 'ok', 'sensors': []}
|
||||
sensors = self.get_sensors()
|
||||
|
||||
for sensor in sensors:
|
||||
currhealth = sensor.get('health', 'ok')
|
||||
if currhealth != 'ok':
|
||||
healthdata['sensors'].append(sensor)
|
||||
if sensor['health'] == 'critical':
|
||||
healthdata['health'] = 'critical'
|
||||
elif sensor['health'] == 'warning' and healthdata['health'] != 'critical':
|
||||
healthdata['health'] = 'warning'
|
||||
|
||||
return healthdata
|
||||
|
||||
def get_inventory(self):
|
||||
invdata = []
|
||||
results = self._get_state('/platform/chassis')
|
||||
for result in results:
|
||||
invinfo = {'name': 'System', 'present': True}
|
||||
invinfo['information'] = {'Manufacturer': 'Nokia'}
|
||||
|
||||
if isinstance(result, dict):
|
||||
for key, value in result.items():
|
||||
if key == 'serial-number':
|
||||
invinfo['information']['Serial Number'] = value
|
||||
elif key == 'part-number':
|
||||
invinfo['information']['Part Number'] = value
|
||||
elif key == 'type':
|
||||
invinfo['information']['Model'] = value
|
||||
|
||||
if invinfo['information']:
|
||||
invdata.append(invinfo)
|
||||
return invdata
|
||||
|
||||
def get_mac_table(self):
|
||||
macdict = {}
|
||||
response = self._get_state('/network-instance/bridge-table/mac-table/mac')
|
||||
for datum in response:
|
||||
for niname in datum:
|
||||
for nin in datum[niname]:
|
||||
btable = nin.get('bridge-table', {})
|
||||
for btab in btable:
|
||||
macs = btable[btab].get('mac', [])
|
||||
for macent in macs:
|
||||
macaddr = macent.get('address', None)
|
||||
if macaddr:
|
||||
macport = macent.get('destination', None)
|
||||
if macport:
|
||||
macdict.setdefault(macport, []).append(macaddr)
|
||||
return macdict
|
||||
|
||||
def get_lldp(self):
|
||||
lldpbyport = {}
|
||||
|
||||
response = self._get_state('/system/lldp/interface')
|
||||
for datum in response:
|
||||
for intfname in datum:
|
||||
lldpallinfo = datum[intfname]
|
||||
for lldpdatum in lldpallinfo:
|
||||
myportname = lldpdatum.get('name', None)
|
||||
for neighinfo in lldpdatum.get('neighbor', []):
|
||||
peerdesc = neighinfo.get('system-description', 'Unknown')
|
||||
peername = neighinfo.get('system-name', 'Unknown')
|
||||
peerchassisid = neighinfo.get('chassis-id', 'Unknown')
|
||||
peerportid = neighinfo.get('port-id', 'Unknown')
|
||||
lldpinfo = {
|
||||
'verified': True, # Data provided with authentication over TLS
|
||||
'peerdescription': peerdesc,
|
||||
'peername': peername,
|
||||
'peerchassisid': peerchassisid,
|
||||
'peerportid': peerportid,
|
||||
'portid': myportname,
|
||||
'port': myportname,
|
||||
}
|
||||
lldpbyport[myportname] = lldpinfo
|
||||
return lldpbyport
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
import os
|
||||
from pprint import pprint
|
||||
myuser = os.environ.get('SWITCHUSER')
|
||||
mypass = os.environ.get('SWITCHPASS')
|
||||
if not myuser or not mypass:
|
||||
print("Set SWITCHUSER and SWITCHPASS environment variables")
|
||||
sys.exit(1)
|
||||
|
||||
srl = SRLinuxClient(sys.argv[1], myuser, mypass, None)
|
||||
pprint(srl.get_firmware())
|
||||
pprint(srl.get_inventory())
|
||||
pprint(srl.get_sensors())
|
||||
pprint(srl.get_health())
|
||||
pprint(srl.get_lldp())
|
||||
pprint(srl.get_mac_table())
|
||||
@@ -1137,6 +1137,8 @@ def generate_stock_profiles(defprofile, distpath, targpath, osname,
|
||||
initrds = ['{0}/initramfs/{1}'.format(defprofile, initrd) for initrd in os.listdir('{0}/initramfs'.format(defprofile))]
|
||||
if os.path.exists('{0}/initramfs/{1}'.format(defprofile, arch)):
|
||||
initrds.extend(['{0}/initramfs/{1}/{2}'.format(defprofile, arch, initrd) for initrd in os.listdir('{0}/initramfs/{1}'.format(defprofile, arch))])
|
||||
elif arch == 'arm64' and os.path.exists('{0}/initramfs/aarch64'.format(defprofile)):
|
||||
initrds.extend(['{0}/initramfs/aarch64/{1}'.format(defprofile, initrd) for initrd in os.listdir('{0}/initramfs/aarch64'.format(defprofile))])
|
||||
for fullpath in initrds:
|
||||
initrd = os.path.basename(fullpath)
|
||||
if os.path.isdir(fullpath):
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2019 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
# This plugin provides an ssh implementation comforming to the 'console'
|
||||
# specification. consoleserver or shellserver would be equally likely
|
||||
# to use this.
|
||||
|
||||
import confluent.exceptions as cexc
|
||||
import confluent.interface.console as conapi
|
||||
import confluent.log as log
|
||||
import confluent.util as util
|
||||
import pyghmi.exceptions as pygexc
|
||||
import pyghmi.redfish.command as rcmd
|
||||
import eventlet
|
||||
import eventlet.green.ssl as ssl
|
||||
try:
|
||||
websocket = eventlet.import_patched('websocket')
|
||||
wso = websocket.WebSocket
|
||||
except Exception:
|
||||
wso = object
|
||||
|
||||
|
||||
def get_conn_params(node, configdata):
|
||||
if 'secret.hardwaremanagementuser' in configdata:
|
||||
username = configdata['secret.hardwaremanagementuser']['value']
|
||||
else:
|
||||
username = 'USERID'
|
||||
if 'secret.hardwaremanagementpassword' in configdata:
|
||||
passphrase = configdata['secret.hardwaremanagementpassword']['value']
|
||||
else:
|
||||
passphrase = 'PASSW0RD' # for lack of a better guess
|
||||
if 'hardwaremanagement.manager' in configdata:
|
||||
bmc = configdata['hardwaremanagement.manager']['value']
|
||||
else:
|
||||
bmc = node
|
||||
bmc = bmc.split('/', 1)[0]
|
||||
return {
|
||||
'username': username,
|
||||
'passphrase': passphrase,
|
||||
'bmc': bmc,
|
||||
}
|
||||
_configattributes = ('secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword',
|
||||
'hardwaremanagement.manager')
|
||||
|
||||
|
||||
class WrappedWebSocket(wso):
|
||||
|
||||
def set_verify_callback(self, callback):
|
||||
self._certverify = callback
|
||||
|
||||
def connect(self, url, **options):
|
||||
add_tls = url.startswith('wss://')
|
||||
if add_tls:
|
||||
hostname, port, resource, _ = websocket._url.parse_url(url)
|
||||
if hostname[0] != '[' and ':' in hostname:
|
||||
hostname = '[{0}]'.format(hostname)
|
||||
if resource[0] != '/':
|
||||
resource = '/{0}'.format(resource)
|
||||
url = 'ws://{0}:443{1}'.format(hostname,resource)
|
||||
else:
|
||||
return super(WrappedWebSocket, self).connect(url, **options)
|
||||
self.sock_opt.timeout = options.get('timeout', self.sock_opt.timeout)
|
||||
self.sock, addrs = websocket._http.connect(url, self.sock_opt, websocket._http.proxy_info(**options),
|
||||
options.pop('socket', None))
|
||||
self.sock = ssl.wrap_socket(self.sock, cert_reqs=ssl.CERT_NONE)
|
||||
# The above is supersedeed by the _certverify, which provides
|
||||
# known-hosts style cert validaiton
|
||||
bincert = self.sock.getpeercert(binary_form=True)
|
||||
if not self._certverify(bincert):
|
||||
raise pygexc.UnrecognizedCertificate('Unknown certificate', bincert)
|
||||
try:
|
||||
self.handshake_response = websocket._handshake.handshake(self.sock, url, *addrs, **options)
|
||||
if self.handshake_response.status in websocket._handshake.SUPPORTED_REDIRECT_STATUSES:
|
||||
options['redirect_limit'] = options.pop('redirect_limit', 3) - 1
|
||||
if options['redirect_limit'] < 0:
|
||||
raise Exception('Redirect limit hit')
|
||||
url = self.handshake_response.headers['location']
|
||||
self.sock.close()
|
||||
return self.connect(url, **options)
|
||||
self.connected = True
|
||||
except:
|
||||
if self.sock:
|
||||
self.sock.close()
|
||||
self.sock = None
|
||||
raise
|
||||
|
||||
|
||||
class TsmConsole(conapi.Console):
|
||||
|
||||
def __init__(self, node, config):
|
||||
self.node = node
|
||||
self.ws = None
|
||||
configdata = config.get_node_attributes([node], _configattributes, decrypt=True)
|
||||
connparams = get_conn_params(node, configdata[node])
|
||||
self.username = connparams['username']
|
||||
self.password = connparams['passphrase']
|
||||
self.bmc = connparams['bmc']
|
||||
self.origbmc = connparams['bmc']
|
||||
if ':' in self.bmc:
|
||||
self.bmc = '[{0}]'.format(self.bmc)
|
||||
self.datacallback = None
|
||||
self.nodeconfig = config
|
||||
self.connected = False
|
||||
|
||||
|
||||
def recvdata(self):
|
||||
while self.connected:
|
||||
pendingdata = self.ws.recv()
|
||||
if pendingdata == '':
|
||||
self.datacallback(conapi.ConsoleEvent.Disconnect)
|
||||
return
|
||||
self.datacallback(pendingdata)
|
||||
|
||||
def connect(self, callback):
|
||||
self.datacallback = callback
|
||||
rc = rcmd.Command(self.origbmc, self.username,
|
||||
self.password,
|
||||
verifycallback=lambda x: True)
|
||||
wc = rc.oem.wc
|
||||
bmc = self.bmc
|
||||
if '%' in self.bmc:
|
||||
prefix = self.bmc.split('%')[0]
|
||||
bmc = prefix + ']'
|
||||
self.ws = WrappedWebSocket(host=bmc)
|
||||
kv = util.TLSCertVerifier(
|
||||
self.nodeconfig, self.node, 'pubkeys.tls_hardwaremanager').verify_cert
|
||||
self.ws.set_verify_callback(kv)
|
||||
self.ws.connect('wss://{0}/h5sol'.format(self.bmc), host=bmc, cookie='QSESSIONID={0}; __Host-garc={1}'.format(wc.cookies['QSESSIONID'], rc.oem.csrftok))
|
||||
self.connected = True
|
||||
eventlet.spawn_n(self.recvdata)
|
||||
return
|
||||
|
||||
def write(self, data):
|
||||
self.ws.send(data)
|
||||
|
||||
def close(self):
|
||||
if self.ws:
|
||||
self.ws.close()
|
||||
self.connected = False
|
||||
self.datacallback = None
|
||||
|
||||
def create(nodes, element, configmanager, inputdata):
|
||||
if len(nodes) == 1:
|
||||
return TsmConsole(nodes[0], configmanager)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import confluent.selfservice as selfservice
|
||||
import confluent.messages as msg
|
||||
import confluent.runansible as runansible
|
||||
|
||||
_user_initiated_runs = {}
|
||||
def update(nodes, element, configmanager, inputdata):
|
||||
if element[-1] != 'run':
|
||||
raise ValueError('Invalid element for remoteconfig plugin')
|
||||
for node in nodes:
|
||||
category = inputdata.inputbynode[node]
|
||||
playlist = selfservice.list_ansible_scripts(configmanager, node, category)
|
||||
if playlist:
|
||||
_user_initiated_runs[node] = True
|
||||
runansible.run_playbooks(playlist, [node])
|
||||
yield msg.CreatedResource(
|
||||
'/nodes/{0}/deployment/remote_config/active/{0}'.format(node))
|
||||
else:
|
||||
yield msg.ConfluentNodeError('No remote configuration for category "{0}"', node)
|
||||
|
||||
def retrieve(nodes, element, configmanager, inputdata):
|
||||
for node in nodes:
|
||||
if element[-1] == 'active':
|
||||
rst = runansible.running_status.get(node, None)
|
||||
if not rst:
|
||||
return
|
||||
yield msg.ChildCollection(node)
|
||||
elif element[-2] == 'active' and element[-1] == node:
|
||||
rst = runansible.running_status.get(node, None)
|
||||
if not rst:
|
||||
return
|
||||
playstatus = rst.dump_dict()
|
||||
if playstatus['complete'] and _user_initiated_runs.get(node, False):
|
||||
del runansible.running_status[node]
|
||||
del _user_initiated_runs[node]
|
||||
yield msg.KeyValueData(playstatus, node)
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
|
||||
import confluent.vinzmanager as vinzmanager
|
||||
import confluent.exceptions as exc
|
||||
import confluent.firmwaremanager as firmwaremanager
|
||||
@@ -20,6 +22,7 @@ import confluent.messages as msg
|
||||
import confluent.util as util
|
||||
import copy
|
||||
import errno
|
||||
from confluent import certutil
|
||||
import eventlet
|
||||
import eventlet.event
|
||||
import eventlet.green.threading as threading
|
||||
@@ -37,6 +40,7 @@ ipmicommand = eventlet.import_patched('pyghmi.redfish.command')
|
||||
import socket
|
||||
import ssl
|
||||
import traceback
|
||||
import tempfile
|
||||
|
||||
if not hasattr(ssl, 'SSLEOFError'):
|
||||
ssl.SSLEOFError = None
|
||||
@@ -184,8 +188,12 @@ class IpmiCommandWrapper(ipmicommand.Command):
|
||||
(node,), ('secret.hardwaremanagementuser', 'collective.manager',
|
||||
'secret.hardwaremanagementpassword',
|
||||
'hardwaremanagement.manager'), self._attribschanged)
|
||||
htn = cfm.get_node_attributes(node, 'hardwaremanagement.manager_tls_name')
|
||||
subject = htn.get(node, {}).get('hardwaremanagement.manager_tls_name', {}).get('value', None)
|
||||
if not subject:
|
||||
subject = kwargs['bmc']
|
||||
kv = util.TLSCertVerifier(cfm, node,
|
||||
'pubkeys.tls_hardwaremanager').verify_cert
|
||||
'pubkeys.tls_hardwaremanager', subject).verify_cert
|
||||
kwargs['verifycallback'] = kv
|
||||
try:
|
||||
super(IpmiCommandWrapper, self).__init__(**kwargs)
|
||||
@@ -480,8 +488,16 @@ class IpmiHandler(object):
|
||||
else:
|
||||
raise Exception('Not Implemented')
|
||||
|
||||
def update_firmware(self, filename, progress, data, bank):
|
||||
params=()
|
||||
if self.inputdata.parameterdata:
|
||||
params = self.inputdata.parameterdata
|
||||
if params and isinstance(params, str):
|
||||
params = json.loads(params)
|
||||
return self.ipmicmd.update_firmware(filename, progress=progress, data=data, bank=bank, otherfields=params)
|
||||
|
||||
def handle_update(self):
|
||||
u = firmwaremanager.Updater(self.node, self.ipmicmd.update_firmware,
|
||||
u = firmwaremanager.Updater(self.node, self.update_firmware,
|
||||
self.inputdata.nodefile(self.node), self.tenant,
|
||||
bank=self.inputdata.bank,
|
||||
configmanager=self.cfm)
|
||||
@@ -528,6 +544,8 @@ class IpmiHandler(object):
|
||||
return self.handle_alerts()
|
||||
elif self.element[1:3] == ['management_controller', 'certificate_authorities']:
|
||||
return self.handle_cert_authorities()
|
||||
elif self.element[1:3] == ['management_controller', 'certificate']:
|
||||
return self.handle_certificate()
|
||||
elif self.element[1:3] == ['management_controller', 'users']:
|
||||
return self.handle_users()
|
||||
elif self.element[1:3] == ['management_controller', 'net_interfaces']:
|
||||
@@ -578,6 +596,26 @@ class IpmiHandler(object):
|
||||
self.pyghmi_event_to_confluent(event)
|
||||
self.output.put(msg.EventCollection((event,), name=self.node))
|
||||
|
||||
def handle_certificate(self):
|
||||
self.element = self.element[3:]
|
||||
if len(self.element) != 1:
|
||||
raise Exception('Not implemented')
|
||||
if self.element[0] == 'sign' and self.op == 'update':
|
||||
csr = self.ipmicmd.get_bmc_csr()
|
||||
subj, san = util.get_bmc_subject_san(self.cfm, self.node, self.inputdata.get_added_names(self.node))
|
||||
with tempfile.NamedTemporaryFile() as tmpfile:
|
||||
tmpfile.write(csr.encode())
|
||||
tmpfile.flush()
|
||||
certfile = tempfile.NamedTemporaryFile(delete=False)
|
||||
certname = certfile.name
|
||||
certfile.close()
|
||||
certutil.create_certificate(None, certname, tmpfile.name, subj, san, backdate=False,
|
||||
days=self.inputdata.get_days(self.node))
|
||||
with open(certname, 'rb') as certf:
|
||||
cert = certf.read()
|
||||
os.unlink(certname)
|
||||
self.ipmicmd.install_bmc_certificate(cert)
|
||||
|
||||
def handle_cert_authorities(self):
|
||||
if len(self.element) == 3:
|
||||
if self.op == 'read':
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import confluent.exceptions as exc
|
||||
import confluent.networking.srlinux as srlinux
|
||||
import eventlet
|
||||
import eventlet.queue as queue
|
||||
import confluent.messages as msg
|
||||
|
||||
|
||||
def retrieve_node(node, element, user, pwd, configmanager, inputdata, results):
|
||||
try:
|
||||
retrieve_node_backend(node, element, user, pwd, configmanager, inputdata, results)
|
||||
except exc.PubkeyInvalid as e:
|
||||
results.put(msg.ConfluentNodeError(node, 'Mismatch detected between target certificate fingerprint '
|
||||
'and pubkeys.tls_hardwaremanager attribute'))
|
||||
except Exception as e:
|
||||
results.put(e)
|
||||
|
||||
|
||||
def simplify_name(name):
|
||||
return name.lower().replace(' ', '_').replace('/', '-').replace(
|
||||
'_-_', '-')
|
||||
|
||||
|
||||
def retrieve_node_backend(node, element, user, pwd, configmanager, inputdata, results):
|
||||
cli = srlinux.SRLinuxClient(node, user, pwd, configmanager)
|
||||
if element == ['power', 'state']: # client initted successfully, must be on
|
||||
results.put(msg.PowerState(node, 'on'))
|
||||
elif element == ['health', 'hardware']:
|
||||
hinfo = cli.get_health()
|
||||
results.put(msg.HealthSummary(hinfo.get('health', 'unknown'), name=node))
|
||||
results.put(msg.SensorReadings(hinfo.get('sensors', []), name=node))
|
||||
elif element[:3] == ['inventory', 'hardware', 'all']:
|
||||
if len(element) == 3:
|
||||
results.put(msg.ChildCollection('all'))
|
||||
return
|
||||
invinfo = cli.get_inventory()
|
||||
if invinfo:
|
||||
results.put(msg.KeyValueData({'inventory': invinfo}, node))
|
||||
elif element[:3] == ['inventory', 'firmware', 'all']:
|
||||
if len(element) == 3:
|
||||
results.put(msg.ChildCollection('all'))
|
||||
return
|
||||
fwinfo = []
|
||||
for fwnam, fwdat in cli.get_firmware().items():
|
||||
fwinfo.append({fwnam: fwdat})
|
||||
if fwinfo:
|
||||
results.put(msg.Firmware(fwinfo, node))
|
||||
elif element == ['sensors', 'hardware', 'all']:
|
||||
sensors = cli.get_sensors()
|
||||
for sensor in sensors:
|
||||
results.put(msg.ChildCollection(simplify_name(sensor['name'])))
|
||||
elif element[:3] == ['sensors', 'hardware', 'all']:
|
||||
sensors = cli.get_sensors()
|
||||
for sensor in sensors:
|
||||
if element[-1] == 'all' or simplify_name(sensor['name']) == element[-1]:
|
||||
results.put(msg.SensorReadings([sensor], node))
|
||||
else:
|
||||
results.put(msg.ConfluentNodeError(node, 'Not supported'))
|
||||
|
||||
|
||||
def retrieve(nodes, element, configmanager, inputdata):
|
||||
results = queue.LightQueue()
|
||||
workers = set([])
|
||||
creds = configmanager.get_node_attributes(
|
||||
nodes, ['secret.hardwaremanagementuser', 'secret.hardwaremanagementpassword'], decrypt=True)
|
||||
for node in nodes:
|
||||
cred = creds.get(node, {})
|
||||
user = cred.get('secret.hardwaremanagementuser', {}).get('value')
|
||||
pwd = cred.get('secret.hardwaremanagementpassword', {}).get('value')
|
||||
try:
|
||||
user = user.decode()
|
||||
pwd = pwd.decode()
|
||||
except Exception:
|
||||
pass
|
||||
if not user or not pwd:
|
||||
yield msg.ConfluentTargetInvalidCredentials(node)
|
||||
continue
|
||||
workers.add(eventlet.spawn(retrieve_node, node, element, user, pwd, configmanager, inputdata, results))
|
||||
while workers:
|
||||
try:
|
||||
datum = results.get(block=True, timeout=10)
|
||||
while datum:
|
||||
if isinstance(datum, Exception):
|
||||
raise datum
|
||||
if datum:
|
||||
yield datum
|
||||
datum = results.get_nowait()
|
||||
except queue.Empty:
|
||||
pass
|
||||
eventlet.sleep(0.001)
|
||||
for t in list(workers):
|
||||
if t.dead:
|
||||
workers.discard(t)
|
||||
try:
|
||||
while True:
|
||||
datum = results.get_nowait()
|
||||
if datum:
|
||||
yield datum
|
||||
except queue.Empty:
|
||||
pass
|
||||
@@ -21,12 +21,16 @@ try:
|
||||
import eventlet.green.subprocess as subprocess
|
||||
except ImportError:
|
||||
pass
|
||||
import base64
|
||||
import eventlet.green.select as select
|
||||
import shutil
|
||||
import json
|
||||
import socket
|
||||
import msgpack
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
anspypath = None
|
||||
running_status = {}
|
||||
@@ -38,6 +42,7 @@ class PlayRunner(object):
|
||||
self.worker = None
|
||||
self.results = []
|
||||
self.complete = False
|
||||
self.stdout = ''
|
||||
|
||||
def _start_playbooks(self):
|
||||
self.worker = eventlet.spawn(self._really_run_playbooks)
|
||||
@@ -49,6 +54,7 @@ class PlayRunner(object):
|
||||
|
||||
def dump_text(self):
|
||||
stderr = self.stderr
|
||||
stdout = self.stdout
|
||||
retinfo = self.dump_dict()
|
||||
textout = ''
|
||||
for result in retinfo['results']:
|
||||
@@ -65,6 +71,9 @@ class PlayRunner(object):
|
||||
else:
|
||||
textout += result['state'] + '\n'
|
||||
textout += '\n'
|
||||
if stdout:
|
||||
textout += "OUTPUT **********************************\n"
|
||||
textout += stdout
|
||||
if stderr:
|
||||
textout += "ERRORS **********************************\n"
|
||||
textout += stderr
|
||||
@@ -88,25 +97,58 @@ class PlayRunner(object):
|
||||
if ansloc:
|
||||
with open(ansloc, 'r') as onsop:
|
||||
shebang = onsop.readline()
|
||||
anspypath = shebang.strip().replace('#!', '')
|
||||
anspypath = shebang.strip().replace('#!', '').strip()
|
||||
mypath = anspypath
|
||||
if not mypath:
|
||||
mypath = sys.executable
|
||||
with open(os.devnull, 'w+') as devnull:
|
||||
targnodes = ','.join(self.nodes)
|
||||
for playfilename in self.playfiles:
|
||||
worker = subprocess.Popen(
|
||||
[mypath, __file__, targnodes, playfilename],
|
||||
stdin=devnull, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stdout, stder = worker.communicate()
|
||||
self.stderr += stder.decode('utf8')
|
||||
current = memoryview(stdout)
|
||||
while len(current):
|
||||
sz = struct.unpack('=q', current[:8])[0]
|
||||
result = msgpack.unpackb(current[8:8+sz], raw=False)
|
||||
self.results.append(result)
|
||||
current = current[8+sz:]
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
feedback = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
suffix = base64.urlsafe_b64encode(os.urandom(6)).decode('ascii')
|
||||
sockpath = os.path.join(tmpdir, 'feedback.sock.' + suffix)
|
||||
localenv = os.environ.copy()
|
||||
localenv['FEEDBACK_SOCK'] = sockpath
|
||||
feedback.bind(sockpath)
|
||||
feedback.listen(1)
|
||||
with feedback:
|
||||
with open(os.devnull, 'w+') as devnull:
|
||||
targnodes = ','.join(self.nodes)
|
||||
for playfilename in self.playfiles:
|
||||
worker = subprocess.Popen(
|
||||
[mypath, __file__, targnodes, playfilename],
|
||||
stdin=devnull, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, env=localenv)
|
||||
rlist, _, _ = select.select([feedback], [], [], 10)
|
||||
if not rlist:
|
||||
raise RuntimeError(
|
||||
f"Timed out waiting for feedback socket connection for playbook '{playfilename}'"
|
||||
)
|
||||
|
||||
def _recv_results(sock, timeout=0.1):
|
||||
while select.select([sock], [], [], timeout)[0]:
|
||||
hdr = b''
|
||||
while len(hdr) < 8:
|
||||
chunk = sock.recv(8 - len(hdr))
|
||||
if not chunk:
|
||||
if not hdr:
|
||||
return
|
||||
raise RuntimeError("Socket closed while receiving message header")
|
||||
hdr += chunk
|
||||
msglen = struct.unpack('=q', hdr)[0]
|
||||
msg = b''
|
||||
while len(msg) < msglen:
|
||||
chunk = sock.recv(msglen - len(msg))
|
||||
if not chunk:
|
||||
raise RuntimeError("Socket closed while receiving message")
|
||||
msg += chunk
|
||||
self.results.append(msgpack.unpackb(msg, raw=False))
|
||||
conn, _ = feedback.accept()
|
||||
with conn:
|
||||
while worker.poll() is None:
|
||||
_recv_results(conn)
|
||||
_recv_results(conn, timeout=0)
|
||||
stdout, stder = worker.communicate()
|
||||
self.stderr += stder.decode('utf8')
|
||||
self.stdout += stdout.decode('utf8')
|
||||
finally:
|
||||
self.complete = True
|
||||
|
||||
@@ -119,7 +161,7 @@ def run_playbooks(playfiles, nodes):
|
||||
runner._start_playbooks()
|
||||
|
||||
|
||||
def print_result(result, state, collector=None):
|
||||
def print_result(result, state, collector=None, callbacksock=None):
|
||||
output = {
|
||||
'task_name': result.task_name,
|
||||
'changed': result._result.get('changed', ''),
|
||||
@@ -130,12 +172,12 @@ def print_result(result, state, collector=None):
|
||||
del result._result['warnings']
|
||||
except KeyError:
|
||||
pass
|
||||
if collector:
|
||||
if state != 'ok' and collector and hasattr(collector, '_dump_results'):
|
||||
output['errorinfo'] = collector._dump_results(result._result)
|
||||
msg = msgpack.packb(output, use_bin_type=True)
|
||||
msglen = len(msg)
|
||||
sys.stdout.buffer.write(struct.pack('=q', msglen))
|
||||
sys.stdout.buffer.write(msg)
|
||||
callbacksock.sendall(struct.pack('=q', msglen))
|
||||
callbacksock.sendall(msg)
|
||||
|
||||
if __name__ == '__main__':
|
||||
from ansible.inventory.manager import InventoryManager
|
||||
@@ -149,16 +191,25 @@ if __name__ == '__main__':
|
||||
import ansible.plugins.loader
|
||||
import yaml
|
||||
|
||||
sockpath = os.environ.get('FEEDBACK_SOCK')
|
||||
if not sockpath:
|
||||
sys.stderr.write('No feedback socket specified\n')
|
||||
sys.exit(1)
|
||||
|
||||
callbacksock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
callbacksock.connect(sockpath)
|
||||
|
||||
|
||||
class ResultsCollector(CallbackBase):
|
||||
|
||||
def v2_runner_on_unreachable(self, result):
|
||||
print_result(result, 'UNREACHABLE', self)
|
||||
print_result(result, 'UNREACHABLE', self, callbacksock)
|
||||
|
||||
def v2_runner_on_ok(self, result, *args, **kwargs):
|
||||
print_result(result, 'ok')
|
||||
print_result(result, 'ok', self, callbacksock)
|
||||
|
||||
def v2_runner_on_failed(self, result, *args, **kwargs):
|
||||
print_result(result, 'FAILED', self)
|
||||
print_result(result, 'FAILED', self, callbacksock)
|
||||
|
||||
context.CLIARGS = ImmutableDict(
|
||||
connection='smart', module_path=['/usr/share/ansible'], forks=10,
|
||||
|
||||
@@ -395,7 +395,7 @@ def handle_request(env, start_response):
|
||||
keymap = ckeymap
|
||||
try:
|
||||
tdc = util.run(['timedatectl'])[0].split(b'\n')
|
||||
except subprocess.CalledProcessError:
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
tdc = []
|
||||
currtzvintage = time.time()
|
||||
ncfg['timezone'] = currtz
|
||||
@@ -519,19 +519,7 @@ def handle_request(env, start_response):
|
||||
yield ''
|
||||
elif env['PATH_INFO'].startswith('/self/remoteconfig/') and 'POST' == operation:
|
||||
scriptcat = env['PATH_INFO'].replace('/self/remoteconfig/', '')
|
||||
playlist = []
|
||||
for privacy in ('public', 'private'):
|
||||
slist, profile = get_scriptlist(
|
||||
scriptcat, cfg, nodename,
|
||||
'/var/lib/confluent/{0}/os/{{0}}/ansible/{{1}}'.format(privacy))
|
||||
dirname = '/var/lib/confluent/{2}/os/{0}/ansible/{1}/'.format(
|
||||
profile, scriptcat, privacy)
|
||||
if not os.path.isdir(dirname):
|
||||
dirname = '/var/lib/confluent/{2}/os/{0}/ansible/{1}.d/'.format(
|
||||
profile, scriptcat, privacy)
|
||||
for filename in slist:
|
||||
if filename.endswith('.yaml') or filename.endswith('.yml'):
|
||||
playlist.append(os.path.join(dirname, filename))
|
||||
playlist = list_ansible_scripts(cfg, nodename, scriptcat)
|
||||
if playlist:
|
||||
runansible.run_playbooks(playlist, [nodename])
|
||||
start_response('202 Queued', ())
|
||||
@@ -590,8 +578,13 @@ def handle_request(env, start_response):
|
||||
yield 'No profile'
|
||||
return
|
||||
fname = '/var/lib/confluent/private/os/{}/{}'.format(profile, fname)
|
||||
fullpath = os.path.abspath(fname)
|
||||
if not fullpath.startswith('/var/lib/confluent/private/os/{}/'.format(profile)):
|
||||
start_response('400 Bad Request', ())
|
||||
yield 'Bad Request'
|
||||
return
|
||||
try:
|
||||
with open(fname, 'rb') as privdata:
|
||||
with open(fullpath, 'rb') as privdata:
|
||||
start_response('200 OK', ())
|
||||
yield privdata.read()
|
||||
return
|
||||
@@ -603,6 +596,22 @@ def handle_request(env, start_response):
|
||||
start_response('404 Not Found', ())
|
||||
yield 'Not found'
|
||||
|
||||
def list_ansible_scripts(cfg, nodename, scriptcat):
|
||||
playlist = []
|
||||
for privacy in ('public', 'private'):
|
||||
slist, profile = get_scriptlist(
|
||||
scriptcat, cfg, nodename,
|
||||
'/var/lib/confluent/{0}/os/{{0}}/ansible/{{1}}'.format(privacy))
|
||||
dirname = '/var/lib/confluent/{2}/os/{0}/ansible/{1}/'.format(
|
||||
profile, scriptcat, privacy)
|
||||
if not os.path.isdir(dirname):
|
||||
dirname = '/var/lib/confluent/{2}/os/{0}/ansible/{1}.d/'.format(
|
||||
profile, scriptcat, privacy)
|
||||
for filename in slist:
|
||||
if filename.endswith('.yaml') or filename.endswith('.yml'):
|
||||
playlist.append(os.path.join(dirname, filename))
|
||||
return playlist
|
||||
|
||||
def get_scriptlist(scriptcat, cfg, nodename, pathtemplate):
|
||||
if '..' in scriptcat:
|
||||
return None, None
|
||||
@@ -619,6 +628,11 @@ def get_scriptlist(scriptcat, cfg, nodename, pathtemplate):
|
||||
'deployment.profile', {}).get('value', '')
|
||||
slist = []
|
||||
target = pathtemplate.format(profile, scriptcat)
|
||||
target = os.path.abspath(target)
|
||||
allowedbase = os.path.abspath(pathtemplate.format(profile, '').rstrip('/'))
|
||||
allowedbaseprefix = os.path.join(allowedbase, '')
|
||||
if not target.startswith(allowedbaseprefix):
|
||||
return None, None
|
||||
if not os.path.isdir(target) and os.path.isdir(target + '.d'):
|
||||
target = target + '.d'
|
||||
try:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2016 Lenovo
|
||||
# Copyright 2016-2025 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -26,21 +26,59 @@ import eventlet
|
||||
from eventlet.support.greendns import getaddrinfo
|
||||
import pysnmp.smi.error as snmperr
|
||||
import socket
|
||||
import asyncio
|
||||
snmp = eventlet.import_patched('pysnmp.hlapi')
|
||||
asyn = False
|
||||
if not hasattr(snmp, 'UsmUserData'):
|
||||
# pysnmp that dropped the sync support
|
||||
import pysnmp.hlapi.v3arch.asyncio as snmp
|
||||
asyn = True
|
||||
import pysnmp.smi.rfc1902 as rfc1902
|
||||
|
||||
|
||||
def get_loop():
|
||||
try:
|
||||
return asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
return loop
|
||||
|
||||
def _run_coro(coro):
|
||||
loop = get_loop()
|
||||
fun = asyncio.wait_for(coro, None)
|
||||
if loop.is_running():
|
||||
task = asyncio.ensure_future(fun)
|
||||
return loop.run_until_complete(task)
|
||||
return loop.run_until_complete(fun)
|
||||
|
||||
async def _agen_to_list(agen):
|
||||
out = []
|
||||
async for item in agen:
|
||||
out.append(item)
|
||||
return out
|
||||
|
||||
def _sync_gen(agen):
|
||||
return _run_coro(_agen_to_list(agen))
|
||||
|
||||
def _get_transport(name):
|
||||
# Annoyingly, pysnmp does not automatically determine ipv6 v ipv4
|
||||
res = getaddrinfo(name, 161, 0, socket.SOCK_DGRAM)
|
||||
if res[0][0] == socket.AF_INET6:
|
||||
return snmp.Udp6TransportTarget(res[0][4], 2)
|
||||
if asyn:
|
||||
return _run_coro(snmp.Udp6TransportTarget.create(res[0][4], 2))
|
||||
else:
|
||||
return snmp.Udp6TransportTarget(res[0][4], 2)
|
||||
else:
|
||||
return snmp.UdpTransportTarget(res[0][4], 2)
|
||||
if asyn:
|
||||
return _run_coro(snmp.UdpTransportTarget.create(res[0][4], 2))
|
||||
else:
|
||||
return snmp.UdpTransportTarget(res[0][4], 2)
|
||||
|
||||
|
||||
class Session(object):
|
||||
|
||||
def __init__(self, server, secret, username=None, context=None):
|
||||
def __init__(self, server, secret, username=None, context=None, privacy_protocol=None):
|
||||
"""Create a new session to interrogate a switch
|
||||
|
||||
If username is not given, it is assumed that
|
||||
@@ -59,9 +97,17 @@ class Session(object):
|
||||
# SNMP v2c
|
||||
self.authdata = snmp.CommunityData(secret, mpModel=1)
|
||||
else:
|
||||
if privacy_protocol == 'aes':
|
||||
privproto = snmp.usmAesCfb128Protocol
|
||||
elif privacy_protocol in ('des', None):
|
||||
privproto = snmp.usmDESPrivProtocol
|
||||
else:
|
||||
raise exc.ConfluentException('Unsupported SNMPv3 privacy protocol '
|
||||
'{0}'.format(privacy_protocol))
|
||||
self.authdata = snmp.UsmUserData(
|
||||
username, authKey=secret, privKey=secret,
|
||||
authProtocol=snmp.usmHMACSHAAuthProtocol)
|
||||
authProtocol=snmp.usmHMACSHAAuthProtocol,
|
||||
privProtocol=privproto)
|
||||
self.eng = snmp.SnmpEngine()
|
||||
|
||||
def walk(self, oid):
|
||||
@@ -83,12 +129,22 @@ class Session(object):
|
||||
if '::' in oid:
|
||||
resolvemib = True
|
||||
mib, field = oid.split('::')
|
||||
obj = snmp.ObjectType(snmp.ObjectIdentity(mib, field))
|
||||
if asyn:
|
||||
obj = rfc1902.ObjectType(rfc1902.ObjectIdentity(mib, field))
|
||||
else:
|
||||
obj = snmp.ObjectType(snmp.ObjectIdentity(mib, field))
|
||||
else:
|
||||
obj = snmp.ObjectType(snmp.ObjectIdentity(oid))
|
||||
|
||||
walking = snmp.bulkCmd(self.eng, self.authdata, tp, ctx, 0, 10, obj,
|
||||
lexicographicMode=False, lookupMib=resolvemib)
|
||||
if asyn:
|
||||
obj = rfc1902.ObjectType(rfc1902.ObjectIdentity(oid))
|
||||
else:
|
||||
obj = snmp.ObjectType(snmp.ObjectIdentity(oid))
|
||||
if asyn:
|
||||
walking = snmp.bulk_walk_cmd(self.eng, self.authdata, tp, ctx, 0, 10, obj,
|
||||
lexicographicMode=False, lookupMib=resolvemib)
|
||||
walking = _sync_gen(walking)
|
||||
else:
|
||||
walking = snmp.bulkCmd(self.eng, self.authdata, tp, ctx, 0, 10, obj,
|
||||
lexicographicMode=False, lookupMib=resolvemib)
|
||||
try:
|
||||
for rsp in walking:
|
||||
errstr, errnum, erridx, answers = rsp
|
||||
|
||||
@@ -26,6 +26,7 @@ import ctypes.util
|
||||
import errno
|
||||
import os
|
||||
import pwd
|
||||
import shutil
|
||||
import stat
|
||||
import struct
|
||||
import sys
|
||||
@@ -458,7 +459,7 @@ def removesocket():
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _unixdomainhandler():
|
||||
def _unixdomainhandler(bind_group=None, bind_perms=None):
|
||||
unixsocket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
try:
|
||||
os.remove("/var/run/confluent/api.sock")
|
||||
@@ -466,10 +467,12 @@ def _unixdomainhandler():
|
||||
pass
|
||||
if not os.path.isdir("/var/run/confluent"):
|
||||
os.makedirs('/var/run/confluent', 0o755)
|
||||
oldumask = os.umask(0o777 - bind_perms)
|
||||
unixsocket.bind("/var/run/confluent/api.sock")
|
||||
os.chmod("/var/run/confluent/api.sock",
|
||||
stat.S_IWOTH | stat.S_IROTH | stat.S_IWGRP |
|
||||
stat.S_IRGRP | stat.S_IWUSR | stat.S_IRUSR)
|
||||
os.chmod("/var/run/confluent/api.sock", bind_perms)
|
||||
if bind_group:
|
||||
shutil.chown("/var/run/confluent/api.sock", group=bind_group)
|
||||
os.umask(oldumask)
|
||||
atexit.register(removesocket)
|
||||
unixsocket.listen(5)
|
||||
while True:
|
||||
@@ -498,11 +501,13 @@ def _unixdomainhandler():
|
||||
|
||||
|
||||
class SockApi(object):
|
||||
def __init__(self, bindhost=None, bindport=None):
|
||||
def __init__(self, bindhost=None, bindport=None, bind_group=None, bind_perms=None):
|
||||
self.tlsserver = None
|
||||
self.unixdomainserver = None
|
||||
self.bind_host = bindhost or '::'
|
||||
self.bind_port = bindport or 13001
|
||||
self.bind_group = bind_group
|
||||
self.bind_perms = bind_perms or 0o666
|
||||
|
||||
def start(self):
|
||||
global auditlog
|
||||
@@ -515,7 +520,7 @@ class SockApi(object):
|
||||
else:
|
||||
eventlet.spawn_n(self.watch_for_cert)
|
||||
eventlet.spawn_n(self.watch_resolv)
|
||||
self.unixdomainserver = eventlet.spawn(_unixdomainhandler)
|
||||
self.unixdomainserver = eventlet.spawn(_unixdomainhandler, self.bind_group, self.bind_perms)
|
||||
|
||||
def watch_resolv(self):
|
||||
while True:
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
import base64
|
||||
import confluent.exceptions as cexc
|
||||
import confluent.log as log
|
||||
import glob
|
||||
import hashlib
|
||||
import ipaddress
|
||||
try:
|
||||
import psutil
|
||||
except ImportError:
|
||||
@@ -31,6 +33,12 @@ import socket
|
||||
import ssl
|
||||
import struct
|
||||
import eventlet.green.subprocess as subprocess
|
||||
import cryptography.x509 as x509
|
||||
try:
|
||||
import cryptography.x509.verification as verification
|
||||
except ImportError:
|
||||
verification = None
|
||||
|
||||
|
||||
|
||||
def mkdirp(path, mode=0o777):
|
||||
@@ -86,6 +94,53 @@ def list_interface_indexes():
|
||||
return
|
||||
|
||||
|
||||
def get_bmc_subject_san(configmanager, nodename, addnames=()):
|
||||
bmc_san = []
|
||||
subject = ''
|
||||
ipas = set([])
|
||||
dnsnames = set([])
|
||||
for addname in addnames:
|
||||
try:
|
||||
addr = ipaddress.ip_address(addname)
|
||||
ipas.add(addname)
|
||||
except Exception:
|
||||
dnsnames.add(addname)
|
||||
nodecfg = configmanager.get_node_attributes(nodename,
|
||||
('dns.domain', 'hardwaremanagement.manager', 'hardwaremanagement.manager_tls_name'))
|
||||
bmcaddr = nodecfg.get(nodename, {}).get('hardwaremanagement.manager', {}).get('value', '')
|
||||
domain = nodecfg.get(nodename, {}).get('dns.domain', {}).get('value', '')
|
||||
isipv4 = False
|
||||
if bmcaddr:
|
||||
bmcaddr = bmcaddr.split('/', 1)[0]
|
||||
bmcaddr = bmcaddr.split('%', 1)[0]
|
||||
dnsnames.add(bmcaddr)
|
||||
subject = bmcaddr
|
||||
if ':' in bmcaddr:
|
||||
ipas.add(bmcaddr)
|
||||
dnsnames.add('{0}.ipv6-literal.net'.format(bmcaddr.replace(':', '-')))
|
||||
else:
|
||||
try:
|
||||
socket.inet_aton(bmcaddr)
|
||||
isipv4 = True
|
||||
ipas.add(bmcaddr)
|
||||
except socket.error:
|
||||
pass
|
||||
if not isipv4: # neither ipv6 nor ipv4, should be a name
|
||||
if domain and domain not in bmcaddr:
|
||||
dnsnames.add('{0}.{1}'.format(bmcaddr, domain))
|
||||
bmcname = nodecfg.get(nodename, {}).get('hardwaremanagement.manager_tls_name', {}).get('value', '')
|
||||
if bmcname:
|
||||
subject = bmcname
|
||||
dnsnames.add(bmcname)
|
||||
if domain and domain not in bmcname:
|
||||
dnsnames.add('{0}.{1}'.format(bmcname, domain))
|
||||
for dns in dnsnames:
|
||||
bmc_san.append('DNS:{0}'.format(dns))
|
||||
for ip in ipas:
|
||||
bmc_san.append('IP:{0}'.format(ip))
|
||||
return subject, ','.join(bmc_san)
|
||||
|
||||
|
||||
def list_ips():
|
||||
# Used for getting addresses to indicate the multicast address
|
||||
# as well as getting all the broadcast addresses
|
||||
@@ -184,23 +239,57 @@ def cert_matches(fingerprint, certificate):
|
||||
return newfp and fingerprint == newfp
|
||||
|
||||
|
||||
_polbuilder = None
|
||||
|
||||
|
||||
class TLSCertVerifier(object):
|
||||
def __init__(self, configmanager, node, fieldname):
|
||||
def __init__(self, configmanager, node, fieldname, subject=None):
|
||||
self.cfm = configmanager
|
||||
self.node = node
|
||||
self.fieldname = fieldname
|
||||
self.subject = subject
|
||||
|
||||
def verify_by_ca(self, certificate):
|
||||
global _polbuilder
|
||||
_polbuilder = None
|
||||
if not _polbuilder:
|
||||
certs = []
|
||||
for cert in glob.glob('/var/lib/confluent/public/site/tls/*.pem'):
|
||||
with open(cert, 'rb') as certfile:
|
||||
certs.extend(x509.load_pem_x509_certificates(certfile.read()))
|
||||
if not certs:
|
||||
return False
|
||||
castore = verification.Store(certs)
|
||||
_polbuilder = verification.PolicyBuilder()
|
||||
eep = verification.ExtensionPolicy.permit_all().require_present(
|
||||
x509.SubjectAlternativeName, verification.Criticality.AGNOSTIC, None).may_be_present(
|
||||
x509.KeyUsage, verification.Criticality.AGNOSTIC, None)
|
||||
cap = verification.ExtensionPolicy.webpki_defaults_ca().require_present(
|
||||
x509.BasicConstraints, verification.Criticality.AGNOSTIC, None).may_be_present(
|
||||
x509.KeyUsage, verification.Criticality.AGNOSTIC, None)
|
||||
_polbuilder = _polbuilder.store(castore).extension_policies(
|
||||
ee_policy=eep, ca_policy=cap)
|
||||
try:
|
||||
addr = ipaddress.ip_address(self.subject)
|
||||
subject = x509.IPAddress(addr)
|
||||
except ValueError:
|
||||
subject = x509.DNSName(self.subject)
|
||||
cert = x509.load_der_x509_certificate(certificate)
|
||||
_polbuilder.build_server_verifier(subject).verify(cert, [])
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def verify_cert(self, certificate):
|
||||
storedprint = self.cfm.get_node_attributes(self.node, (self.fieldname,)
|
||||
)
|
||||
if (self.fieldname not in storedprint[self.node] or
|
||||
storedprint[self.node][self.fieldname]['value'] == ''):
|
||||
storedprint = storedprint.get(self.node, {}).get(self.fieldname, {}).get('value', '')
|
||||
newpolicy = self.cfm.get_node_attributes(self.node,
|
||||
('pubkeys.addpolicy',))
|
||||
newpolicy = newpolicy.get(self.node, {}).get('pubkeys.addpolicy', {}).get('value', '')
|
||||
if not storedprint:
|
||||
# no stored value, check policy for next action
|
||||
newpolicy = self.cfm.get_node_attributes(self.node,
|
||||
('pubkeys.addpolicy',))
|
||||
if ('pubkeys.addpolicy' in newpolicy[self.node] and
|
||||
'value' in newpolicy[self.node]['pubkeys.addpolicy'] and
|
||||
newpolicy[self.node]['pubkeys.addpolicy']['value'] == 'manual'):
|
||||
if newpolicy == 'manual':
|
||||
# manual policy means always raise unless a match is set
|
||||
# manually
|
||||
fingerprint = get_fingerprint(certificate, 'sha256')
|
||||
@@ -209,17 +298,30 @@ class TLSCertVerifier(object):
|
||||
self.fieldname, 'newkey')
|
||||
# since the policy is not manual, go ahead and add new key
|
||||
# after logging to audit log
|
||||
fingerprint = get_fingerprint(certificate, 'sha256')
|
||||
auditlog = log.Logger('audit')
|
||||
auditlog.log({'node': self.node, 'event': 'certautoadd',
|
||||
'fingerprint': fingerprint})
|
||||
self.cfm.set_node_attributes(
|
||||
{self.node: {self.fieldname: fingerprint}})
|
||||
return True
|
||||
elif cert_matches(storedprint[self.node][self.fieldname]['value'],
|
||||
certificate):
|
||||
if newpolicy in ('tofu', ''):
|
||||
fingerprint = get_fingerprint(certificate, 'sha256')
|
||||
auditlog = log.Logger('audit')
|
||||
auditlog.log({'node': self.node, 'event': 'certautoadd',
|
||||
'fingerprint': fingerprint})
|
||||
self.cfm.set_node_attributes(
|
||||
{self.node: {self.fieldname: fingerprint}})
|
||||
return True
|
||||
elif cert_matches(storedprint, certificate) and newpolicy != 'ca-only':
|
||||
return True
|
||||
fingerprint = get_fingerprint(certificate, 'sha256')
|
||||
# No pinned certificate match, try to validate by CA if possible
|
||||
if self.subject:
|
||||
try:
|
||||
if verification and self.verify_by_ca(certificate):
|
||||
auditlog = log.Logger('audit')
|
||||
auditlog.log({'node': self.node, 'event': 'certautoupdate',
|
||||
'fingerprint': fingerprint})
|
||||
self.cfm.set_node_attributes(
|
||||
{self.node: {self.fieldname: fingerprint}})
|
||||
return True
|
||||
except Exception:
|
||||
if newpolicy == 'ca-only':
|
||||
raise
|
||||
raise cexc.PubkeyInvalid(
|
||||
'Mismatched certificate detected', certificate, fingerprint,
|
||||
self.fieldname, 'mismatch')
|
||||
|
||||
@@ -25,7 +25,7 @@ Requires: python-pyghmi >= 1.5.71, python-eventlet, python-greenlet, python-pycr
|
||||
Requires: python3-pyghmi >= 1.5.71, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-enum34, python3-asn1crypto, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute
|
||||
%else
|
||||
%if "%{dist}" == ".el9"
|
||||
Requires: python3-pyghmi >= 1.5.71, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-webauthn, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute
|
||||
Requires: python3-pyghmi >= 1.5.71, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-webauthn, python3-netifaces, python3-pyasn1 >= 0.2.3, ((python3-pysnmp >= 4.3.4 and python3-pysnmp < 5.0.0) or python3-pysnmp >= 7.1.21), python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute
|
||||
%else
|
||||
%if "%{dist}" == ".el10"
|
||||
Requires: python3-pyghmi >= 1.5.71, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-webauthn, python3-psutil, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user