mirror of
https://github.com/xcat2/confluent.git
synced 2026-09-29 08:41:00 +00:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -1,127 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
shellcheck:
|
||||
name: ShellCheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Run ShellCheck (errors only)
|
||||
# Check every tracked file that has a .sh extension or an sh/bash
|
||||
# shebang. SC2148 (missing shebang) is excluded because many .sh
|
||||
# files are sourced fragments or dracut hooks; ShellCheck then
|
||||
# falls back to checking them as bash.
|
||||
run: |
|
||||
shebang_re='^#!.*[/ ](sh|bash|dash|ash|ksh)([[:blank:]]|$)'
|
||||
{
|
||||
git ls-files '*.sh'
|
||||
git ls-files | while IFS= read -r f; do
|
||||
[ -f "$f" ] || continue
|
||||
firstline=
|
||||
# An empty file makes read fail, which under -e would end the run.
|
||||
IFS= read -r -n 200 firstline < "$f" 2>/dev/null || true
|
||||
if [[ $firstline =~ $shebang_re ]]; then
|
||||
printf '%s\n' "$f"
|
||||
fi
|
||||
done
|
||||
} | sort -u > /tmp/shfiles
|
||||
# A selection that quietly comes up empty would check nothing and
|
||||
# still pass, so say how many files there are and insist on some.
|
||||
echo "$(wc -l < /tmp/shfiles) shell files"
|
||||
[ -s /tmp/shfiles ] || { echo '::error::No shell files found'; exit 1; }
|
||||
xargs -d '\n' shellcheck --severity=error --exclude=SC2148 < /tmp/shfiles
|
||||
|
||||
ruff:
|
||||
name: Ruff
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
# Pinned to an exact release: unlike actions/checkout, ruff-action
|
||||
# publishes no moving major tag past v3, so @v4 does not resolve.
|
||||
- uses: astral-sh/ruff-action@v4.1.0
|
||||
with:
|
||||
version: latest
|
||||
# Rule selection, file discovery (the many extensionless Python
|
||||
# executables) and exclusions all live in ruff.toml, so the whole
|
||||
# workspace can be handed over as-is.
|
||||
args: check --output-format=github
|
||||
|
||||
pyrefly:
|
||||
name: Pyrefly
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: facebook/pyrefly@main
|
||||
|
||||
python-compileall:
|
||||
name: Python compileall
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# One entry per Python version shipped by the distros confluent
|
||||
# targets, limited to versions actions/setup-python still provides
|
||||
# on current runners (sles15/alma8 ship 3.6, which is unavailable).
|
||||
# Newline-separated so it feeds both setup-python (multiline input)
|
||||
# and the shell loop below (word-split on whitespace).
|
||||
PYTHON_VERSIONS: |
|
||||
3.8
|
||||
3.9
|
||||
3.10
|
||||
3.12
|
||||
3.13
|
||||
3.14
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSIONS }}
|
||||
- name: List the Python files without a .py name
|
||||
# compileall only ever compiles *.py: handed anything else, even by
|
||||
# name, it skips it and still exits 0. That leaves every extensionless
|
||||
# CLI tool, deploy script and setup.py.tmpl unchecked, so collect them
|
||||
# here and feed them to py_compile, which does compile what it is
|
||||
# given. The first line is read with the shell builtin rather than
|
||||
# forking head and grep per file.
|
||||
run: |
|
||||
shebang_re='^#!.*python'
|
||||
{
|
||||
git ls-files | while IFS= read -r f; do
|
||||
[ -f "$f" ] || continue
|
||||
case "$f" in *.py) continue ;; esac
|
||||
firstline=
|
||||
# An empty file makes read fail, which under -e would end the run.
|
||||
IFS= read -r -n 200 firstline < "$f" 2>/dev/null || true
|
||||
if [[ $firstline =~ $shebang_re ]]; then
|
||||
printf '%s\n' "$f"
|
||||
fi
|
||||
done
|
||||
# Python that carries no shebang at all, so nothing can detect it.
|
||||
# Kept in step with extend-include in ruff.toml.
|
||||
git ls-files '*/setup.py.tmpl' '*/scripts/configbmc' \
|
||||
'*/scripts/add_local_repositories' 'misc/filterpasswd'
|
||||
} | sort -u > /tmp/pyfiles
|
||||
# An empty list would leave py_compile with nothing to do and the
|
||||
# job green, which is the very hole this step exists to close.
|
||||
echo "$(wc -l < /tmp/pyfiles) files without a .py name"
|
||||
[ -s /tmp/pyfiles ] || { echo '::error::No such files found'; exit 1; }
|
||||
- name: Compile all Python files
|
||||
run: |
|
||||
rc=0
|
||||
for v in $PYTHON_VERSIONS; do
|
||||
echo "::group::Python $v"
|
||||
ok=0
|
||||
"python$v" -W error -m compileall -q -x '/\.git/' . || ok=1
|
||||
xargs -d '\n' "python$v" -W error -m py_compile < /tmp/pyfiles || ok=1
|
||||
echo "::endgroup::"
|
||||
if [ "$ok" -ne 0 ]; then
|
||||
echo "::error::Python $v compileall failed"
|
||||
rc=1
|
||||
fi
|
||||
done
|
||||
exit "$rc"
|
||||
@@ -1,19 +0,0 @@
|
||||
name: Create Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
generate_release_notes: true
|
||||
@@ -5,10 +5,11 @@
|
||||
Confluent is a software package to handle essential bootstrap and operation of scale-out server configurations.
|
||||
It supports stateful and stateless deployments for various operating systems.
|
||||
|
||||
Check [this page](https://xcat2.github.io/confluent-docs/) for a more detailed list of features.
|
||||
Check [this page](https://hpc.lenovo.com/users/documentation/whatisconfluent.html
|
||||
) for a more detailed list of features.
|
||||
|
||||
Confluent is the modern successor of [xCAT](https://github.com/xcat2/xcat-core).
|
||||
If you're coming from xCAT, check out [this comparison](https://xcat2.github.io/confluent-docs/miscellaneous/confluentvxcat/).
|
||||
If you're coming from xCAT, check out [this comparison](https://hpc.lenovo.com/users/documentation/confluentvxcat.html).
|
||||
|
||||
# Documentation
|
||||
|
||||
@@ -16,9 +17,9 @@ Confluent documentation is hosted on: https://xcat2.github.io/confluent-docs/
|
||||
|
||||
# Download
|
||||
|
||||
Get the latest version from: https://xcat2.github.io/confluent-docs/downloads/
|
||||
Get the latest version from: https://hpc.lenovo.com/users/downloads/
|
||||
|
||||
Check release notes on: https://xcat2.github.io/confluent-docs/release_notes/
|
||||
Check release notes on: https://hpc.lenovo.com/users/news/
|
||||
|
||||
# Open Source License
|
||||
|
||||
@@ -27,24 +28,3 @@ Confluent is made available under the Apache 2.0 license: https://opensource.org
|
||||
# Developers
|
||||
|
||||
Want to help? Submit a [Pull Request](https://github.com/xcat2/confluent/pulls).
|
||||
|
||||
## Versioning and releases
|
||||
|
||||
The top-level `VERSION` file names the release the current branch is working toward. Build scripts
|
||||
call `./mkversion`, which turns it into the version stamped on packages: the tag itself on a release
|
||||
tag (`4.0.1`), otherwise a development version (`4.0.1.dev5+gdeadbee`, or `4.0.1~dev5+gdeadbee` for
|
||||
the packages that have no `setup.py`).
|
||||
|
||||
`mkversion` also derives a version from the newest tag reachable from HEAD and uses whichever is
|
||||
higher, so forgetting to bump `VERSION` after tagging the current branch cannot walk the version
|
||||
backwards. Staying ahead of tags on *other* branches is what `VERSION` itself is for: patch releases
|
||||
are tagged on release branches, which master never sees.
|
||||
|
||||
Cutting a new series:
|
||||
|
||||
1. Land the release commit on master and tag `X.Y.0` there.
|
||||
2. Create branch `X.Y` from that tag; it inherits `VERSION=X.Y.0`.
|
||||
3. Only then bump `VERSION` on master. Doing it before step 2 leaves the release branch on the
|
||||
wrong series.
|
||||
|
||||
Patch releases land on branch `X.Y` and are tagged `X.Y.Z` there; no `VERSION` edit is needed.
|
||||
|
||||
@@ -3,7 +3,7 @@ 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-netifaces", "dh-python", "libjson-perl", "pandoc", "alien", "gcc", "make", "alien"]
|
||||
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/"]
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
FROM ubuntu:resolute
|
||||
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-netifaces", "python3-asyncssh", "dh-python", "libjson-perl", "pandoc", "alien", "gcc", "make"]
|
||||
RUN ["mkdir", "-p", "/sources/git/"]
|
||||
RUN ["mkdir", "-p", "/debs/"]
|
||||
RUN ["mkdir", "-p", "/apt/"]
|
||||
CMD ["/bin/bash", "/bin/buildapt.sh"]
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
#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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
Origin: Lenovo HPC Packages
|
||||
Label: Lenovo HPC Packages
|
||||
Codename: #CODENAME#
|
||||
Architectures: amd64 source
|
||||
Components: main
|
||||
Description: Lenovo HPC Packages
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
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)
|
||||
|
||||
@@ -24,7 +24,6 @@ import os
|
||||
import re
|
||||
import select
|
||||
import sys
|
||||
import csv
|
||||
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
@@ -52,8 +51,6 @@ argparser.add_option('-s', '--skipcommon', action='store_true',
|
||||
'groups, useful when combined with -d')
|
||||
argparser.add_option('-c', '--count', action='store_true',
|
||||
help='Also display count of nodes in a given group')
|
||||
argparser.add_option('-t', '--tabular', action='store_true',
|
||||
help='Output in CSV format')
|
||||
argparser.add_option('-r', '--reverse', action='store_true',
|
||||
help='Reverse sort order to show biggest output group '
|
||||
'last')
|
||||
@@ -71,30 +68,13 @@ if options.abbreviate:
|
||||
else:
|
||||
grouped = tg.GroupedData()
|
||||
|
||||
def print_current(tabular=False):
|
||||
def print_current():
|
||||
if options.diff:
|
||||
grouped.print_deviants(skipmodal=options.skipcommon, count=options.count,
|
||||
reverse=options.reverse, basenode=options.base)
|
||||
elif options.groupcount:
|
||||
grouped.generate_byoutput()
|
||||
print(len(grouped.byoutput))
|
||||
elif tabular:
|
||||
databynode = {}
|
||||
fields = ['node']
|
||||
for n in grouped.bynode:
|
||||
for ln in grouped.bynode[n]:
|
||||
label, value = ln.split(':', 1)
|
||||
label = label.strip()
|
||||
value = value.strip()
|
||||
databynode.setdefault(n, {})[label] = value
|
||||
if label not in fields:
|
||||
fields.append(label)
|
||||
writer = csv.DictWriter(sys.stdout, fieldnames=fields)
|
||||
writer.writeheader()
|
||||
for n in databynode:
|
||||
row = dict(databynode[n])
|
||||
row['node'] = n
|
||||
writer.writerow(row)
|
||||
else:
|
||||
grouped.print_all(skipmodal=options.skipcommon,
|
||||
count=options.count,
|
||||
@@ -145,5 +125,5 @@ while fullline:
|
||||
if printpending:
|
||||
if clearpending:
|
||||
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
|
||||
print_current(options.tabular)
|
||||
print_current()
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
# esc-( would interfere with normal esc use too much
|
||||
# ~ I will not use for now...
|
||||
|
||||
import math
|
||||
import getpass
|
||||
import optparse
|
||||
import os
|
||||
@@ -107,8 +108,8 @@ class BailOut(Exception):
|
||||
def print_help():
|
||||
print("confetty provides a filesystem like interface to confluent. "
|
||||
"Navigation is done using the same commands as would be used in a "
|
||||
"filesystem. Tab completion is supported to aid in navigation, "
|
||||
"as is up arrow to recall previous commands and control-r to search "
|
||||
"filesystem. Tab completion is supported to aid in navigation,"
|
||||
"as is up arrow to recall previous commands and control-r to search"
|
||||
"previous command history, similar to using bash\n\n"
|
||||
"The supported commands are:\n"
|
||||
"cd [location] - Set the current command context, similar to a "
|
||||
@@ -130,15 +131,6 @@ def print_help():
|
||||
#common with the api document
|
||||
|
||||
|
||||
def writeout(data):
|
||||
while True:
|
||||
try:
|
||||
select.select((), (sys.stdout,), ())
|
||||
sys.stdout.write(data)
|
||||
break
|
||||
except BlockingIOError:
|
||||
continue
|
||||
|
||||
def updatestatus(stateinfo={}):
|
||||
global powerstate, powertime, clearpowermessage
|
||||
if opts.headless:
|
||||
@@ -160,10 +152,10 @@ def updatestatus(stateinfo={}):
|
||||
if 'state' in stateinfo: # currently only read power means anything
|
||||
newpowerstate = stateinfo['state']['value']
|
||||
if newpowerstate != powerstate and newpowerstate == 'off':
|
||||
writeout("\x1b[2J\x1b[;H[powered off]\r\n")
|
||||
sys.stdout.write("\x1b[2J\x1b[;H[powered off]\r\n")
|
||||
clearpowermessage = True
|
||||
if newpowerstate == 'on' and clearpowermessage:
|
||||
writeout("\x1b[2J\x1b[;H")
|
||||
sys.stdout.write("\x1b[2J\x1b[;H")
|
||||
clearpowermessage = False
|
||||
powerstate = newpowerstate
|
||||
if 'clientcount' in laststate and laststate['clientcount'] != 1:
|
||||
@@ -181,7 +173,7 @@ def updatestatus(stateinfo={}):
|
||||
if info:
|
||||
status += ' [' + ','.join(info) + ']'
|
||||
if os.environ.get('TERM', '') not in ('linux'):
|
||||
writeout('\x1b]0;console: %s\x07' % status)
|
||||
sys.stdout.write('\x1b]0;console: %s\x07' % status)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
@@ -461,14 +453,14 @@ def do_command(command, server):
|
||||
print_result(res)
|
||||
elif argv[0] == 'start':
|
||||
targpath = fullpath_target(argv[1])
|
||||
nodename = targpath.split('/')[2]
|
||||
nodename = targpath.split('/')[-3]
|
||||
currconsole = targpath
|
||||
startrequest = {'operation': 'start', 'path': targpath,
|
||||
'parameters': {}}
|
||||
height, width = 31, 100
|
||||
if not opts.headless:
|
||||
height, width = struct.unpack(
|
||||
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'........')[:4])[:2]
|
||||
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'....'))[:2]
|
||||
startrequest['parameters']['width'] = width
|
||||
startrequest['parameters']['height'] = height
|
||||
for param in argv[2:]:
|
||||
@@ -624,7 +616,7 @@ def do_resize(a, b):
|
||||
if not inconsole:
|
||||
return
|
||||
height, width = struct.unpack(
|
||||
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'........')[:4])[:2]
|
||||
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'....'))[:2]
|
||||
tlvdata.send(session.connection, {'operation': 'resize', 'width': width,
|
||||
'height': height})
|
||||
|
||||
@@ -673,10 +665,9 @@ def get_session_node(shellargs):
|
||||
return targ, shellargs[0]
|
||||
if len(shellargs) == 2 and shellargs[0] == 'start':
|
||||
args = [s for s in shellargs[1].split('/') if s]
|
||||
if len(args) == 4 and args[0] == 'nodes':
|
||||
if args[2] == 'console' and \
|
||||
args[3] == 'session':
|
||||
return shellargs[1], args[1]
|
||||
if len(args) == 4 and args[0] == 'nodes' and args[2] == 'console' and \
|
||||
args[3] == 'session':
|
||||
return shellargs[1], args[1]
|
||||
if len(args) == 5 and args[0] == 'nodes' and args[2] == 'shell' and \
|
||||
args[3] == 'sessions':
|
||||
return shellargs[1], args[1]
|
||||
@@ -1042,7 +1033,7 @@ def main():
|
||||
except IOError:
|
||||
pass
|
||||
if powerstate is None or powertime < time.time() - 10: # Check powerstate every 10 seconds
|
||||
if powerstate is None:
|
||||
if powerstate == None:
|
||||
powerstate = True
|
||||
powertime = time.time()
|
||||
check_power_state()
|
||||
@@ -1205,21 +1196,14 @@ def consume_termdata(fh, bufferonly=False):
|
||||
clearpowermessage = False
|
||||
if bufferonly:
|
||||
return data
|
||||
data = data.encode('utf8')
|
||||
written = False
|
||||
while not written:
|
||||
select.select((), (sys.stdout,), ())
|
||||
try:
|
||||
sys.stdout.buffer.write(data)
|
||||
written = True
|
||||
except BlockingIOError:
|
||||
continue
|
||||
except IOError: # Some times circumstances are bad
|
||||
# resort to byte at a time...
|
||||
raise
|
||||
for d in data:
|
||||
sys.stdout.write(d)
|
||||
written = True
|
||||
try:
|
||||
sys.stdout.write(data)
|
||||
except UnicodeEncodeError:
|
||||
sys.stdout.buffer.write(data.encode('utf8'))
|
||||
except IOError: # Some times circumstances are bad
|
||||
# resort to byte at a time...
|
||||
for d in data:
|
||||
sys.stdout.write(d)
|
||||
now = time.time()
|
||||
if ('showtime' not in laststate or
|
||||
(now // 60) != laststate['showtime'] // 60):
|
||||
|
||||
@@ -1,567 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
# Generate a dnsmasq static DHCP configuration (a file under /etc/dnsmasq.d)
|
||||
# from the confluent attribute database. One "dhcp-host=" reservation is
|
||||
# emitted for every network of every node that has a hardware (MAC) address
|
||||
# defined (net.hwaddr, net.<iface>.hwaddr, net.bmc.hwaddr, ...).
|
||||
|
||||
import argparse
|
||||
import ipaddress
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
TARGET = '/etc/dnsmasq.d/confluent-dhcp.conf'
|
||||
TAGPREFIX = 'cdhcp'
|
||||
# Invocation flags that do not affect the generated content; excluded from the
|
||||
# recorded "Regenerate:" line and from the settings-changed comparison.
|
||||
NONCONFIG_ARGS = {'-y', '--yes', '-n', '--stdout'}
|
||||
|
||||
|
||||
def split_list(val):
|
||||
"""Split a confluent comma/whitespace delimited attribute value."""
|
||||
if not val:
|
||||
return []
|
||||
return [v for v in re.split(r'[\s,]+', val.strip()) if v]
|
||||
|
||||
|
||||
def config_args(argv):
|
||||
"""Join the invocation arguments that affect the generated content."""
|
||||
return ' '.join(a for a in argv if a not in NONCONFIG_ARGS)
|
||||
|
||||
|
||||
def parse_net_attrib(attrib):
|
||||
"""Split a net.* attribute into (network_name, field).
|
||||
|
||||
The field is always the final dotted component; everything between
|
||||
'net.' and the field is the network name, which may itself span
|
||||
several dotted components.
|
||||
"""
|
||||
parts = attrib.split('.')
|
||||
field = parts[-1]
|
||||
if len(parts) == 2:
|
||||
currnet = None
|
||||
else:
|
||||
currnet = '.'.join(parts[1:-1])
|
||||
return currnet, field
|
||||
|
||||
|
||||
def subnet_sort_key(netaddr):
|
||||
try:
|
||||
return tuple(int(o) for o in netaddr.split('.'))
|
||||
except ValueError:
|
||||
return (0, 0, 0, 0)
|
||||
|
||||
|
||||
def parse_routes():
|
||||
"""Parse `ip --json route` into a list of (network, route_dict) tuples for
|
||||
routes with a concrete destination prefix (default routes and the like are
|
||||
skipped). Returns [] if iproute2 or its JSON output is unavailable."""
|
||||
try:
|
||||
out = subprocess.check_output(['ip', '--json', 'route'],
|
||||
stderr=subprocess.DEVNULL)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return []
|
||||
try:
|
||||
entries = json.loads(out.decode('utf-8', 'replace'))
|
||||
except ValueError:
|
||||
return []
|
||||
routes = []
|
||||
for route in entries:
|
||||
dst = route.get('dst')
|
||||
if not dst or dst == 'default':
|
||||
continue
|
||||
if '/' not in dst:
|
||||
dst += '/32'
|
||||
try:
|
||||
net = ipaddress.ip_network(dst, strict=False)
|
||||
except ValueError:
|
||||
continue
|
||||
routes.append((net, route))
|
||||
return routes
|
||||
|
||||
|
||||
def connected_network_for_ip(routes, ipstr):
|
||||
"""Most specific directly-attached (prefsrc-bearing) route network that
|
||||
contains `ipstr`, or None. Used to fill in a missing prefix length."""
|
||||
try:
|
||||
ip = ipaddress.ip_address(ipstr)
|
||||
except ValueError:
|
||||
return None
|
||||
best = None
|
||||
for net, route in routes:
|
||||
if 'prefsrc' not in route:
|
||||
continue
|
||||
if ip in net and (best is None or net.prefixlen > best.prefixlen):
|
||||
best = net
|
||||
return best
|
||||
|
||||
|
||||
def prefsrc_for_network(routes, network):
|
||||
"""This host's preferred source address on `network` per the routing table
|
||||
(the address dnsmasq should listen on for that subnet), or None."""
|
||||
fallback = None
|
||||
for net, route in routes:
|
||||
prefsrc = route.get('prefsrc')
|
||||
if not prefsrc:
|
||||
continue
|
||||
if net == network:
|
||||
return prefsrc
|
||||
if network.network_address in net:
|
||||
fallback = prefsrc
|
||||
return fallback
|
||||
|
||||
|
||||
def existing_regen_args(targetpath):
|
||||
"""Return the content arguments recorded in an existing config's
|
||||
'Regenerate:' header line, or None if not present/readable."""
|
||||
try:
|
||||
with open(targetpath) as cfg:
|
||||
for line in cfg:
|
||||
if line.startswith('# Regenerate:'):
|
||||
rest = line[len('# Regenerate:'):].strip()
|
||||
marker = 'confluent2dnsmasq'
|
||||
if rest.startswith(marker):
|
||||
return rest[len(marker):].strip()
|
||||
return rest
|
||||
if not line.startswith('#'):
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def warn_conflicts(hostentries):
|
||||
"""Warn about reservations that conflict with one another: the same
|
||||
hostname on different IPs, one IP reserved more than once (dnsmasq
|
||||
refuses to start on a duplicate dhcp-host IP), or one MAC reserved
|
||||
more than once. Diagnostics only; every entry is still emitted."""
|
||||
byname = {}
|
||||
byip = {}
|
||||
bymac = {}
|
||||
for e in hostentries:
|
||||
if e['hostname']:
|
||||
prev = byname.setdefault(e['hostname'], e)
|
||||
if prev is not e and prev['ip'] != e['ip']:
|
||||
sys.stderr.write(
|
||||
"WARNING: hostname '{0}' maps to both {1} ({2}) and "
|
||||
'{3} ({4})\n'.format(
|
||||
e['hostname'], prev['ip'], prev['node'],
|
||||
e['ip'], e['node']))
|
||||
prev = byip.setdefault(e['ip'], e)
|
||||
if prev is not e:
|
||||
sys.stderr.write(
|
||||
'WARNING: address {0} is reserved for both {1} ({2}) and '
|
||||
'{3} ({4}); dnsmasq refuses to start on a duplicate '
|
||||
'dhcp-host IP address\n'.format(
|
||||
e['ip'], ','.join(prev['macs']), prev['node'],
|
||||
','.join(e['macs']), e['node']))
|
||||
for mac in e['macs']:
|
||||
prev = bymac.setdefault(mac.lower(), e)
|
||||
if prev is not e:
|
||||
sys.stderr.write(
|
||||
'WARNING: MAC {0} is reserved for both {1} ({2}) and '
|
||||
'{3} ({4})\n'.format(
|
||||
mac, prev['ip'], prev['node'],
|
||||
e['ip'], e['node']))
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(
|
||||
description='Create /etc/dnsmasq.d static DHCP reservations for a '
|
||||
'noderange from the confluent database')
|
||||
ap.add_argument('noderange',
|
||||
help='Noderange to generate DHCP reservations for')
|
||||
ap.add_argument('--target', default=TARGET, metavar='PATH',
|
||||
help='Output configuration file (default: %(default)s).')
|
||||
ap.add_argument('--tag-prefix', default=TAGPREFIX, metavar='PREFIX',
|
||||
help='Prefix for generated dnsmasq tag names '
|
||||
'(default: %(default)s).')
|
||||
ap.add_argument('--no-bind-dynamic', dest='emit_bind_dynamic',
|
||||
action='store_false',
|
||||
help="Do not emit the 'bind-dynamic' line "
|
||||
'(it is emitted by default).')
|
||||
ap.add_argument('--no-listen-address', dest='detect_listen',
|
||||
action='store_false',
|
||||
help='Do not autodetect and emit a listen-address line for '
|
||||
"this host's address on each managed subnet. By "
|
||||
'default they are emitted together with localhost '
|
||||
'(127.0.0.1 and ::1).')
|
||||
ap.add_argument('--no-range', dest='emit_range', action='store_false',
|
||||
help='Do not emit dhcp-range lines (manage ranges '
|
||||
'elsewhere). Note: dnsmasq will not answer DHCP on a '
|
||||
'subnet that has no covering dhcp-range.')
|
||||
ap.add_argument('--lease', default='24h', metavar='TIME',
|
||||
help='Lease time for emitted dhcp-range lines '
|
||||
'(default: 24h; e.g. 1h, 24h, infinite). '
|
||||
'Pass an empty string to omit it.')
|
||||
ap.add_argument('-n', '--stdout', action='store_true',
|
||||
help='Write the configuration to stdout instead of '
|
||||
'writing the target file.')
|
||||
ap.add_argument('-y', '--yes', dest='assume_yes', action='store_true',
|
||||
help='Do not prompt for confirmation when an existing '
|
||||
'config was generated with different settings.')
|
||||
ap.add_argument('--allow-empty', action='store_true',
|
||||
help='Write the file even if no reservations were '
|
||||
'produced (default: refuse, to avoid clobbering an '
|
||||
'existing config on an empty/failed read).')
|
||||
|
||||
# Optional extra data injected into the DHCP reply, sourced from the
|
||||
# confluent database. All are OFF by default and scoped per-subnet.
|
||||
grp = ap.add_argument_group(
|
||||
'optional DHCP reply data (sourced from confluent, off by default)')
|
||||
grp.add_argument('--all-options', action='store_true',
|
||||
help='Enable every optional reply datum listed below.')
|
||||
grp.add_argument('--gateway', '--router', dest='opt_gateway',
|
||||
action='store_true',
|
||||
help='router, option 3, from net.<net>.ipv4_gateway')
|
||||
grp.add_argument('--dns', dest='opt_dns', action='store_true',
|
||||
help='dns-server, option 6, from dns.servers')
|
||||
grp.add_argument('--domain', dest='opt_domain', action='store_true',
|
||||
help='domain-name, option 15, from dns.domain')
|
||||
grp.add_argument('--ntp', dest='opt_ntp', action='store_true',
|
||||
help='ntp-server, option 42, from ntp.servers')
|
||||
grp.add_argument('--mtu', dest='opt_mtu', action='store_true',
|
||||
help='interface-mtu, option 26, from net.<net>.mtu')
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.all_options:
|
||||
args.opt_gateway = True
|
||||
args.opt_dns = True
|
||||
args.opt_domain = True
|
||||
args.opt_ntp = True
|
||||
args.opt_mtu = True
|
||||
|
||||
c = client.Command()
|
||||
|
||||
# Kernel routing table (ip --json route), used to fill in a missing prefix
|
||||
# length on a node address and to pick this host's listen-address (prefsrc)
|
||||
# for each managed subnet.
|
||||
routes = parse_routes()
|
||||
|
||||
# Node level attributes
|
||||
domainbynode = {}
|
||||
dnsbynode = {}
|
||||
ntpbynode = {}
|
||||
# Per (node, net) network attributes: nets[node][net] = {field: value}
|
||||
nets = {}
|
||||
|
||||
read_error = False
|
||||
for ent in c.read('/noderange/{0}/attributes/current'.format(
|
||||
args.noderange)):
|
||||
if 'error' in ent:
|
||||
sys.stderr.write(ent['error'] + '\n')
|
||||
read_error = True
|
||||
continue
|
||||
ent = ent.get('databynode', {})
|
||||
for node in ent:
|
||||
for attrib in ent[node]:
|
||||
val = ent[node][attrib]
|
||||
if not isinstance(val, dict):
|
||||
continue
|
||||
val = val.get('value', None)
|
||||
if val in (None, ''):
|
||||
continue
|
||||
if attrib == 'dns.domain':
|
||||
domainbynode[node] = val
|
||||
elif attrib == 'dns.servers':
|
||||
dnsbynode[node] = val
|
||||
elif attrib == 'ntp.servers':
|
||||
ntpbynode[node] = val
|
||||
elif attrib.startswith('net.'):
|
||||
currnet, field = parse_net_attrib(attrib)
|
||||
if field not in ('hwaddr', 'ipv4_address',
|
||||
'ipv4_gateway', 'hostname', 'mtu'):
|
||||
continue
|
||||
nets.setdefault(node, {}).setdefault(currnet, {})
|
||||
nets[node][currnet][field] = val
|
||||
|
||||
# Build reservation entries and aggregate per-subnet information.
|
||||
hostentries = [] # {subnetkey, macs, ip, hostname}
|
||||
subnets = {} # subnetkey (netaddr, prefixlen) -> aggregate dict
|
||||
|
||||
for node in sorted(nets):
|
||||
netnames = sorted(nets[node], key=lambda n: (n is not None, n or ''))
|
||||
for currnet in netnames:
|
||||
slot = nets[node][currnet]
|
||||
hwaddr = slot.get('hwaddr')
|
||||
if not hwaddr:
|
||||
continue
|
||||
label = currnet if currnet else '(primary)'
|
||||
netprefix = (currnet + '.') if currnet else ''
|
||||
addr = slot.get('ipv4_address')
|
||||
if not addr:
|
||||
sys.stderr.write(
|
||||
'{0}: net {1} has a hwaddr but no net.{2}ipv4_address; '
|
||||
'skipping\n'.format(node, label, netprefix))
|
||||
continue
|
||||
# If confluent has no prefix length on the address, borrow it from
|
||||
# the matching directly-attached route.
|
||||
if '/' not in addr:
|
||||
cnet = connected_network_for_ip(routes, addr)
|
||||
if cnet is not None:
|
||||
addr = '{0}/{1}'.format(addr, cnet.prefixlen)
|
||||
ip = addr.split('/', 1)[0]
|
||||
|
||||
# Reservation hostname: first token of the per-net hostname,
|
||||
# else the node name for the primary (unnamed) network, else
|
||||
# omit it (named net with no hostname).
|
||||
hostname = None
|
||||
if slot.get('hostname'):
|
||||
hostname = split_list(slot['hostname'])[0]
|
||||
elif currnet is None:
|
||||
hostname = node
|
||||
|
||||
# Derive the subnet (needed for dhcp-range and for scoping
|
||||
# the optional dhcp-option tags).
|
||||
subnetkey = None
|
||||
if '/' in addr:
|
||||
try:
|
||||
iface = ipaddress.ip_interface(addr)
|
||||
except ValueError as e:
|
||||
sys.stderr.write(
|
||||
'{0}: net {1} has invalid address {2} ({3}); '
|
||||
'skipping\n'.format(node, label, addr, e))
|
||||
continue
|
||||
network = iface.network
|
||||
subnetkey = (str(network.network_address), network.prefixlen)
|
||||
sub = subnets.get(subnetkey)
|
||||
if sub is None:
|
||||
tag = '{0}_{1}_{2}'.format(
|
||||
args.tag_prefix,
|
||||
str(network.network_address).replace('.', '_'),
|
||||
network.prefixlen)
|
||||
sub = {'netmask': str(network.netmask), 'tag': tag,
|
||||
'prefsrc': prefsrc_for_network(routes, network),
|
||||
'gateway': set(), 'dns': set(), 'domain': set(),
|
||||
'ntp': set(), 'mtu': set()}
|
||||
subnets[subnetkey] = sub
|
||||
if slot.get('ipv4_gateway'):
|
||||
sub['gateway'].add(slot['ipv4_gateway'])
|
||||
if node in dnsbynode:
|
||||
sub['dns'].add(dnsbynode[node])
|
||||
if node in domainbynode:
|
||||
sub['domain'].add(domainbynode[node])
|
||||
if node in ntpbynode:
|
||||
sub['ntp'].add(ntpbynode[node])
|
||||
if slot.get('mtu'):
|
||||
sub['mtu'].add(slot['mtu'])
|
||||
elif args.emit_range:
|
||||
sys.stderr.write(
|
||||
'{0}: net {1} address {2} has no /prefix; cannot derive a '
|
||||
'subnet for dhcp-range (add a prefix or use --no-range). '
|
||||
'Emitting the dhcp-host anyway.\n'.format(
|
||||
node, label, addr))
|
||||
|
||||
hostentries.append({'subnetkey': subnetkey,
|
||||
'macs': split_list(hwaddr),
|
||||
'ip': ip, 'hostname': hostname,
|
||||
'node': node})
|
||||
|
||||
warn_conflicts(hostentries)
|
||||
|
||||
# Resolve optional per-subnet options from the aggregated values.
|
||||
def resolve(values, what, subnetkey):
|
||||
if not values:
|
||||
return None
|
||||
if len(values) > 1:
|
||||
chosen = sorted(values)[0]
|
||||
sys.stderr.write(
|
||||
'subnet {0}/{1}: inconsistent {2} across nodes {3}; using '
|
||||
'{4}\n'.format(subnetkey[0], subnetkey[1], what,
|
||||
sorted(values), chosen))
|
||||
return chosen
|
||||
return next(iter(values))
|
||||
|
||||
optionlines = {} # subnetkey -> [ 'option:...,value', ... ]
|
||||
for subnetkey, sub in subnets.items():
|
||||
lines = []
|
||||
if args.opt_gateway:
|
||||
gw = resolve(sub['gateway'], 'net.ipv4_gateway', subnetkey)
|
||||
if gw:
|
||||
lines.append('option:router,{0}'.format(gw))
|
||||
if args.opt_dns:
|
||||
dv = resolve(sub['dns'], 'dns.servers', subnetkey)
|
||||
if dv:
|
||||
lines.append('option:dns-server,{0}'.format(
|
||||
','.join(split_list(dv))))
|
||||
if args.opt_domain:
|
||||
dom = resolve(sub['domain'], 'dns.domain', subnetkey)
|
||||
if dom:
|
||||
lines.append('option:domain-name,{0}'.format(dom))
|
||||
if args.opt_ntp:
|
||||
nv = resolve(sub['ntp'], 'ntp.servers', subnetkey)
|
||||
if nv:
|
||||
lines.append('option:ntp-server,{0}'.format(
|
||||
','.join(split_list(nv))))
|
||||
if args.opt_mtu:
|
||||
mv = resolve(sub['mtu'], 'net.mtu', subnetkey)
|
||||
if mv:
|
||||
lines.append('option:mtu,{0}'.format(mv))
|
||||
if lines:
|
||||
optionlines[subnetkey] = lines
|
||||
|
||||
orderedsubnets = sorted(subnets,
|
||||
key=lambda k: subnet_sort_key(k[0]) + (k[1],))
|
||||
|
||||
# Render.
|
||||
out = ['# Managed by confluent2dnsmasq -- DO NOT EDIT BY HAND.',
|
||||
'# Regenerate: confluent2dnsmasq {0}'.format(
|
||||
config_args(sys.argv[1:])),
|
||||
'# Generated {0}'.format(time.strftime('%Y-%m-%d %H:%M:%S %z')),
|
||||
'']
|
||||
if args.emit_bind_dynamic:
|
||||
out.append('# Allow confluent and dnsmasq to share the same network '
|
||||
'for DHCP')
|
||||
out.append('bind-dynamic')
|
||||
out.append('')
|
||||
|
||||
detected = []
|
||||
if args.detect_listen:
|
||||
for subnetkey in orderedsubnets:
|
||||
prefsrc = subnets[subnetkey].get('prefsrc')
|
||||
if prefsrc and prefsrc not in detected:
|
||||
detected.append(prefsrc)
|
||||
out.append('# Listen on localhost plus this host\'s address on each '
|
||||
'managed subnet,')
|
||||
out.append('# so dnsmasq serves these networks alongside confluent '
|
||||
'and bind-dynamic')
|
||||
out.append('# (a listen-address line also overrides local-service in '
|
||||
'dnsmasq.conf).')
|
||||
out.append('listen-address=127.0.0.1')
|
||||
out.append('listen-address=::1')
|
||||
for addr in detected:
|
||||
out.append('listen-address={0}'.format(addr))
|
||||
out.append('')
|
||||
|
||||
hosts_by_subnet = {}
|
||||
nosubnet_hosts = []
|
||||
for e in hostentries:
|
||||
fields = list(e['macs'])
|
||||
if e['subnetkey'] in optionlines:
|
||||
fields.append('set:{0}'.format(subnets[e['subnetkey']]['tag']))
|
||||
fields.append(e['ip'])
|
||||
if e['hostname']:
|
||||
fields.append(e['hostname'])
|
||||
line = 'dhcp-host={0}'.format(','.join(fields))
|
||||
if e['subnetkey'] is None:
|
||||
nosubnet_hosts.append(line)
|
||||
else:
|
||||
hosts_by_subnet.setdefault(e['subnetkey'], []).append(line)
|
||||
|
||||
for subnetkey in orderedsubnets:
|
||||
sub = subnets[subnetkey]
|
||||
netaddr, prefixlen = subnetkey
|
||||
out.append('# subnet {0}/{1}'.format(netaddr, prefixlen))
|
||||
if args.emit_range:
|
||||
rangefields = [netaddr, 'static', sub['netmask']]
|
||||
if args.lease:
|
||||
rangefields.append(args.lease)
|
||||
out.append('dhcp-range={0}'.format(','.join(rangefields)))
|
||||
for optline in optionlines.get(subnetkey, []):
|
||||
out.append('dhcp-option=tag:{0},{1}'.format(sub['tag'], optline))
|
||||
for line in sorted(hosts_by_subnet.get(subnetkey, [])):
|
||||
out.append(line)
|
||||
out.append('')
|
||||
|
||||
if nosubnet_hosts:
|
||||
out.append('# reservations with no derivable subnet (address had no '
|
||||
'/prefix)')
|
||||
out.extend(sorted(nosubnet_hosts))
|
||||
out.append('')
|
||||
|
||||
text = '\n'.join(out).rstrip('\n') + '\n'
|
||||
|
||||
# Warnings about configuration choices that affect coexistence with
|
||||
# confluent's own DHCP service.
|
||||
if args.detect_listen and subnets and not detected:
|
||||
sys.stderr.write(
|
||||
'WARNING: no local address was found on any managed subnet, so '
|
||||
'dnsmasq will only listen on localhost. Run this on the dnsmasq '
|
||||
'host, or set interface/listen-address manually.\n')
|
||||
if not args.detect_listen:
|
||||
sys.stderr.write(
|
||||
'WARNING: listen-address autodetection disabled. For dnsmasq to '
|
||||
'serve these networks together with confluent and bind-dynamic, '
|
||||
'set interface, except-interface or listen-address manually, or '
|
||||
'disable local-service=host in dnsmasq.conf.\n')
|
||||
if not args.emit_bind_dynamic:
|
||||
sys.stderr.write(
|
||||
'WARNING: bind-dynamic not emitted; this can conflict with '
|
||||
"confluent's DHCP unless bind-dynamic is set elsewhere in the "
|
||||
'dnsmasq configuration.\n')
|
||||
|
||||
if args.stdout:
|
||||
sys.stdout.write(text)
|
||||
return
|
||||
|
||||
if not hostentries and not args.allow_empty:
|
||||
sys.stderr.write(
|
||||
'No DHCP reservations were generated (no networks with both a '
|
||||
'hwaddr and an ipv4_address were found){0}. Refusing to overwrite '
|
||||
'{1}; pass --allow-empty to override.\n'.format(
|
||||
' and there were read errors' if read_error else '',
|
||||
args.target))
|
||||
sys.exit(1)
|
||||
|
||||
# If a config already exists that was generated with different
|
||||
# content-affecting settings, confirm before overwriting it.
|
||||
if os.path.exists(args.target):
|
||||
prev = existing_regen_args(args.target)
|
||||
cur = config_args(sys.argv[1:])
|
||||
if prev is not None and prev != cur:
|
||||
sys.stderr.write(
|
||||
'Existing {0} was generated with different settings:\n'
|
||||
' old: confluent2dnsmasq {1}\n'
|
||||
' new: confluent2dnsmasq {2}\n'.format(
|
||||
args.target, prev or '(no arguments)',
|
||||
cur or '(no arguments)'))
|
||||
if not args.assume_yes:
|
||||
if not sys.stdin.isatty():
|
||||
sys.stderr.write(
|
||||
'Refusing to regenerate with changed settings '
|
||||
'non-interactively; pass -y/--yes to confirm.\n')
|
||||
sys.exit(1)
|
||||
try:
|
||||
resp = input('Are you sure you want to regenerate using the new settings? [y/N] ')
|
||||
except EOFError:
|
||||
resp = ''
|
||||
if resp.strip().lower() not in ('y', 'yes'):
|
||||
sys.stderr.write(
|
||||
'Aborted; {0} left unchanged.\n'.format(args.target))
|
||||
sys.exit(1)
|
||||
|
||||
targetdir = os.path.dirname(args.target)
|
||||
targetname = os.path.basename(args.target)
|
||||
if targetdir and not os.path.isdir(targetdir):
|
||||
os.makedirs(targetdir, exist_ok=True)
|
||||
# Write via a hidden temp file in the same directory, then rename: the
|
||||
# rename is atomic, and the dot-prefix keeps a dnsmasq conf-dir from
|
||||
# loading the temp file if it scans mid-write.
|
||||
tmp = os.path.join(targetdir, '.' + targetname + '.tmp')
|
||||
with open(tmp, 'w') as cfg:
|
||||
cfg.write(text)
|
||||
os.replace(tmp, args.target)
|
||||
sys.stderr.write('Wrote {0} reservation(s) across {1} subnet(s) to {2}\n'
|
||||
.format(len(hostentries), len(subnets), args.target))
|
||||
sys.stderr.write('Be sure to restart dnsmasq for the changes to take effect\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -3,7 +3,6 @@ import argparse
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import shutil
|
||||
import sys
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
@@ -15,6 +14,7 @@ path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
import confluent.client as client
|
||||
import confluent.sortutil as sortutil
|
||||
|
||||
def partitionhostsline(line):
|
||||
comment = ''
|
||||
@@ -33,8 +33,6 @@ class HostMerger(object):
|
||||
self.byip = {}
|
||||
self.byname = {}
|
||||
self.byname6 = {}
|
||||
self.ipbyname = {}
|
||||
self.ipbyname6 = {}
|
||||
self.sourcelines = []
|
||||
self.targlines = []
|
||||
|
||||
@@ -56,21 +54,14 @@ class HostMerger(object):
|
||||
|
||||
def add_entry(self, ip, names):
|
||||
targ = self.byname
|
||||
ipbyname = self.ipbyname
|
||||
if ':' in ip:
|
||||
targ = self.byname6
|
||||
ipbyname = self.ipbyname6
|
||||
line = '{:<39} {}'.format(ip, names)
|
||||
x = len(self.sourcelines)
|
||||
self.sourcelines.append(line)
|
||||
for name in names.split():
|
||||
if not name:
|
||||
continue
|
||||
if ipbyname.setdefault(name, ip) != ip:
|
||||
sys.stderr.write(
|
||||
'WARNING: {0} is mapped to both {1} and {2}; all entries '
|
||||
'will be written to /etc/hosts\n'.format(
|
||||
name, ipbyname[name], ip))
|
||||
targ[name] = x
|
||||
self.byip[ip] = x
|
||||
|
||||
@@ -192,6 +183,7 @@ def main():
|
||||
names.append(fqdn)
|
||||
names = ' '.join(names)
|
||||
merger.add_entry(ipdb[node][currnet], names)
|
||||
merger.write_out('/etc/whatnowhosts')
|
||||
else:
|
||||
namesbynode = {}
|
||||
ipbynode = {}
|
||||
@@ -206,7 +198,7 @@ def main():
|
||||
merger.add_entry(ipbynode[node], namesbynode[node])
|
||||
if os.path.exists('/etc/hosts'):
|
||||
merger.read_target('/etc/hosts')
|
||||
shutil.copy2('/etc/hosts', '/etc/hosts.confluentbkup')
|
||||
os.rename('/etc/hosts', '/etc/hosts.confluentbkup')
|
||||
merger.write_out('/etc/hosts')
|
||||
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ def create_image(directory, image, label=None, esize=0, totalsize=None):
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 3:
|
||||
sys.stderr.write("Usage: {0} <directory> <imagefile>\n".format(
|
||||
sys.stderr.write("Usage: {0} <directory> <imagefile>".format(
|
||||
sys.argv[0]))
|
||||
sys.exit(1)
|
||||
label = None
|
||||
|
||||
@@ -37,7 +37,6 @@ import confluent.sortutil as sortutil
|
||||
devnull = None
|
||||
|
||||
def run_automation(noderange, category, c):
|
||||
exitcode = 0
|
||||
automationbynode = {}
|
||||
for res in c.update('/noderange/{0}/deployment/remote_config/run'.format(noderange), {
|
||||
'category': category,
|
||||
@@ -65,8 +64,7 @@ def run_automation(noderange, category, c):
|
||||
if res.get('complete', False):
|
||||
del automationbynode[node]
|
||||
sys.stdout.write('{0}: Automation complete\n'.format(node))
|
||||
return exitcode
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
global devnull
|
||||
@@ -106,13 +104,10 @@ def run():
|
||||
pipedesc = {}
|
||||
pendingexecs = deque()
|
||||
exitcode = 0
|
||||
# Kept apart from exitcode: a failed automation run must not trip the
|
||||
# early exit below, which would abandon ssh children already spawned.
|
||||
autoexitcode = 0
|
||||
|
||||
c.stop_if_noderange_over(args[0], options.maxnodes)
|
||||
if options.automation:
|
||||
autoexitcode = run_automation(args[0], options.automation, c)
|
||||
run_automation(args[0], options.automation, c)
|
||||
|
||||
nodemap = {}
|
||||
cmdparms = []
|
||||
@@ -129,7 +124,7 @@ def run():
|
||||
cmdstorun.append(['run_remote', script])
|
||||
if not cmdstorun:
|
||||
if options.automation:
|
||||
sys.exit(autoexitcode)
|
||||
sys.exit(0)
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
for res in c.read('/noderange/{0}/nodes/'.format(args[0])):
|
||||
@@ -150,7 +145,7 @@ def run():
|
||||
else:
|
||||
pendingexecs.append((sshnode, cmdv))
|
||||
if not all or exitcode:
|
||||
sys.exit(exitcode | autoexitcode)
|
||||
sys.exit(exitcode)
|
||||
rdy = poller.poll(10)
|
||||
while all:
|
||||
pernodeout = {}
|
||||
@@ -198,7 +193,7 @@ def run():
|
||||
sys.stdout.flush()
|
||||
if all:
|
||||
rdy = poller.poll(10)
|
||||
sys.exit(exitcode | autoexitcode)
|
||||
sys.exit(exitcode)
|
||||
|
||||
|
||||
def run_cmdv(node, cmdv, all, poller, pipedesc):
|
||||
|
||||
+114
-119
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
__author__ = 'alin37'
|
||||
|
||||
import asyncio
|
||||
from getpass import getpass
|
||||
import optparse
|
||||
import os
|
||||
@@ -35,130 +34,126 @@ path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.asynclient as client
|
||||
import confluent.client as client
|
||||
|
||||
async def main():
|
||||
argparser = optparse.OptionParser(
|
||||
usage='''\n %prog [-b] noderange [list of attributes or 'all'] \
|
||||
\n %prog -c noderange <list of attributes> \
|
||||
\n %prog -e noderange <attribute names to set> \
|
||||
\n %prog noderange attribute1=value1 attribute2=value,...
|
||||
\n ''')
|
||||
argparser.add_option('-b', '--blame', action='store_true',
|
||||
help='Show information about how attributes are inherited')
|
||||
argparser.add_option('-e', '--environment', action='store_true',
|
||||
help='Set attributes, but from environment variable of '
|
||||
'same name')
|
||||
argparser.add_option('-c', '--clear', action='store_true',
|
||||
help='Clear attributes')
|
||||
argparser.add_option('-p', '--prompt', action='store_true',
|
||||
help='Prompt for attribute values interactively')
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Prompt if trying to set attributes on more '
|
||||
'than specified number of nodes')
|
||||
argparser.add_option('-s', '--set', dest='set', metavar='settings.batch',
|
||||
default=False, help='set attributes using a batch file')
|
||||
(options, args) = argparser.parse_args()
|
||||
argparser = optparse.OptionParser(
|
||||
usage='''\n %prog [-b] noderange [list of attributes or 'all'] \
|
||||
\n %prog -c noderange <list of attributes> \
|
||||
\n %prog -e noderange <attribute names to set> \
|
||||
\n %prog noderange attribute1=value1 attribute2=value,...
|
||||
\n ''')
|
||||
argparser.add_option('-b', '--blame', action='store_true',
|
||||
help='Show information about how attributes inherited')
|
||||
argparser.add_option('-e', '--environment', action='store_true',
|
||||
help='Set attributes, but from environment variable of '
|
||||
'same name')
|
||||
argparser.add_option('-c', '--clear', action='store_true',
|
||||
help='Clear attributes')
|
||||
argparser.add_option('-p', '--prompt', action='store_true',
|
||||
help='Prompt for attribute values interactively')
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Prompt if trying to set attributes on more '
|
||||
'than specified number of nodes')
|
||||
argparser.add_option('-s', '--set', dest='set', metavar='settings.batch',
|
||||
default=False, help='set attributes using a batch file')
|
||||
(options, args) = argparser.parse_args()
|
||||
|
||||
|
||||
#setting minimal output to only output current information
|
||||
showtype = 'current'
|
||||
requestargs=None
|
||||
#setting minimal output to only output current information
|
||||
showtype = 'current'
|
||||
requestargs=None
|
||||
try:
|
||||
noderange = args[0]
|
||||
nodelist = '/noderange/{0}/nodes/'.format(noderange)
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
|
||||
#Sets attributes
|
||||
nodetype="noderange"
|
||||
|
||||
if len(args) > 1:
|
||||
if "=" in args[1] or options.clear or options.environment or options.prompt:
|
||||
if "=" in args[1] and options.clear:
|
||||
print("Can not clear and set at the same time!")
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
argassign = None
|
||||
if options.prompt:
|
||||
argassign = {}
|
||||
for arg in args[1:]:
|
||||
oneval = 1
|
||||
twoval = 2
|
||||
while oneval != twoval:
|
||||
oneval = getpass('Enter value for {0}: '.format(arg))
|
||||
twoval = getpass('Confirm value for {0}: '.format(arg))
|
||||
if oneval != twoval:
|
||||
print('Values did not match.')
|
||||
argassign[arg] = twoval
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
exitcode=client.updateattrib(session,args,nodetype, noderange, options, argassign)
|
||||
try:
|
||||
noderange = args[0]
|
||||
nodelist = '/noderange/{0}/nodes/'.format(noderange)
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
|
||||
#Sets attributes
|
||||
nodetype="noderange"
|
||||
|
||||
if len(args) > 1:
|
||||
if "=" in args[1] or options.clear or options.environment or options.prompt:
|
||||
if "=" in args[1] and options.clear:
|
||||
print("Can not clear and set at the same time!")
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
argassign = None
|
||||
if options.prompt:
|
||||
argassign = {}
|
||||
for arg in args[1:]:
|
||||
oneval = 1
|
||||
twoval = 2
|
||||
while oneval != twoval:
|
||||
oneval = getpass('Enter value for {0}: '.format(arg))
|
||||
twoval = getpass('Confirm value for {0}: '.format(arg))
|
||||
if oneval != twoval:
|
||||
print('Values did not match.')
|
||||
argassign[arg] = twoval
|
||||
await session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
exitcode = await client.updateattrib(session,args,nodetype, noderange, options, argassign)
|
||||
try:
|
||||
# setting user output to what the user inputs
|
||||
if args[1] == 'all':
|
||||
showtype = 'all'
|
||||
requestargs=args[2:]
|
||||
elif args[1] == 'current':
|
||||
showtype = 'current'
|
||||
requestargs=args[2:]
|
||||
else:
|
||||
showtype = 'all'
|
||||
requestargs=args[1:]
|
||||
except:
|
||||
pass
|
||||
elif options.clear or options.environment or options.prompt:
|
||||
sys.stderr.write('Attribute names required with specified options\n')
|
||||
argparser.print_help()
|
||||
exitcode = 400
|
||||
|
||||
elif options.set:
|
||||
arglist = [noderange]
|
||||
showtype='current'
|
||||
argfile = open(options.set, 'r')
|
||||
argset = argfile.readline()
|
||||
while argset:
|
||||
try:
|
||||
argset = argset[:argset.index('#')]
|
||||
except ValueError:
|
||||
pass
|
||||
argset = argset.strip()
|
||||
if argset:
|
||||
arglist += shlex.split(argset)
|
||||
argset = argfile.readline()
|
||||
await session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
exitcode= await client.updateattrib(session,arglist,nodetype, noderange, options, None)
|
||||
if exitcode != 0:
|
||||
sys.exit(exitcode)
|
||||
|
||||
# Lists all attributes
|
||||
if len(args) > 0:
|
||||
# setting output to all so it can search since if we do have something to search, we want to show all outputs even if it is blank.
|
||||
if requestargs is None:
|
||||
# setting user output to what the user inputs
|
||||
if args[1] == 'all':
|
||||
showtype = 'all'
|
||||
requestargs=args[2:]
|
||||
elif args[1] == 'current':
|
||||
showtype = 'current'
|
||||
elif requestargs == []:
|
||||
#showtype already set
|
||||
pass
|
||||
requestargs=args[2:]
|
||||
else:
|
||||
try:
|
||||
requestargs.remove('all')
|
||||
requestargs.remove('current')
|
||||
except ValueError:
|
||||
pass
|
||||
exitcode = await client.printattributes(session, requestargs, showtype,nodetype, noderange, options)
|
||||
else:
|
||||
for res in session.read(nodelist):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
else:
|
||||
print(res['item']['href'].replace('/', ''))
|
||||
showtype = 'all'
|
||||
requestargs=args[1:]
|
||||
except:
|
||||
pass
|
||||
elif options.clear or options.environment or options.prompt:
|
||||
sys.stderr.write('Attribute names required with specified options\n')
|
||||
argparser.print_help()
|
||||
exitcode = 400
|
||||
|
||||
elif options.set:
|
||||
arglist = [noderange]
|
||||
showtype='current'
|
||||
argfile = open(options.set, 'r')
|
||||
argset = argfile.readline()
|
||||
while argset:
|
||||
try:
|
||||
argset = argset[:argset.index('#')]
|
||||
except ValueError:
|
||||
pass
|
||||
argset = argset.strip()
|
||||
if argset:
|
||||
arglist += shlex.split(argset)
|
||||
argset = argfile.readline()
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
exitcode=client.updateattrib(session,arglist,nodetype, noderange, options, None)
|
||||
if exitcode != 0:
|
||||
sys.exit(exitcode)
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
# Lists all attributes
|
||||
|
||||
if len(args) > 0:
|
||||
# setting output to all so it can search since if we do have something to search, we want to show all outputs even if it is blank.
|
||||
if requestargs is None:
|
||||
showtype = 'current'
|
||||
elif requestargs == []:
|
||||
#showtype already set
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
requestargs.remove('all')
|
||||
requestargs.remove('current')
|
||||
except ValueError:
|
||||
pass
|
||||
exitcode = client.printattributes(session, requestargs, showtype,nodetype, noderange, options)
|
||||
else:
|
||||
for res in session.read(nodelist):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
else:
|
||||
print(res['item']['href'].replace('/', ''))
|
||||
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -40,7 +40,7 @@ argparser.add_option('-m', '--maxnodes', type='int',
|
||||
argparser.add_option('-p', '--prompt', action='store_true',
|
||||
help='Prompt for password values interactively')
|
||||
argparser.add_option('-e', '--environment', action='store_true',
|
||||
help='Set password, but from environment variable of '
|
||||
help='Set passwod, but from environment variable of '
|
||||
'same name')
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ for rsp in session.read('/noderange/{0}/configuration/management_controller/user
|
||||
continue
|
||||
for user in rsp['databynode'][node]['users']:
|
||||
if user['username'] == username:
|
||||
if user['uid'] not in uid_dict:
|
||||
if not user['uid'] in uid_dict:
|
||||
uid_dict[user['uid']] = node
|
||||
continue
|
||||
uid_dict[user['uid']] = uid_dict[user['uid']] + ',{}'.format(node)
|
||||
|
||||
+106
-114
@@ -15,7 +15,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
|
||||
import os
|
||||
import signal
|
||||
import optparse
|
||||
@@ -31,7 +31,7 @@ path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.asynclient as client
|
||||
import confluent.client as client
|
||||
|
||||
class NullOpt(object):
|
||||
blame = None
|
||||
@@ -78,9 +78,6 @@ argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to configure, '
|
||||
'prompting if over the threshold')
|
||||
argparser.add_option('-p', '--pending', dest='showpending',
|
||||
action='store_true', default=False,
|
||||
help='Show pending changes for attributes')
|
||||
(options, args) = argparser.parse_args()
|
||||
|
||||
cfgpaths = {
|
||||
@@ -173,7 +170,7 @@ def parse_config_line(arguments, single=False):
|
||||
if '=' in param or param[-1] == ':' or forceset:
|
||||
if setmode is None:
|
||||
setmode = True
|
||||
if not setmode:
|
||||
if setmode != True:
|
||||
bailout('Cannot do set and query in same command: Query detected but "{0}" appears to be set'.format(param))
|
||||
if '=' in param:
|
||||
key, _, value = param.partition('=')
|
||||
@@ -185,7 +182,7 @@ def parse_config_line(arguments, single=False):
|
||||
else:
|
||||
if setmode is None:
|
||||
setmode = False
|
||||
if setmode:
|
||||
if setmode != False:
|
||||
bailout('Cannot do set and query in same command: Set mode detected but "{0}" appears to be a query'.format(param))
|
||||
if '.' not in param:
|
||||
if param == 'bmc':
|
||||
@@ -225,114 +222,109 @@ def parse_config_line(arguments, single=False):
|
||||
queryparms[path] = {}
|
||||
queryparms[path][attrib] = param
|
||||
|
||||
async def main():
|
||||
global printsys
|
||||
if options.batch:
|
||||
printsys = []
|
||||
argfile = open(options.batch, 'r')
|
||||
if options.batch:
|
||||
printsys = []
|
||||
argfile = open(options.batch, 'r')
|
||||
argset = argfile.readline()
|
||||
while argset:
|
||||
try:
|
||||
argset = argset[:argset.index('#')]
|
||||
except ValueError:
|
||||
pass
|
||||
argset = argset.strip()
|
||||
if argset:
|
||||
parse_config_line(shlex.split(argset), single=True)
|
||||
argset = argfile.readline()
|
||||
while argset:
|
||||
try:
|
||||
argset = argset[:argset.index('#')]
|
||||
except ValueError:
|
||||
pass
|
||||
argset = argset.strip()
|
||||
if argset:
|
||||
parse_config_line(shlex.split(argset), single=True)
|
||||
argset = argfile.readline()
|
||||
else:
|
||||
parse_config_line(args[1:])
|
||||
session = client.Command()
|
||||
rcode = 0
|
||||
if options.restoredefault:
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
if options.restoredefault.lower() in (
|
||||
'sys', 'system', 'uefi', 'bios'):
|
||||
for fr in session.update(
|
||||
'/noderange/{0}/configuration/system/clear'.format(noderange),
|
||||
{'clear': True}):
|
||||
rcode |= client.printerror(fr)
|
||||
sys.exit(rcode)
|
||||
elif options.restoredefault.lower() in (
|
||||
'bmc', 'imm', 'xcc'):
|
||||
for fr in session.update(
|
||||
'/noderange/{0}/configuration/management_controller/clear'.format(noderange),
|
||||
{'clear': True}):
|
||||
rcode |= client.printerror(fr)
|
||||
sys.exit(rcode)
|
||||
else:
|
||||
parse_config_line(args[1:])
|
||||
session = client.Command()
|
||||
rcode = 0
|
||||
if options.restoredefault:
|
||||
await session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
if options.restoredefault.lower() in (
|
||||
'sys', 'system', 'uefi', 'bios'):
|
||||
async for fr in session.update(
|
||||
'/noderange/{0}/configuration/system/clear'.format(noderange),
|
||||
{'clear': True}):
|
||||
rcode |= client.printerror(fr)
|
||||
sys.exit(rcode)
|
||||
elif options.restoredefault.lower() in (
|
||||
'bmc', 'imm', 'xcc'):
|
||||
async for fr in session.update(
|
||||
'/noderange/{0}/configuration/management_controller/clear'.format(noderange),
|
||||
{'clear': True}):
|
||||
rcode |= client.printerror(fr)
|
||||
sys.exit(rcode)
|
||||
sys.stderr.write(
|
||||
'Unrecognized component to restore defaults: {0}\n'.format(
|
||||
options.restoredefault))
|
||||
sys.exit(1)
|
||||
if setmode:
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
if options.exclude:
|
||||
sys.stderr.write('Cannot use exclude and assign at the same time\n')
|
||||
sys.exit(1)
|
||||
updatebypath = {}
|
||||
attrnamebypath = {}
|
||||
for key in assignment:
|
||||
if key not in cfgpaths:
|
||||
if key.startswith('bmc.'):
|
||||
path = 'configuration/management_controller/extended/all'
|
||||
attrib = key.replace('bmc.', '')
|
||||
else:
|
||||
path = 'configuration/system/all'
|
||||
attrib = key
|
||||
else:
|
||||
sys.stderr.write(
|
||||
'Unrecognized component to restore defaults: {0}\n'.format(
|
||||
options.restoredefault))
|
||||
sys.exit(1)
|
||||
if setmode:
|
||||
await session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
if options.exclude:
|
||||
sys.stderr.write('Cannot use exclude and assign at the same time\n')
|
||||
sys.exit(1)
|
||||
updatebypath = {}
|
||||
attrnamebypath = {}
|
||||
for key in assignment:
|
||||
if key not in cfgpaths:
|
||||
if key.startswith('bmc.'):
|
||||
path = 'configuration/management_controller/extended/all'
|
||||
attrib = key.replace('bmc.', '')
|
||||
else:
|
||||
path = 'configuration/system/all'
|
||||
attrib = key
|
||||
path, attrib = cfgpaths[key]
|
||||
if path not in updatebypath:
|
||||
updatebypath[path] = {}
|
||||
attrnamebypath[path] = {}
|
||||
updatebypath[path][attrib] = assignment[key]
|
||||
attrnamebypath[path][attrib] = key
|
||||
# well, we want to expand things..
|
||||
# check ipv4, if requested change method to static
|
||||
for path in updatebypath:
|
||||
for fr in session.update('/noderange/{0}/{1}'.format(noderange, path),
|
||||
updatebypath[path]):
|
||||
rcode |= client.printerror(fr)
|
||||
for node in fr.get('databynode', []):
|
||||
r = fr['databynode'][node]
|
||||
if 'value' not in r:
|
||||
continue
|
||||
keyval = r['value']
|
||||
key, val = keyval.split('=')
|
||||
if key in attrnamebypath[path]:
|
||||
key = attrnamebypath[path][key]
|
||||
print('{0}: {1}: {2}'.format(node, key, val))
|
||||
else:
|
||||
for path in queryparms:
|
||||
if options.comparedefault:
|
||||
continue
|
||||
rcode |= client.print_attrib_path(path, session, list(queryparms[path]),
|
||||
NullOpt(), queryparms[path])
|
||||
if printsys == 'all' or printextbmc or printbmc or printallbmc:
|
||||
if printbmc or not printextbmc:
|
||||
rcode |= client.print_attrib_path(
|
||||
'/noderange/{0}/configuration/management_controller/extended/all'.format(noderange),
|
||||
session, printbmc, options, attrprefix='bmc.')
|
||||
if options.extra:
|
||||
if options.advanced:
|
||||
rcode |= client.print_attrib_path(
|
||||
'/noderange/{0}/configuration/management_controller/extended/extra_advanced'.format(noderange),
|
||||
session, printextbmc, options)
|
||||
else:
|
||||
path, attrib = cfgpaths[key]
|
||||
if path not in updatebypath:
|
||||
updatebypath[path] = {}
|
||||
attrnamebypath[path] = {}
|
||||
updatebypath[path][attrib] = assignment[key]
|
||||
attrnamebypath[path][attrib] = key
|
||||
# well, we want to expand things..
|
||||
# check ipv4, if requested change method to static
|
||||
for path in updatebypath:
|
||||
async for fr in session.update('/noderange/{0}/{1}'.format(noderange, path),
|
||||
updatebypath[path]):
|
||||
rcode |= client.printerror(fr)
|
||||
for node in fr.get('databynode', []):
|
||||
r = fr['databynode'][node]
|
||||
if 'value' not in r:
|
||||
continue
|
||||
keyval = r['value']
|
||||
key, val = keyval.split('=')
|
||||
if key in attrnamebypath[path]:
|
||||
key = attrnamebypath[path][key]
|
||||
print('{0}: {1}: {2}'.format(node, key, val))
|
||||
else:
|
||||
for path in queryparms:
|
||||
if options.comparedefault:
|
||||
continue
|
||||
rcode |= await client.print_attrib_path(path, session, list(queryparms[path]),
|
||||
NullOpt(), queryparms[path], showpending=options.showpending)
|
||||
if printsys == 'all' or printextbmc or printbmc or printallbmc:
|
||||
if printbmc or not printextbmc:
|
||||
rcode |= await client.print_attrib_path(
|
||||
'/noderange/{0}/configuration/management_controller/extended/all'.format(noderange),
|
||||
session, printbmc, options, attrprefix='bmc.', showpending=options.showpending)
|
||||
if options.extra:
|
||||
if options.advanced:
|
||||
rcode |= await client.print_attrib_path(
|
||||
'/noderange/{0}/configuration/management_controller/extended/extra_advanced'.format(noderange),
|
||||
session, printextbmc, options, showpending=options.showpending)
|
||||
else:
|
||||
rcode |= await client.print_attrib_path(
|
||||
'/noderange/{0}/configuration/management_controller/extended/extra'.format(noderange),
|
||||
session, printextbmc, options, showpending=options.showpending)
|
||||
if printsys or options.exclude:
|
||||
if printsys == 'all':
|
||||
printsys = []
|
||||
if (options.comparedefault or printsys == []) and not options.advanced:
|
||||
path = '/noderange/{0}/configuration/system/all'.format(noderange)
|
||||
else:
|
||||
path = '/noderange/{0}/configuration/system/advanced'.format(
|
||||
noderange)
|
||||
rcode |= await client.print_attrib_path(path, session, printsys,
|
||||
options, showpending=options.showpending)
|
||||
sys.exit(rcode)
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
rcode |= client.print_attrib_path(
|
||||
'/noderange/{0}/configuration/management_controller/extended/extra'.format(noderange),
|
||||
session, printextbmc, options)
|
||||
if printsys or options.exclude:
|
||||
if printsys == 'all':
|
||||
printsys = []
|
||||
if (options.comparedefault or printsys == []) and not options.advanced:
|
||||
path = '/noderange/{0}/configuration/system/all'.format(noderange)
|
||||
else:
|
||||
path = '/noderange/{0}/configuration/system/advanced'.format(
|
||||
noderange)
|
||||
rcode = client.print_attrib_path(path, session, printsys,
|
||||
options)
|
||||
sys.exit(rcode)
|
||||
|
||||
+117
-725
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -15,7 +15,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
@@ -31,38 +30,29 @@ path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.asynclient as client
|
||||
import confluent.client as client
|
||||
|
||||
async def main():
|
||||
argparser = optparse.OptionParser(
|
||||
usage='''\n %prog noderange attribute1=value1 attribute2=value,...
|
||||
\n ''')
|
||||
(options, args) = argparser.parse_args()
|
||||
requestargs = None
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
attribs = {'name': noderange}
|
||||
for arg in args[1:]:
|
||||
if '=' not in arg:
|
||||
sys.stderr.write(
|
||||
'Attributes must be given as attribute=value, got "{0}"\n'.format(
|
||||
arg))
|
||||
sys.exit(1)
|
||||
key, val = arg.split('=', 1)
|
||||
attribs[key] = val
|
||||
async for r in session.create('/noderange/', attribs):
|
||||
if 'error' in r:
|
||||
sys.stderr.write(r['error'] + '\n')
|
||||
exitcode |= 1
|
||||
if 'created' in r:
|
||||
print('{0}: created'.format(r['created']))
|
||||
sys.exit(exitcode)
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
argparser = optparse.OptionParser(
|
||||
usage='''\n %prog noderange attribute1=value1 attribute2=value,...
|
||||
\n ''')
|
||||
(options, args) = argparser.parse_args()
|
||||
requestargs=None
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
client.check_globbing(noderange)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
attribs = {'name': noderange}
|
||||
for arg in args[1:]:
|
||||
key, val = arg.split('=', 1)
|
||||
attribs[key] = val
|
||||
for r in session.create('/noderange/', attribs):
|
||||
if 'error' in r:
|
||||
sys.stderr.write(r['error'] + '\n')
|
||||
exitcode |= 1
|
||||
if 'created' in r:
|
||||
print('{0}: created'.format(r['created']))
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
@@ -53,7 +52,7 @@ def setpending(nr, profile, profilebynodes, cli):
|
||||
if profilebynodes:
|
||||
for node in sortutil.natural_sort(profilebynodes):
|
||||
prof = profilebynodes[node]
|
||||
args = {'deployment.pendingprofile': prof, 'deployment.state': '', 'deployment.state_detail': '', 'deployment.state_last_updated': ''}
|
||||
args = {'deployment.pendingprofile': prof, 'deployment.state': '', 'deployment.state_detail': ''}
|
||||
if not prof.startswith('genesis-'):
|
||||
args['deployment.stagedprofile'] = ''
|
||||
args['deployment.profile'] = ''
|
||||
@@ -61,7 +60,7 @@ def setpending(nr, profile, profilebynodes, cli):
|
||||
args):
|
||||
pass
|
||||
return
|
||||
args = {'deployment.pendingprofile': profile, 'deployment.state': '', 'deployment.state_detail': '', 'deployment.state_last_updated': ''}
|
||||
args = {'deployment.pendingprofile': profile, 'deployment.state': '', 'deployment.state_detail': ''}
|
||||
if not profile.startswith('genesis-'):
|
||||
args['deployment.stagedprofile'] = ''
|
||||
args['deployment.profile'] = ''
|
||||
@@ -81,7 +80,7 @@ def main(args):
|
||||
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='Specify a maximum nodes to be deployed')
|
||||
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')
|
||||
ap.add_argument('noderange', help='Set of nodes to deploy')
|
||||
ap.add_argument('profile', nargs='?', help='Profile name to deploy')
|
||||
@@ -133,6 +132,11 @@ def main(args):
|
||||
curr = nodeinfo[attr].get('value', '')
|
||||
if curr and node not in profilebynode:
|
||||
profilebynode[node] = curr
|
||||
for lockinfo in c.read('/noderange/{0}/deployment/lock'.format(args.noderange)):
|
||||
for node in lockinfo.get('databynode', {}):
|
||||
lockstate = lockinfo['databynode'][node]['lock']['value']
|
||||
if lockstate == 'locked':
|
||||
lockednodes.append(node)
|
||||
if args.profile and profilebynode:
|
||||
sys.stderr.write('The -r/--redeploy option cannot be used with a profile, it redeploys the current or pending profile\n')
|
||||
return 1
|
||||
@@ -175,7 +179,7 @@ def main(args):
|
||||
if node not in databynode:
|
||||
databynode[node] = {}
|
||||
for attr in dbn[node]:
|
||||
if attr in ('deployment.pendingprofile', 'deployment.apiarmed', 'deployment.stagedprofile', 'deployment.profile', 'deployment.state', 'deployment.state_detail', 'deployment.state_last_updated'):
|
||||
if attr in ('deployment.pendingprofile', 'deployment.apiarmed', 'deployment.stagedprofile', 'deployment.profile', 'deployment.state', 'deployment.state_detail'):
|
||||
databynode[node][attr] = dbn[node][attr].get('value', '')
|
||||
for node in sortutil.natural_sort(databynode):
|
||||
profile = databynode[node].get('deployment.pendingprofile', '')
|
||||
@@ -189,8 +193,6 @@ def main(args):
|
||||
profile = databynode[node].get('deployment.profile', '')
|
||||
if profile:
|
||||
profile = 'completed: {}'.format(profile)
|
||||
elif args.clear:
|
||||
profile = 'No profile applied, any pending profile has been cleared'
|
||||
else:
|
||||
profile= 'No profile pending or applied'
|
||||
armed = databynode[node].get('deployment.apiarmed', '')
|
||||
@@ -201,19 +203,11 @@ def main(args):
|
||||
stateinfo = ''
|
||||
deploymentstate = databynode[node].get('deployment.state', '')
|
||||
if deploymentstate:
|
||||
deploymentdate = databynode[node].get('deployment.state_last_updated', '')
|
||||
if deploymentdate:
|
||||
try:
|
||||
deploymentdate = datetime.datetime.fromisoformat(deploymentdate).strftime('%m/%d/%Y %H:%M:%S')
|
||||
except ValueError:
|
||||
pass
|
||||
statedetails = databynode[node].get('deployment.state_detail', '')
|
||||
if statedetails:
|
||||
stateinfo = '{}: {}'.format(deploymentstate, statedetails)
|
||||
else:
|
||||
stateinfo = deploymentstate
|
||||
if deploymentdate:
|
||||
stateinfo += ' - {}'.format(deploymentdate)
|
||||
if stateinfo:
|
||||
print('{0}: {1} ({2})'.format(node, profile, stateinfo))
|
||||
else:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -15,18 +15,19 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
import csv
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.asynclient as client
|
||||
import confluent.client as client
|
||||
import confluent.sortutil as sortutil
|
||||
|
||||
defcolumns = ['Node', 'Model', 'Serial', 'UUID', 'Mac Address', 'Type',
|
||||
'Current IP Addresses']
|
||||
@@ -49,11 +50,10 @@ columnmapping = {
|
||||
}
|
||||
#TODO: add chassis uuid
|
||||
|
||||
async def register_endpoint(options, session, addr):
|
||||
def register_endpoint(options, session, addr):
|
||||
neednewline = False
|
||||
current = 0
|
||||
total = 0
|
||||
async for rsp in session.update('/discovery/register', {'addresses': addr}):
|
||||
for rsp in session.update('/discovery/register', {'addresses': addr}):
|
||||
if 'count' in rsp:
|
||||
total = rsp['count']
|
||||
elif total > 1:
|
||||
@@ -69,28 +69,29 @@ async def register_endpoint(options, session, addr):
|
||||
if neednewline:
|
||||
print('')
|
||||
|
||||
async def subscribe_discovery(options, session, subscribe, targ):
|
||||
def subscribe_discovery(options, session, subscribe, targ):
|
||||
keyn = 'subscribe' if subscribe else 'unsubscribe'
|
||||
payload = {keyn: targ}
|
||||
if subscribe:
|
||||
async for rsp in session.update('/discovery/subscriptions/{0}'.format(targ), payload):
|
||||
for rsp in session.update('/discovery/subscriptions/{0}'.format(targ), payload):
|
||||
if 'status' in rsp:
|
||||
print(rsp['status'])
|
||||
else:
|
||||
async for rsp in session.delete('/discovery/subscriptions/{0}'.format(targ)):
|
||||
for rsp in session.delete('/discovery/subscriptions/{0}'.format(targ)):
|
||||
if 'status' in rsp:
|
||||
print(rsp['status'])
|
||||
|
||||
async def print_disco(options, session, currid, outhandler, columns):
|
||||
def print_disco(options, session, currmac, outhandler, columns):
|
||||
procinfo = {}
|
||||
async for tmpinfo in session.read('/discovery/by-id/{0}'.format(currid)):
|
||||
for tmpinfo in session.read('/discovery/by-mac/{0}'.format(currmac)):
|
||||
|
||||
procinfo.update(tmpinfo)
|
||||
if 'Switch' in columns or 'Port' in columns:
|
||||
if 'switch' in procinfo:
|
||||
procinfo['port'] = procinfo['switchport']
|
||||
elif ':' in currid:
|
||||
async for tmpinfo in session.read(
|
||||
'/networking/macs/by-mac/{0}'.format(currid)):
|
||||
else:
|
||||
for tmpinfo in session.read(
|
||||
'/networking/macs/by-mac/{0}'.format(currmac)):
|
||||
if 'ports' in tmpinfo:
|
||||
# The api sorts so that the most specific available value
|
||||
# is last
|
||||
@@ -159,12 +160,12 @@ def datum_complete(datum):
|
||||
searchkeys = set(['mac', 'serial', 'uuid'])
|
||||
|
||||
|
||||
async def search_record(datum, options, session):
|
||||
def search_record(datum, options, session):
|
||||
for searchkey in searchkeys:
|
||||
options.__dict__[searchkey] = None
|
||||
for searchkey in searchkeys & set(datum):
|
||||
options.__dict__[searchkey] = datum[searchkey]
|
||||
return [x async for x in list_matching_ents(options, session)]
|
||||
return list(list_matching_macs(options, session))
|
||||
|
||||
|
||||
def datum_to_attrib(datum):
|
||||
@@ -179,10 +180,7 @@ def datum_to_attrib(datum):
|
||||
|
||||
unique_fields = frozenset(['serial', 'mac', 'uuid'])
|
||||
|
||||
# Cap how many nodes hold a discovery session at once while importing
|
||||
maxconcurrentassign = 128
|
||||
|
||||
async def import_csv(options, session):
|
||||
def import_csv(options, session):
|
||||
nodedata = []
|
||||
unique_data = {}
|
||||
exitcode = 0
|
||||
@@ -213,12 +211,12 @@ async def import_csv(options, session):
|
||||
alldata.append(nodedatum)
|
||||
allthere = True
|
||||
for nodedatum in alldata:
|
||||
if not await search_record(nodedatum, options, session) and not broken:
|
||||
if not search_record(nodedatum, options, session) and not broken:
|
||||
allthere = False
|
||||
await blocking_scan(session)
|
||||
blocking_scan(session)
|
||||
break
|
||||
for nodedatum in alldata:
|
||||
if not allthere and not await search_record(nodedatum, options, session):
|
||||
if not allthere and not search_record(nodedatum, options, session):
|
||||
sys.stderr.write(
|
||||
"Could not match the following data: " +
|
||||
repr(nodedatum) + '\n')
|
||||
@@ -226,13 +224,11 @@ async def import_csv(options, session):
|
||||
nodedata.append(nodedatum)
|
||||
if broken:
|
||||
sys.exit(1)
|
||||
assignments = []
|
||||
assignlimit = asyncio.Semaphore(maxconcurrentassign)
|
||||
for datum in nodedata:
|
||||
maclist = await search_record(datum, options, session)
|
||||
maclist = search_record(datum, options, session)
|
||||
datum = datum_to_attrib(datum)
|
||||
nodename = datum['name']
|
||||
async for res in session.create('/nodes/', datum):
|
||||
for res in session.create('/nodes/', datum):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode |= res.get('errorcode', 1)
|
||||
@@ -241,26 +237,13 @@ async def import_csv(options, session):
|
||||
print('Defined ' + res['created'])
|
||||
else:
|
||||
print(repr(res))
|
||||
assignments.append(
|
||||
asyncio.create_task(assign_macs(maclist, nodename, assignlimit)))
|
||||
for rcode in await asyncio.gather(*assignments, return_exceptions=True):
|
||||
if isinstance(rcode, BaseException):
|
||||
sys.stderr.write('Error assigning discovery data: {0}\n'.format(rcode))
|
||||
rcode = 1
|
||||
exitcode |= rcode
|
||||
if exitcode:
|
||||
sys.exit(exitcode)
|
||||
|
||||
|
||||
async def assign_macs(maclist, nodename, assignlimit):
|
||||
exitcode = 0
|
||||
async with assignlimit:
|
||||
# A session of our own, since the connection carries one request at a
|
||||
# time and the caller's is busy defining the remaining nodes
|
||||
mysess = client.Command()
|
||||
child = os.fork()
|
||||
if child:
|
||||
continue
|
||||
for mac in maclist:
|
||||
async for res in mysess.update('/discovery/by-id/{0}'.format(mac),
|
||||
{'node': nodename}):
|
||||
mysess = client.Command()
|
||||
for res in mysess.update('/discovery/by-mac/{0}'.format(mac),
|
||||
{'node': nodename}):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode |= res.get('errorcode', 1)
|
||||
@@ -269,10 +252,17 @@ async def assign_macs(maclist, nodename, assignlimit):
|
||||
print('Discovered ' + res['assigned'])
|
||||
else:
|
||||
print(repr(res))
|
||||
return exitcode
|
||||
sys.exit(0)
|
||||
while True:
|
||||
try:
|
||||
os.wait()
|
||||
except ChildProcessError:
|
||||
break
|
||||
if exitcode:
|
||||
sys.exit(exitcode)
|
||||
|
||||
|
||||
async def list_discovery(options, session):
|
||||
def list_discovery(options, session):
|
||||
orderby = None
|
||||
if options.fields:
|
||||
columns = []
|
||||
@@ -288,24 +278,23 @@ async def list_discovery(options, session):
|
||||
if options.order.lower() == field.lower():
|
||||
orderby = field
|
||||
outhandler = client.Tabulator(columns)
|
||||
for infoid in [x async for x in list_matching_ents(options, session)]:
|
||||
await print_disco(options, session, infoid, outhandler, columns)
|
||||
for mac in list_matching_macs(options, session):
|
||||
print_disco(options, session, mac, outhandler, columns)
|
||||
if options.csv:
|
||||
outhandler.write_csv(sys.stdout, orderby)
|
||||
else:
|
||||
for row in outhandler.get_table(orderby):
|
||||
print(row)
|
||||
|
||||
async def clear_discovery(options, session):
|
||||
allidentifiers = [x async for x in list_matching_ents(options, session)]
|
||||
for infoid in allidentifiers:
|
||||
async for res in session.delete('/discovery/by-id/{0}'.format(infoid)):
|
||||
def clear_discovery(options, session):
|
||||
for mac in list_matching_macs(options, session):
|
||||
for res in session.delete('/discovery/by-mac/{0}'.format(mac)):
|
||||
if 'deleted' in res:
|
||||
print('Cleared info for {0}'.format(res['deleted']))
|
||||
else:
|
||||
print(repr(res))
|
||||
|
||||
async def list_matching_ents(options, session, node=None, checknode=True):
|
||||
def list_matching_macs(options, session, node=None, checknode=True):
|
||||
path = '/discovery/'
|
||||
if node:
|
||||
path += 'by-node/{0}/'.format(node)
|
||||
@@ -324,22 +313,23 @@ async def list_matching_ents(options, session, node=None, checknode=True):
|
||||
options.state = 'unidentified'
|
||||
path += 'by-state/{0}/'.format(options.state).lower()
|
||||
if options.mac:
|
||||
path += 'by-id/{0}'.format(options.mac)
|
||||
result = list([x async for x in session.read(path)])[0]
|
||||
path += 'by-mac/{0}'.format(options.mac)
|
||||
result = list(session.read(path))[0]
|
||||
if 'error' in result:
|
||||
return
|
||||
yield options.mac.replace(':', '-')
|
||||
return
|
||||
return []
|
||||
return [options.mac.replace(':', '-')]
|
||||
else:
|
||||
path += 'by-id/'
|
||||
async for x in session.read(path):
|
||||
path += 'by-mac/'
|
||||
ret = []
|
||||
for x in session.read(path):
|
||||
if 'item' in x and 'href' in x['item']:
|
||||
yield x['item']['href']
|
||||
ret.append(x['item']['href'])
|
||||
return ret
|
||||
|
||||
async def assign_discovery(options, session, needid=True):
|
||||
def assign_discovery(options, session, needid=True):
|
||||
abort = False
|
||||
if options.importfile:
|
||||
return await import_csv(options, session)
|
||||
return import_csv(options, session)
|
||||
if not options.node:
|
||||
sys.stderr.write("Node (-n) must be specified for assignment\n")
|
||||
abort = True
|
||||
@@ -350,19 +340,16 @@ async def assign_discovery(options, session, needid=True):
|
||||
abort = True
|
||||
if abort:
|
||||
sys.exit(1)
|
||||
matches = [x async for x in list_matching_ents(options, session, None if needid else options.node, False)]
|
||||
matches = list_matching_macs(options, session, None if needid else options.node, False)
|
||||
if not matches:
|
||||
# Do a rescan to catch missing requested data
|
||||
if needid and options.mac and '.' in options.mac:
|
||||
await register_endpoint(options, session, options.mac)
|
||||
else:
|
||||
await blocking_scan(session)
|
||||
matches = [x async for x in list_matching_ents(options, session, None if needid else options.node, False)]
|
||||
blocking_scan(session)
|
||||
matches = list_matching_macs(options, session, None if needid else options.node, False)
|
||||
if not matches:
|
||||
sys.stderr.write("No matching discovery candidates found\n")
|
||||
sys.exit(1)
|
||||
exitcode = 0
|
||||
async for res in session.update('/discovery/by-id/{0}'.format(matches[0]),
|
||||
for res in session.update('/discovery/by-mac/{0}'.format(matches[0]),
|
||||
{'node': options.node}):
|
||||
if 'assigned' in res:
|
||||
print('Assigned: {0}'.format(res['assigned']))
|
||||
@@ -374,14 +361,14 @@ async def assign_discovery(options, session, needid=True):
|
||||
if exitcode:
|
||||
sys.exit(exitcode)
|
||||
|
||||
async def blocking_scan(session, aggressive=False):
|
||||
list([x async for x in session.update('/discovery/rescan', {'rescan': 'aggressive' if aggressive else 'start'})])
|
||||
while(list([x async for x in session.read('/discovery/rescan')])[0].get('scanning', False)):
|
||||
await asyncio.sleep(0.5)
|
||||
list([x async for x in session.update('/networking/macs/rescan', {'rescan': 'start'})])
|
||||
def blocking_scan(session):
|
||||
list(session.update('/discovery/rescan', {'rescan': 'start'}))
|
||||
while(list(session.read('/discovery/rescan'))[0].get('scanning', False)):
|
||||
time.sleep(0.5)
|
||||
list(session.update('/networking/macs/rescan', {'rescan': 'start'}))
|
||||
|
||||
|
||||
async def main():
|
||||
def main():
|
||||
parser = optparse.OptionParser(
|
||||
usage='Usage: %prog [list|assign|rescan|clear|subscribe|unsubscribe|register] [options]')
|
||||
# -a for 'address' maybe?
|
||||
@@ -391,9 +378,6 @@ async def main():
|
||||
# flush to clear old data out? (e.g. no good way to age pxe data)
|
||||
# also delete discovery datum... more targeted
|
||||
# defect: -t lenovo-imm returns all
|
||||
parser.add_option('-a', '--aggressive', dest='aggressive',
|
||||
help='Use aggressive scanning and fingerprinting that is slower and more intrusive',
|
||||
action='store_true')
|
||||
parser.add_option('-m', '--model', dest='model',
|
||||
help='Operate with nodes matching the specified model '
|
||||
'number', metavar='MODEL')
|
||||
@@ -408,8 +392,8 @@ async def main():
|
||||
'UUID', metavar='UUID')
|
||||
parser.add_option('-n', '--node', help='Operate with the given nodename')
|
||||
parser.add_option('-e', '--ethaddr', dest='mac',
|
||||
help='Operate against the system with the specified MAC or IP '
|
||||
'address if mac not available', metavar='MAC')
|
||||
help='Operate against the system with the specified MAC '
|
||||
'address', metavar='MAC')
|
||||
parser.add_option('-t', '--type', dest='type',
|
||||
help='Operate against the system of the specified type',
|
||||
metavar='TYPE')
|
||||
@@ -435,23 +419,23 @@ async def main():
|
||||
sys.exit(1)
|
||||
session = client.Command()
|
||||
if args[0] == 'list':
|
||||
await list_discovery(options, session)
|
||||
list_discovery(options, session)
|
||||
if args[0] == 'clear':
|
||||
await clear_discovery(options, session)
|
||||
clear_discovery(options, session)
|
||||
if args[0] == 'assign':
|
||||
await assign_discovery(options, session)
|
||||
assign_discovery(options, session)
|
||||
if args[0] == 'reassign':
|
||||
await assign_discovery(options, session, False)
|
||||
assign_discovery(options, session, False)
|
||||
if args[0] == 'register':
|
||||
await register_endpoint(options, session, args[1])
|
||||
register_endpoint(options, session, args[1])
|
||||
if args[0] == 'subscribe':
|
||||
await subscribe_discovery(options, session, True, args[1])
|
||||
subscribe_discovery(options, session, True, args[1])
|
||||
if args[0] == 'unsubscribe':
|
||||
await subscribe_discovery(options, session, False, args[1])
|
||||
subscribe_discovery(options, session, False, args[1])
|
||||
if args[0] == 'rescan':
|
||||
await blocking_scan(session, aggressive=options.aggressive)
|
||||
blocking_scan(session)
|
||||
print("Rescan complete")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
main()
|
||||
|
||||
@@ -53,13 +53,7 @@ argparser.add_option('-t', '--timeframe', type='string',
|
||||
'entries from the last hours or days. '
|
||||
'1h would be one hour, 4d would be four days. '
|
||||
'format <num>h or <num>d'
|
||||
)
|
||||
|
||||
|
||||
argparser.add_option('-s', '--source', type='string',
|
||||
help='only show entries from the named logs, comma '
|
||||
'delimited. "-s list" names the logs this node '
|
||||
'offers instead of showing entries')
|
||||
)
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
@@ -78,19 +72,6 @@ if len(args) == 2:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
listmode = bool(options.source) and options.source.lower() == 'list'
|
||||
wantsources = None
|
||||
if options.source and not listmode:
|
||||
wantsources = set(x.strip().lower() for x in options.source.split(',')
|
||||
if x.strip())
|
||||
if options.source and deletemode:
|
||||
# Clearing a subset is not something the platforms offer, and clearing more
|
||||
# than was asked for is not something to do quietly
|
||||
sys.stderr.write(
|
||||
'Clearing a selection of logs is not supported, so --source cannot be '
|
||||
'combined with clear\n')
|
||||
sys.exit(1)
|
||||
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
|
||||
@@ -144,7 +125,6 @@ if options.timeframe:
|
||||
sys.exit(1)
|
||||
timeframe = dt.now() - tdelta
|
||||
|
||||
sources_seen = {}
|
||||
event_dict = {}
|
||||
nodes = []
|
||||
for res in session.read('/noderange/{0}/nodes/'.format(args[0])):
|
||||
@@ -165,17 +145,6 @@ for rsp in func('/noderange/{0}/events/hardware/log'.format(noderange)):
|
||||
exitcode |= 1
|
||||
if 'events' in thisdata:
|
||||
evtdata = thisdata['events']
|
||||
for evt in evtdata:
|
||||
if evt.get('log_id', None):
|
||||
sources_seen.setdefault(node, set()).add(evt['log_id'])
|
||||
if listmode:
|
||||
continue
|
||||
if wantsources is not None:
|
||||
# Filter before any tail is taken, or asking for the last
|
||||
# few entries of one log would answer with fewer
|
||||
evtdata = [x for x in evtdata if (x.get('log_id', '')
|
||||
or '').lower()
|
||||
in wantsources]
|
||||
if options.lines:
|
||||
event_dict[node].extend(evtdata)
|
||||
else:
|
||||
@@ -189,34 +158,7 @@ for rsp in func('/noderange/{0}/events/hardware/log'.format(noderange)):
|
||||
else:
|
||||
print('{0}: {1}'.format(node, format_event(evt)))
|
||||
|
||||
if listmode:
|
||||
for node in nodes:
|
||||
found = sorted(sources_seen.get(node, ()))
|
||||
if found:
|
||||
print('{0}: {1}'.format(node, ','.join(found)))
|
||||
else:
|
||||
sys.stderr.write(
|
||||
'No event log sources reported for "{0}"\n'.format(node))
|
||||
elif wantsources is not None:
|
||||
for node in nodes:
|
||||
found = set(x.lower() for x in sources_seen.get(node, ()))
|
||||
missing = wantsources - found
|
||||
if not missing:
|
||||
continue
|
||||
if found:
|
||||
sys.stderr.write('{0}: no such log source: {1}, this node has {2}\n'
|
||||
.format(node, ','.join(sorted(missing)),
|
||||
','.join(sorted(sources_seen[node]))))
|
||||
else:
|
||||
# A log that holds no entries names no source, so an empty answer
|
||||
# here is as often a quiet platform as a wrong name. Either way
|
||||
# saying nothing would look like the log simply had nothing in it.
|
||||
sys.stderr.write(
|
||||
'{0}: no entries from any log source, so nothing could match '
|
||||
'{1}\n'.format(node, ','.join(sorted(missing))))
|
||||
exitcode |= 1
|
||||
|
||||
if options.lines and not listmode:
|
||||
if options.lines:
|
||||
for node in nodes:
|
||||
evtdata_list = event_dict[node]
|
||||
if len(evtdata_list) != 0:
|
||||
@@ -230,5 +172,3 @@ if options.lines and not listmode:
|
||||
print('{0}: {1}'.format(node, format_event(evt)))
|
||||
else:
|
||||
print('{0}: {1}'.format(node, format_event(evt)))
|
||||
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -56,14 +56,11 @@ components = ['all']
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: "
|
||||
"%prog <noderange> [list][updatestatus][updatetypes][update [--backup] "
|
||||
"[--parameterfile <file>] <file>]|[<components>]")
|
||||
"%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; see '
|
||||
'nodefirmware(8), some platforms need it to say what '
|
||||
'kind of firmware the image holds')
|
||||
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')
|
||||
@@ -71,7 +68,6 @@ argparser.add_option('-m', '--maxnodes', type='int',
|
||||
(options, args) = argparser.parse_args()
|
||||
upfile = None
|
||||
querystatus = False
|
||||
querytypes = False
|
||||
try:
|
||||
noderange = args[0]
|
||||
if len(args) > 1:
|
||||
@@ -83,8 +79,6 @@ try:
|
||||
comps = args[2:]
|
||||
elif args[1] == 'updatestatus':
|
||||
querystatus = True
|
||||
elif args[1] == 'updatetypes':
|
||||
querytypes = True
|
||||
else:
|
||||
comps = args[1:]
|
||||
components = []
|
||||
@@ -102,7 +96,7 @@ def get_update_progress(session, url):
|
||||
for res in session.read(url):
|
||||
status = res.get('phase', 'error')
|
||||
percent = res.get('progress', None)
|
||||
detail = res.get('detail', repr(res))
|
||||
detail = res.get('detail', repr(res)),
|
||||
if status == 'error':
|
||||
text = 'error!'
|
||||
else:
|
||||
@@ -146,11 +140,6 @@ def update_firmware(session, filename):
|
||||
pass
|
||||
for res in session.create(resource, upargs):
|
||||
if 'created' not in res:
|
||||
if not res.get('databynode', None):
|
||||
# A failure that is not attributed to any node still has to be
|
||||
# reported, or the command looks like it quietly did nothing
|
||||
exitcode |= client.printerror(res)
|
||||
continue
|
||||
for nodename in res.get('databynode', ()):
|
||||
output.set_output(nodename, 'error!')
|
||||
noderrs[nodename] = res['databynode'][nodename].get(
|
||||
@@ -204,21 +193,7 @@ def show_firmware(session):
|
||||
if not nodes_matched:
|
||||
sys.stderr.write('No matching nodes for noderange "{0}"\n'.format(noderange))
|
||||
elif not firmware_shown and not exitcode:
|
||||
# Asking about firmware the target does not describe is a legitimate
|
||||
# question with an empty answer, not a mistake in how it was asked
|
||||
sys.stderr.write('No firmware reported for "{0}"\n'.format(
|
||||
','.join(components)))
|
||||
|
||||
|
||||
def show_update_types(session):
|
||||
global exitcode
|
||||
for res in session.read(
|
||||
'/noderange/{0}/inventory/firmware/updatetypes'.format(noderange)):
|
||||
exitcode |= client.printerror(res)
|
||||
for node in res.get('databynode', {}):
|
||||
types = res['databynode'][node].get('types', None)
|
||||
if types:
|
||||
print('{0}: {1}'.format(node, ','.join(types)))
|
||||
argparser.print_help()
|
||||
|
||||
|
||||
try:
|
||||
@@ -226,13 +201,12 @@ try:
|
||||
if querystatus:
|
||||
for res in session.read(
|
||||
'/noderange/{0}/inventory/firmware/updatestatus'.format(noderange)):
|
||||
exitcode |= client.printerror(res)
|
||||
for node in res.get('databynode', {}):
|
||||
currstat = res['databynode'][node].get('status', None)
|
||||
if currstat:
|
||||
print('{}: {}'.format(node, currstat))
|
||||
elif querytypes:
|
||||
show_update_types(session)
|
||||
else:
|
||||
print(repr(res))
|
||||
elif upfile is None:
|
||||
show_firmware(session)
|
||||
else:
|
||||
|
||||
@@ -42,7 +42,7 @@ argparser = optparse.OptionParser(
|
||||
\n %prog [options] nodegroup nodes=value1,value2
|
||||
\n ''')
|
||||
argparser.add_option('-b', '--blame', action='store_true',
|
||||
help='Show information about how attributes are inherited')
|
||||
help='Show information about how attributes inherited')
|
||||
argparser.add_option('-e', '--environment', action='store_true',
|
||||
help='Set attributes, but from environment variable of '
|
||||
'same name')
|
||||
|
||||
@@ -38,8 +38,6 @@ if sys.version_info[0] < 3:
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
|
||||
filters = []
|
||||
wanted = []
|
||||
matched = False
|
||||
|
||||
|
||||
def pretty(text):
|
||||
@@ -123,10 +121,8 @@ if len(args) > 1:
|
||||
os.execlp('nodefirmware', 'nodefirmware', noderange)
|
||||
else:
|
||||
url = '/noderange/{0}/inventory/hardware/all/system'
|
||||
for rawarg in args:
|
||||
for arg in rawarg.split(','):
|
||||
if arg in ('serial', 'model', 'uuid', 'mac'):
|
||||
wanted.append(arg)
|
||||
for arg in args:
|
||||
for arg in arg.split(','):
|
||||
if arg == 'serial':
|
||||
filters.append(re.compile('serial number'))
|
||||
elif arg == 'model':
|
||||
@@ -207,7 +203,6 @@ try:
|
||||
continue
|
||||
if info[datum] is None:
|
||||
continue
|
||||
matched = True
|
||||
if options.json:
|
||||
if node not in databynode:
|
||||
databynode[node] = {}
|
||||
@@ -217,11 +212,6 @@ try:
|
||||
print(u'{0}: {1} {2}: {3}'.format(node, prefix,
|
||||
pretty(datum),
|
||||
info[datum]))
|
||||
if filters and not matched and not options.store:
|
||||
# An inventory that does not describe what was asked for is a valid
|
||||
# answer, but saying nothing at all leaves the caller guessing
|
||||
sys.stderr.write('No {0} reported for "{1}"\n'.format(
|
||||
'/'.join(wanted) if wanted else 'matching inventory', noderange))
|
||||
if options.json:
|
||||
print(json.dumps(databynode, sort_keys=True, indent=4,
|
||||
separators=(',', ': ')))
|
||||
|
||||
@@ -162,7 +162,7 @@ for end_node in end_nodeslist:
|
||||
if end_node:
|
||||
end_switches = host_to_switch(end_node, eface)
|
||||
if not end_switches:
|
||||
print('Error: net.{0}.switch attribute is not valid'.format(eface))
|
||||
print('Error: net.{0}.switch attribute is not valid')
|
||||
continue
|
||||
path = path_between_nodes(start_switches, end_switches)
|
||||
print(f'{start_node} to {end_node}: {path}')
|
||||
|
||||
@@ -19,6 +19,7 @@ import optparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
@@ -119,7 +120,6 @@ def show_licenses(session):
|
||||
for res in session.read(
|
||||
'/noderange/{0}/configuration/management_controller/licenses/'
|
||||
'all'.format(noderange)):
|
||||
exitcode |= client.printerror(res)
|
||||
for node in res.get('databynode', {}):
|
||||
for license in res['databynode'][node].get('License', []):
|
||||
msg = '{0}: {1}'.format(node, license.get('feature',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/libexec/platform-python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
@@ -21,7 +21,6 @@ import optparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import asyncio
|
||||
|
||||
|
||||
|
||||
@@ -34,14 +33,14 @@ path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
|
||||
if path.startswith('/opt'):
|
||||
sys.path.append(path)
|
||||
|
||||
import confluent.asynclient as client
|
||||
import confluent.client as client
|
||||
|
||||
async def main():
|
||||
def main():
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog noderange\n"
|
||||
" or: %prog [options] noderange <nodeattribute>...")
|
||||
argparser.add_option('-b', '--blame', action='store_true',
|
||||
help='Show information about how attributes are inherited')
|
||||
help='Show information about how attributes inherited')
|
||||
argparser.add_option('-d', '--delim', metavar="STRING", default = "\n",
|
||||
help='Delimiter separating the values')
|
||||
(options, args) = argparser.parse_args()
|
||||
@@ -60,9 +59,9 @@ async def main():
|
||||
requestargs=args[1:]
|
||||
nodetype='noderange'
|
||||
if len(args) > 1:
|
||||
exitcode=await client.printattributes(session,requestargs,showtype,nodetype,noderange,options)
|
||||
exitcode=client.printattributes(session,requestargs,showtype,nodetype,noderange,options)
|
||||
else:
|
||||
async for res in session.read(nodelist):
|
||||
for res in session.read(nodelist):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
@@ -74,4 +73,4 @@ async def main():
|
||||
sys.exit(exitcode)
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
main()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -15,7 +15,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
import optparse
|
||||
import os
|
||||
import signal
|
||||
@@ -33,32 +32,26 @@ if path.startswith('/opt'):
|
||||
|
||||
import confluent.client as client
|
||||
|
||||
|
||||
async def main():
|
||||
argparser = optparse.OptionParser(
|
||||
usage='''\n %prog <noderange>
|
||||
argparser = optparse.OptionParser(
|
||||
usage='''\n %prog <noderange>
|
||||
\n ''')
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to delete, '
|
||||
'prompting if over the threshold')
|
||||
(options, args) = argparser.parse_args()
|
||||
if len(args) != 1:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
noderange = args[0]
|
||||
client.check_globbing(noderange)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
for r in session.delete('/noderange/{0}'.format(noderange)):
|
||||
if 'error' in r:
|
||||
sys.stderr.write(r['error'] + '\n')
|
||||
exitcode |= 1
|
||||
if 'deleted' in r:
|
||||
print('{0}: deleted'.format(r['deleted']))
|
||||
sys.exit(exitcode)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
help='Specify a maximum number of '
|
||||
'nodes to delete, '
|
||||
'prompting if over the threshold')
|
||||
(options, args) = argparser.parse_args()
|
||||
if len(args) != 1:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
noderange = args[0]
|
||||
client.check_globbing(noderange)
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
for r in session.delete('/noderange/{0}'.format(noderange)):
|
||||
if 'error' in r:
|
||||
sys.stderr.write(r['error'] + '\n')
|
||||
exitcode |= 1
|
||||
if 'deleted' in r:
|
||||
print('{0}: deleted'.format(r['deleted']))
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -35,6 +35,7 @@ if path.startswith('/opt'):
|
||||
|
||||
import confluent.client as client
|
||||
import confluent.screensqueeze as sq
|
||||
import confluent.sortutil as sortutil
|
||||
|
||||
|
||||
def run():
|
||||
|
||||
@@ -107,7 +107,7 @@ def sensorpass(showout=True, appendtime=False):
|
||||
if 'error' in reading:
|
||||
sys.stderr.write('Error: {0}\n'.format(reading['error']))
|
||||
if 'errorcode' in reading:
|
||||
exitcode |= reading['errorcode']
|
||||
exitcode |= exitcode
|
||||
else:
|
||||
exitcode |= 1
|
||||
if 'databynode' not in reading:
|
||||
@@ -120,10 +120,6 @@ def sensorpass(showout=True, appendtime=False):
|
||||
sys.stderr.write(
|
||||
'{0}: Error: {1}\n'.format(node,
|
||||
reading[node]['error']))
|
||||
if 'errorcode' in reading[node]:
|
||||
exitcode |= reading[node]['errorcode']
|
||||
else:
|
||||
exitcode |= 1
|
||||
if 'sensors' not in reading[node]:
|
||||
continue
|
||||
for sensedata in reading[node]['sensors']:
|
||||
@@ -148,10 +144,7 @@ def sensorpass(showout=True, appendtime=False):
|
||||
showval = u' {0} '.format(floatformat(sensedata['value']))
|
||||
else:
|
||||
showval = u' {0} '.format(sensedata.get('value', ''))
|
||||
# A discrete sensor has no reading for a unit to apply
|
||||
# to, and printing the unit on its own says nothing
|
||||
if (showval
|
||||
and sensedata.get('units', None) not in (None, u'')):
|
||||
if sensedata.get('units', None) not in (None, u''):
|
||||
showval += sensedata['units']
|
||||
if sensedata.get('health', 'ok') != 'ok':
|
||||
datadescription = [sensedata['health']]
|
||||
@@ -265,6 +258,3 @@ try:
|
||||
except KeyboardInterrupt:
|
||||
print('')
|
||||
sys.exit(0)
|
||||
# Falling off the end exited 0 whatever sensorpass recorded, so only the
|
||||
# --interval path ever reported a failed read.
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -53,8 +53,6 @@ def run():
|
||||
help='Specify a custom port for ssh')
|
||||
argparser.add_option('-s', '--substitutename',
|
||||
help='Use a different name other than the nodename for ssh')
|
||||
argparser.add_option('-t', '--timeout', type='int', default=0,
|
||||
help='Timeout in seconds for each node ssh connection')
|
||||
argparser.add_option('-x', '--noexpression', action='store_true',
|
||||
help='Suppress expression expansion of command')
|
||||
argparser.add_option('-m', '--maxnodes', type='int',
|
||||
@@ -121,8 +119,6 @@ def run():
|
||||
cmdv += ['-p', '{0}'.format(options.port)]
|
||||
if options.loginname:
|
||||
cmdv += ['-l', options.loginname]
|
||||
if options.timeout:
|
||||
cmdv += ['-o', 'ConnectTimeout={0}'.format(options.timeout)]
|
||||
cmdv += [sshnode, cmd]
|
||||
if currprocs < concurrentprocs:
|
||||
currprocs += 1
|
||||
|
||||
@@ -244,8 +244,6 @@ def main():
|
||||
sys.stdout.write('Aborting\n')
|
||||
sys.exit(1)
|
||||
handler(noderange, options, args[2:])
|
||||
# The handlers record failures in exitcode, so let a caller see them
|
||||
sys.exit(exitcode)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -76,11 +76,6 @@ def download_servicedata(noderange, media, options):
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
for res in session.create(resource, upargs):
|
||||
if 'created' not in res:
|
||||
if not res.get('databynode', None):
|
||||
# A failure that is not attributed to any node still has to be
|
||||
# reported, or the command looks like it quietly did nothing
|
||||
printerror(res)
|
||||
continue
|
||||
for nodename in res.get('databynode', ()):
|
||||
output.set_output(nodename, 'error!')
|
||||
noderrs[nodename] = res['databynode'][nodename].get(
|
||||
@@ -153,9 +148,5 @@ def main():
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
handler(noderange, media, options)
|
||||
# The handlers record failures in exitcode, so let a caller see them
|
||||
sys.exit(exitcode)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -1,860 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
# 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.
|
||||
|
||||
import asyncio
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import dbm
|
||||
import csv
|
||||
import errno
|
||||
import fnmatch
|
||||
import hashlib
|
||||
import os
|
||||
import shlex
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
import confluent.asynctlvdata as tlvdata
|
||||
import confluent.sortutil as sortutil
|
||||
libssl = ctypes.CDLL(ctypes.util.find_library('ssl'))
|
||||
libssl.SSL_CTX_set_cert_verify_callback.argtypes = [
|
||||
ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
|
||||
|
||||
SO_PASSCRED = 16
|
||||
|
||||
_attraliases = {
|
||||
'bmc': 'hardwaremanagement.manager',
|
||||
'bmcuser': 'secret.hardwaremanagementuser',
|
||||
'switchuser': 'secret.hardwaremanagementuser',
|
||||
'bmcpass': 'secret.hardwaremanagementpassword',
|
||||
'switchpass': 'secret.hardwaremanagementpassword',
|
||||
}
|
||||
|
||||
try:
|
||||
getinput = raw_input
|
||||
except NameError:
|
||||
getinput = input
|
||||
|
||||
|
||||
class PyObject_HEAD(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("ob_refcnt", ctypes.c_ssize_t),
|
||||
("ob_type", ctypes.c_void_p),
|
||||
]
|
||||
|
||||
|
||||
# see main/Modules/_ssl.c, only caring about the SSL_CTX pointer
|
||||
class PySSLContext(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("ob_base", PyObject_HEAD),
|
||||
("ctx", ctypes.c_void_p),
|
||||
]
|
||||
|
||||
|
||||
@ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
|
||||
def verify_stub(store, misc):
|
||||
return 1
|
||||
|
||||
|
||||
class NestedDict(dict):
|
||||
def __missing__(self, key):
|
||||
value = self[key] = type(self)()
|
||||
return value
|
||||
|
||||
|
||||
def stringify(instr):
|
||||
# Normalize unicode and bytes to 'str', correcting for
|
||||
# current python version
|
||||
if isinstance(instr, bytes) and not isinstance(instr, str):
|
||||
return instr.decode('utf-8', errors='replace')
|
||||
elif not isinstance(instr, bytes) and not isinstance(instr, str):
|
||||
return instr.encode('utf-8')
|
||||
return instr
|
||||
|
||||
|
||||
class Tabulator(object):
|
||||
def __init__(self, headers):
|
||||
self.headers = headers
|
||||
self.rows = []
|
||||
|
||||
def add_row(self, row):
|
||||
self.rows.append(row)
|
||||
|
||||
def get_table(self, order=None):
|
||||
i = 0
|
||||
fmtstr = ''
|
||||
separator = []
|
||||
for head in self.headers:
|
||||
if order and order == head:
|
||||
order = i
|
||||
neededlen = len(head)
|
||||
for row in self.rows:
|
||||
if len(row[i]) > neededlen:
|
||||
neededlen = len(row[i])
|
||||
separator.append('-' * (neededlen + 1))
|
||||
fmtstr += '{{{0}:>{1}}}|'.format(i, neededlen + 1)
|
||||
i = i + 1
|
||||
fmtstr = fmtstr[:-1]
|
||||
yield fmtstr.format(*self.headers)
|
||||
yield fmtstr.format(*separator)
|
||||
if order is not None:
|
||||
for row in sorted(
|
||||
self.rows,
|
||||
key=lambda x: sortutil.naturalize_string(x[order])):
|
||||
yield fmtstr.format(*row)
|
||||
else:
|
||||
for row in self.rows:
|
||||
yield fmtstr.format(*row)
|
||||
|
||||
def write_csv(self, output, order=None):
|
||||
output = csv.writer(output)
|
||||
output.writerow(self.headers)
|
||||
i = 0
|
||||
for head in self.headers:
|
||||
if order and order == head:
|
||||
order = i
|
||||
i = i + 1
|
||||
if order is not None:
|
||||
for row in sorted(
|
||||
self.rows,
|
||||
key=lambda x: sortutil.naturalize_string(x[order])):
|
||||
output.writerow(row)
|
||||
else:
|
||||
for row in self.rows:
|
||||
output.writerow(row)
|
||||
|
||||
|
||||
def printerror(res, node=None):
|
||||
exitcode = 0
|
||||
if 'errorcode' in res:
|
||||
exitcode = res['errorcode']
|
||||
for node in res.get('databynode', {}):
|
||||
exitcode = res['databynode'][node].get('errorcode', exitcode)
|
||||
if 'error' in res['databynode'][node]:
|
||||
sys.stderr.write(
|
||||
'{0}: {1}\n'.format(node, res['databynode'][node]['error']))
|
||||
if exitcode == 0:
|
||||
exitcode = 1
|
||||
if 'error' in res:
|
||||
if node:
|
||||
sys.stderr.write('{0}: {1}\n'.format(node, res['error']))
|
||||
else:
|
||||
sys.stderr.write('{0}\n'.format(res['error']))
|
||||
if 'errorcode' not in res:
|
||||
exitcode = 1
|
||||
return exitcode
|
||||
|
||||
|
||||
def cprint(txt):
|
||||
try:
|
||||
print(txt)
|
||||
except UnicodeEncodeError:
|
||||
print(txt.encode('utf8'))
|
||||
sys.stdout.flush()
|
||||
|
||||
def _parseserver(string):
|
||||
if ']:' in string:
|
||||
server, port = string[1:].split(']:')
|
||||
elif string[0] == '[':
|
||||
server = string[1:-1]
|
||||
port = '13001'
|
||||
elif ':' in string:
|
||||
server, port = string.split(':')
|
||||
else:
|
||||
server = string
|
||||
port = '13001'
|
||||
return server, port
|
||||
|
||||
|
||||
class Command(object):
|
||||
def __init__(self, server=None):
|
||||
self._prevdict = None
|
||||
self._prevkeyname = None
|
||||
self.connection = None
|
||||
self._currnoderange = None
|
||||
self.unixdomain = False
|
||||
if server is None:
|
||||
if 'CONFLUENT_HOST' in os.environ:
|
||||
self.serverloc = os.environ['CONFLUENT_HOST']
|
||||
else:
|
||||
self.serverloc = '/var/run/confluent/api.sock'
|
||||
else:
|
||||
self.serverloc = server
|
||||
self.connected = False
|
||||
|
||||
async def ensure_connected(self):
|
||||
if self.connected:
|
||||
return True
|
||||
if os.path.isabs(self.serverloc) and os.path.exists(self.serverloc):
|
||||
self._connect_unix()
|
||||
self.unixdomain = True
|
||||
elif self.serverloc == '/var/run/confluent/api.sock':
|
||||
raise Exception('Confluent service is not available')
|
||||
else:
|
||||
await self._connect_tls()
|
||||
self.protversion = int((await tlvdata.recv(self.connection)).split(
|
||||
b'--')[1].strip()[1:])
|
||||
authdata = await tlvdata.recv(self.connection)
|
||||
if authdata['authpassed'] == 1:
|
||||
self.authenticated = True
|
||||
else:
|
||||
self.authenticated = False
|
||||
if not self.authenticated and 'CONFLUENT_USER' in os.environ:
|
||||
username = os.environ['CONFLUENT_USER']
|
||||
passphrase = os.environ['CONFLUENT_PASSPHRASE']
|
||||
await self.authenticate(username, passphrase)
|
||||
self.connected = True
|
||||
|
||||
async def add_file(self, name, handle, mode):
|
||||
await self.ensure_connected()
|
||||
if self.protversion < 3:
|
||||
raise Exception('Not supported with connected confluent server')
|
||||
if not self.unixdomain:
|
||||
raise Exception('Can only add a file to a unix domain connection')
|
||||
await tlvdata.send(self.connection, {'filename': name, 'mode': mode}, handle)
|
||||
|
||||
async def authenticate(self, username, password):
|
||||
await tlvdata.send(self.connection,
|
||||
{'username': username, 'password': password})
|
||||
authdata = await tlvdata.recv(self.connection)
|
||||
if authdata['authpassed'] == 1:
|
||||
self.authenticated = True
|
||||
|
||||
def add_precede_key(self, keyname):
|
||||
self._prevkeyname = keyname
|
||||
|
||||
def add_precede_dict(self, dict):
|
||||
self._prevdict = dict
|
||||
|
||||
def handle_results(self, ikey, rc, res, errnodes=None, outhandler=None):
|
||||
if 'error' in res:
|
||||
if errnodes is not None:
|
||||
errnodes.add(self._currnoderange)
|
||||
sys.stderr.write('Error: {0}\n'.format(res['error']))
|
||||
if 'errorcode' in res:
|
||||
return res['errorcode']
|
||||
else:
|
||||
return 1
|
||||
if 'databynode' not in res:
|
||||
return 0
|
||||
res = res['databynode']
|
||||
for node in res:
|
||||
if 'error' in res[node]:
|
||||
if errnodes is not None:
|
||||
errnodes.add(node)
|
||||
sys.stderr.write('{0}: Error: {1}\n'.format(
|
||||
node, res[node]['error']))
|
||||
if 'errorcode' in res[node]:
|
||||
rc |= res[node]['errorcode']
|
||||
else:
|
||||
rc |= 1
|
||||
elif ikey in res[node]:
|
||||
if 'value' in res[node][ikey]:
|
||||
val = res[node][ikey]['value']
|
||||
elif 'isset' in res[node][ikey]:
|
||||
val = '********' if res[node][ikey] else ''
|
||||
else:
|
||||
val = repr(res[node][ikey])
|
||||
if self._prevkeyname and self._prevkeyname in res[node]:
|
||||
cprint('{0}: {2}->{1}'.format(
|
||||
node, val, res[node][self._prevkeyname]['value']))
|
||||
elif self._prevdict and node in self._prevdict:
|
||||
cprint('{0}: {2}->{1}'.format(
|
||||
node, val, self._prevdict[node]))
|
||||
else:
|
||||
cprint('{0}: {1}'.format(node, val))
|
||||
elif outhandler:
|
||||
outhandler(node, res)
|
||||
return rc
|
||||
|
||||
async def simple_noderange_command(self, noderange, resource, input=None,
|
||||
key=None, errnodes=None, promptover=None, outhandler=None, **kwargs):
|
||||
try:
|
||||
self._currnoderange = noderange
|
||||
rc = 0
|
||||
if resource[0] == '/':
|
||||
resource = resource[1:]
|
||||
# The implicit key is the resource basename
|
||||
if key is None:
|
||||
ikey = resource.rpartition('/')[-1]
|
||||
else:
|
||||
ikey = key
|
||||
if input is None:
|
||||
async for res in self.read('/noderange/{0}/{1}'.format(
|
||||
noderange, resource)):
|
||||
rc = self.handle_results(ikey, rc, res, errnodes, outhandler)
|
||||
else:
|
||||
await self.stop_if_noderange_over(noderange, promptover)
|
||||
kwargs[ikey] = input
|
||||
async for res in self.update('/noderange/{0}/{1}'.format(
|
||||
noderange, resource), kwargs):
|
||||
rc = self.handle_results(ikey, rc, res, errnodes, outhandler)
|
||||
self._currnoderange = None
|
||||
return rc
|
||||
except KeyboardInterrupt:
|
||||
cprint('')
|
||||
return 0
|
||||
|
||||
async def stop_if_noderange_over(self, noderange, maxnodes):
|
||||
if maxnodes is None:
|
||||
return
|
||||
nsize = await self.get_noderange_size(noderange)
|
||||
if nsize > maxnodes:
|
||||
if nsize == 1:
|
||||
nodename = [x async for x in self.read(
|
||||
'/noderange/{0}/nodes/'.format(noderange))][0].get('item', {}).get('href', None)
|
||||
nodename = nodename[:-1]
|
||||
p = getinput('Command is about to affect node {0}, continue (y/n)? '.format(nodename))
|
||||
else:
|
||||
p = getinput('Command is about to affect {0} nodes, continue (y/n)? '.format(nsize))
|
||||
if p.lower() != 'y':
|
||||
sys.stderr.write('Aborting at user request\n')
|
||||
sys.exit(1)
|
||||
raise Exception("Aborting at user request")
|
||||
|
||||
|
||||
async def get_noderange_size(self, noderange):
|
||||
numnodes = 0
|
||||
async for node in self.read('/noderange/{0}/nodes/'.format(noderange)):
|
||||
if node.get('item', {}).get('href', None):
|
||||
numnodes += 1
|
||||
else:
|
||||
raise Exception("Error trying to size noderange {0}".format(noderange))
|
||||
return numnodes
|
||||
|
||||
async def simple_nodegroups_command(self, noderange, resource, input=None, key=None, **kwargs):
|
||||
try:
|
||||
rc = 0
|
||||
if resource[0] == '/':
|
||||
resource = resource[1:]
|
||||
# The implicit key is the resource basename
|
||||
if key is None:
|
||||
ikey = resource.rpartition('/')[-1]
|
||||
else:
|
||||
ikey = key
|
||||
if input is None:
|
||||
async for res in self.read('/nodegroups/{0}/{1}'.format(
|
||||
noderange, resource)):
|
||||
rc = self.handle_results(ikey, rc, res)
|
||||
else:
|
||||
kwargs[ikey] = input
|
||||
async for res in self.update('/nodegroups/{0}/{1}'.format(
|
||||
noderange, resource), kwargs):
|
||||
rc = self.handle_results(ikey, rc, res)
|
||||
return rc
|
||||
except KeyboardInterrupt:
|
||||
cprint('')
|
||||
return 0
|
||||
|
||||
async def read(self, path, parameters=None):
|
||||
await self.ensure_connected()
|
||||
if not self.authenticated:
|
||||
raise Exception('Unauthenticated')
|
||||
async for rsp in send_request(
|
||||
'retrieve', path, self.connection, parameters):
|
||||
yield rsp
|
||||
|
||||
async def update(self, path, parameters=None):
|
||||
await self.ensure_connected()
|
||||
if not self.authenticated:
|
||||
raise Exception('Unauthenticated')
|
||||
async for rsp in send_request(
|
||||
'update', path, self.connection, parameters):
|
||||
yield rsp
|
||||
|
||||
async def create(self, path, parameters=None):
|
||||
await self.ensure_connected()
|
||||
if not self.authenticated:
|
||||
raise Exception('Unauthenticated')
|
||||
async for rsp in send_request(
|
||||
'create', path, self.connection, parameters):
|
||||
yield rsp
|
||||
|
||||
async def delete(self, path, parameters=None):
|
||||
await self.ensure_connected()
|
||||
if not self.authenticated:
|
||||
raise Exception('Unauthenticated')
|
||||
async for rsp in send_request(
|
||||
'delete', path, self.connection, parameters):
|
||||
yield rsp
|
||||
|
||||
def _connect_unix(self):
|
||||
self.connection = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.connection.setsockopt(socket.SOL_SOCKET, SO_PASSCRED, 1)
|
||||
self.connection.connect(self.serverloc)
|
||||
self.connection.setblocking(False)
|
||||
|
||||
async def _connect_tls(self):
|
||||
server, port = _parseserver(self.serverloc)
|
||||
for res in await asyncio.get_running_loop().getaddrinfo(
|
||||
server, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
|
||||
af, socktype, proto, canonname, sa = res
|
||||
try:
|
||||
self.connection = socket.socket(af, socktype, proto)
|
||||
self.connection.setsockopt(
|
||||
socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
except:
|
||||
self.connection = None
|
||||
continue
|
||||
try:
|
||||
self.connection.settimeout(5)
|
||||
self.connection.connect(sa)
|
||||
self.connection.settimeout(0)
|
||||
except:
|
||||
raise
|
||||
self.connection.close()
|
||||
self.connection = None
|
||||
continue
|
||||
break
|
||||
if self.connection is None:
|
||||
raise Exception("Failed to connect to %s" % self.serverloc)
|
||||
#TODO(jbjohnso): server certificate validation
|
||||
clientcfgdir = os.path.join(os.path.expanduser("~"), ".confluent")
|
||||
try:
|
||||
os.makedirs(clientcfgdir)
|
||||
except OSError as exc:
|
||||
if not (exc.errno == errno.EEXIST and os.path.isdir(clientcfgdir)):
|
||||
raise
|
||||
cacert = os.path.join(clientcfgdir, "ca.pem")
|
||||
certreqs = ssl.CERT_REQUIRED
|
||||
knownhosts = False
|
||||
if not os.path.exists(cacert):
|
||||
cacert = None
|
||||
certreqs = ssl.CERT_NONE
|
||||
knownhosts = True
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
|
||||
ssl_ctx = PySSLContext.from_address(id(ctx)).ctx
|
||||
libssl.SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_stub, 0)
|
||||
sreader = asyncio.StreamReader()
|
||||
sreaderprot = asyncio.StreamReaderProtocol(sreader)
|
||||
cloop = asyncio.get_running_loop()
|
||||
tport, _ = await cloop.create_connection(
|
||||
lambda: sreaderprot, sock=self.connection, ssl=ctx, server_hostname='x')
|
||||
swriter = asyncio.StreamWriter(tport, sreaderprot, sreader, cloop)
|
||||
self.connection = (sreader, swriter)
|
||||
#self.connection = ssl.wrap_socket(self.connection, ca_certs=cacert,
|
||||
# cert_reqs=certreqs)
|
||||
if knownhosts:
|
||||
certdata = tport.get_extra_info('ssl_object').getpeercert(binary_form=True)
|
||||
# certdata = self.connection.getpeercert(binary_form=True)
|
||||
fingerprint = 'sha512$' + hashlib.sha512(certdata).hexdigest()
|
||||
fingerprint = fingerprint.encode('utf-8')
|
||||
hostid = '@'.join((port, server))
|
||||
khf = dbm.open(os.path.join(clientcfgdir, "knownhosts"), 'c', 384)
|
||||
if hostid in khf:
|
||||
if fingerprint == khf[hostid]:
|
||||
return
|
||||
else:
|
||||
replace = getinput(
|
||||
"MISMATCHED CERTIFICATE DATA, ACCEPT NEW? (y/n):")
|
||||
if replace not in ('y', 'Y'):
|
||||
raise Exception("BAD CERTIFICATE")
|
||||
cprint('Adding new key for %s:%s' % (server, port))
|
||||
khf[hostid] = fingerprint
|
||||
|
||||
|
||||
async def send_request(operation, path, server, parameters=None):
|
||||
"""This function iterates over all the responses
|
||||
received from the server.
|
||||
|
||||
:param operation: The operation to request, retrieve, update, delete,
|
||||
create, start, stop
|
||||
:param path: The URI path to the resource to operate on
|
||||
:param server: The socket to send data over
|
||||
:param parameters: Parameters if any to send along with the request
|
||||
"""
|
||||
payload = {'operation': operation, 'path': path}
|
||||
if parameters is not None:
|
||||
payload['parameters'] = parameters
|
||||
await tlvdata.send(server, payload)
|
||||
result = await tlvdata.recv(server)
|
||||
while '_requestdone' not in result:
|
||||
try:
|
||||
yield result
|
||||
except GeneratorExit:
|
||||
while '_requestdone' not in result:
|
||||
result = await tlvdata.recv(server)
|
||||
raise
|
||||
result = await tlvdata.recv(server)
|
||||
|
||||
|
||||
def attrrequested(attr, attrlist, seenattributes, node=None):
|
||||
for candidate in attrlist:
|
||||
truename = candidate
|
||||
if candidate.startswith('hm'):
|
||||
candidate = candidate.replace('hm', 'hardwaremanagement', 1)
|
||||
if candidate in _attraliases:
|
||||
candidate = _attraliases[candidate]
|
||||
if fnmatch.fnmatch(attr.lower(), candidate.lower()):
|
||||
if node is None:
|
||||
seenattributes.add(truename)
|
||||
else:
|
||||
seenattributes[node][truename] = True
|
||||
return True
|
||||
elif attr.lower().startswith(candidate.lower() + '.'):
|
||||
if node is None:
|
||||
seenattributes.add(truename)
|
||||
else:
|
||||
seenattributes[node][truename] = 1
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def printattributes(session, requestargs, showtype, nodetype, noderange, options):
|
||||
path = '/{0}/{1}/attributes/{2}'.format(nodetype, noderange, showtype)
|
||||
return await print_attrib_path(path, session, requestargs, options)
|
||||
|
||||
def _sort_attrib(k):
|
||||
if isinstance(k[1], dict) and k[1].get('sortid', None) is not None:
|
||||
return sortutil.naturalize_string('{}'.format(k[1]['sortid']))
|
||||
return sortutil.naturalize_string(k[0])
|
||||
|
||||
def _is_nondefault(currattr):
|
||||
if currattr.get('default', None) is None:
|
||||
return False # no default specified cannot tell
|
||||
if 'value' not in currattr: # can't compare if no value specified
|
||||
return False
|
||||
if currattr['value'] != currattr['default']:
|
||||
return True
|
||||
# ok, the potentially pending value is the same, but check active
|
||||
if 'active' not in currattr:
|
||||
return False
|
||||
if currattr['active'] != currattr['default']:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def print_attrib_path(path, session, requestargs, options, rename=None, attrprefix=None, showpending=False):
|
||||
exitcode = 0
|
||||
seenattributes = NestedDict()
|
||||
allnodes = set([])
|
||||
async for res in session.read(path):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
continue
|
||||
for node in sorted(res['databynode']):
|
||||
allnodes.add(node)
|
||||
for attr, val in sorted(res['databynode'][node].items(), key=_sort_attrib):
|
||||
if attr == 'error':
|
||||
sys.stderr.write('{0}: Error: {1}\n'.format(node, val))
|
||||
continue
|
||||
if attr == 'errorcode':
|
||||
exitcode |= val
|
||||
continue
|
||||
seenattributes[node][attr] = True
|
||||
if rename:
|
||||
printattr = rename.get(attr, attr)
|
||||
else:
|
||||
printattr = attr
|
||||
if attrprefix:
|
||||
printattr = attrprefix + printattr
|
||||
currattr = res['databynode'][node][attr]
|
||||
if show_attr(attr, requestargs, seenattributes, options, node):
|
||||
if 'value' in currattr:
|
||||
currval = currattr.get('value', None)
|
||||
if isinstance(currval, list):
|
||||
currval = ','.join(currval)
|
||||
activeval = currattr.get('active', currval)
|
||||
if isinstance(activeval, list):
|
||||
activeval = ','.join(activeval)
|
||||
if activeval != currval:
|
||||
if currval is None:
|
||||
currval = ''
|
||||
if activeval is None:
|
||||
activeval = ''
|
||||
if showpending:
|
||||
attrout = f'{node}: {printattr}: {activeval} -> {currval}'
|
||||
else:
|
||||
attrout = f'{node}: {printattr}: {currval} (Pending)'
|
||||
elif showpending:
|
||||
continue
|
||||
elif currval is not None:
|
||||
attrout = '{0}: {1}: {2}'.format(
|
||||
node, printattr, currval).strip()
|
||||
else:
|
||||
attrout = '{0}: {1}:'.format(node, printattr)
|
||||
elif 'isset' in currattr:
|
||||
if currattr['isset']:
|
||||
attrout = '{0}: {1}: ********'.format(node,
|
||||
printattr)
|
||||
else:
|
||||
attrout = '{0}: {1}:'.format(node, printattr)
|
||||
elif isinstance(currattr, dict) and 'broken' in currattr:
|
||||
attrout = '{0}: {1}: *ERROR* BROKEN EXPRESSION: ' \
|
||||
'{2}'.format(node, printattr,
|
||||
currattr['broken'])
|
||||
elif isinstance(currattr, list) or isinstance(currattr, tuple):
|
||||
attrout = '{0}: {1}: {2}'.format(node, attr, ','.join(map(str, currattr)))
|
||||
elif isinstance(currattr, dict):
|
||||
dictout = []
|
||||
for k, v in currattr.items:
|
||||
dictout.append("{0}={1}".format(k, v))
|
||||
attrout = '{0}: {1}: {2}'.format(node, printattr, ','.join(map(str, dictout)))
|
||||
else:
|
||||
cprint("CODE ERROR" + repr(attr))
|
||||
try:
|
||||
blame = options.blame
|
||||
except AttributeError:
|
||||
blame = False
|
||||
if blame or (isinstance(currattr, dict) and 'broken' in currattr):
|
||||
blamedata = []
|
||||
if 'inheritedfrom' in currattr:
|
||||
blamedata.append('inherited from group {0}'.format(
|
||||
currattr['inheritedfrom']
|
||||
))
|
||||
if 'expression' in currattr:
|
||||
blamedata.append(
|
||||
'derived from expression "{0}"'.format(
|
||||
currattr['expression']))
|
||||
if blamedata:
|
||||
attrout += ' (' + ', '.join(blamedata) + ')'
|
||||
try:
|
||||
comparedefault = options.comparedefault
|
||||
except AttributeError:
|
||||
comparedefault = False
|
||||
if comparedefault:
|
||||
try:
|
||||
exclude = options.exclude
|
||||
except AttributeError:
|
||||
exclude = False
|
||||
if ((requestargs and not exclude) or
|
||||
_is_nondefault(currattr)):
|
||||
currval = ','.join(currattr['value']) if isinstance(
|
||||
currattr['value'], list) else currattr['value']
|
||||
activeval = ','.join(currattr['active']) if isinstance(
|
||||
currattr.get('active'), list) else currattr.get('active')
|
||||
dval = ','.join(currattr['default']) if isinstance(
|
||||
currattr['default'], list) else currattr['default']
|
||||
outmsg = '{0}: {1}: {2} (Default: {3}'.format(
|
||||
node, printattr, currval, dval)
|
||||
if 'active' in currattr:
|
||||
outmsg += ', Pending change from: {0}'.format(activeval)
|
||||
outmsg += ')'
|
||||
cprint(outmsg)
|
||||
else:
|
||||
|
||||
try:
|
||||
details = options.detail
|
||||
except AttributeError:
|
||||
details = False
|
||||
if details:
|
||||
if currattr.get('help', None):
|
||||
attrout += u' (Help: {0})'.format(
|
||||
currattr['help'])
|
||||
if currattr.get('possible', None):
|
||||
try:
|
||||
attrout += u' (Choices: {0})'.format(
|
||||
','.join(currattr['possible']))
|
||||
except TypeError:
|
||||
pass
|
||||
cprint(attrout)
|
||||
somematched = set([])
|
||||
printmissing = set([])
|
||||
badnodes = NestedDict()
|
||||
if not exitcode:
|
||||
if requestargs:
|
||||
for attr in requestargs:
|
||||
for node in allnodes:
|
||||
if attr in seenattributes[node]:
|
||||
somematched.add(attr)
|
||||
else:
|
||||
badnodes[node][attr] = True
|
||||
exitcode = 1
|
||||
for node in sortutil.natural_sort(badnodes):
|
||||
for attr in badnodes[node]:
|
||||
if attr in somematched:
|
||||
sys.stderr.write(
|
||||
'Error: {0} matches no valid value for {1}\n'.format(
|
||||
attr, node))
|
||||
else:
|
||||
printmissing.add(attr)
|
||||
for missing in printmissing:
|
||||
sys.stderr.write('Error: {0} not a valid attribute\n'.format(missing))
|
||||
return exitcode
|
||||
|
||||
|
||||
def show_attr(attr, requestargs, seenattributes, options, node):
|
||||
try:
|
||||
reverse = options.exclude
|
||||
except AttributeError:
|
||||
reverse = False
|
||||
if requestargs is None or requestargs == []:
|
||||
return True
|
||||
processattr = attrrequested(attr, requestargs, seenattributes, node)
|
||||
if reverse:
|
||||
processattr = not processattr
|
||||
return processattr
|
||||
|
||||
|
||||
async def printgroupattributes(session, requestargs, showtype, nodetype, noderange, options):
|
||||
exitcode = 0
|
||||
seenattributes = set([])
|
||||
async for res in session.read('/{0}/{1}/attributes/{2}'.format(nodetype, noderange, showtype)):
|
||||
if 'error' in res:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
continue
|
||||
for attr in res:
|
||||
seenattributes.add(attr)
|
||||
currattr = res[attr]
|
||||
if (requestargs is None or requestargs == [] or attrrequested(attr, requestargs, seenattributes)):
|
||||
if 'value' in currattr:
|
||||
if currattr['value'] is not None:
|
||||
attrout = '{0}: {1}: {2}'.format(
|
||||
noderange, attr, currattr['value'])
|
||||
else:
|
||||
attrout = '{0}: {1}:'.format(noderange, attr)
|
||||
elif 'isset' in currattr:
|
||||
if currattr['isset']:
|
||||
attrout = '{0}: {1}: ********'.format(noderange, attr)
|
||||
else:
|
||||
attrout = '{0}: {1}:'.format(noderange, attr)
|
||||
elif isinstance(currattr, dict) and 'broken' in currattr:
|
||||
attrout = '{0}: {1}: *ERROR* BROKEN EXPRESSION: ' \
|
||||
'{2}'.format(noderange, attr,
|
||||
currattr['broken'])
|
||||
elif 'expression' in currattr:
|
||||
attrout = '{0}: {1}: (will derive from expression {2})'.format(noderange, attr, currattr['expression'])
|
||||
elif isinstance(currattr, list) or isinstance(currattr, tuple):
|
||||
attrout = '{0}: {1}: {2}'.format(noderange, attr, ','.join(map(str, currattr)))
|
||||
elif isinstance(currattr, dict):
|
||||
dictout = []
|
||||
for k, v in currattr.items:
|
||||
dictout.append("{0}={1}".format(k, v))
|
||||
attrout = '{0}: {1}: {2}'.format(noderange, attr, ','.join(map(str, dictout)))
|
||||
else:
|
||||
cprint("CODE ERROR" + repr(attr))
|
||||
cprint(attrout)
|
||||
if not exitcode:
|
||||
if requestargs:
|
||||
for attr in requestargs:
|
||||
if attr not in seenattributes:
|
||||
sys.stderr.write('Error: {0} not a valid attribute\n'.format(attr))
|
||||
exitcode = 1
|
||||
return exitcode
|
||||
|
||||
async def updateattrib(session, updateargs, nodetype, noderange, options, dictassign=None):
|
||||
# update attribute
|
||||
exitcode = 0
|
||||
if options.clear:
|
||||
targpath = '/{0}/{1}/attributes/all'.format(nodetype, noderange)
|
||||
keydata = {}
|
||||
for attrib in updateargs[1:]:
|
||||
keydata[attrib] = None
|
||||
async for res in session.update(targpath, keydata):
|
||||
for node in res.get('databynode', {}):
|
||||
for warnmsg in res['databynode'][node].get('_warnings', []):
|
||||
sys.stderr.write('Warning: ' + warnmsg + '\n')
|
||||
if 'error' in res:
|
||||
if 'errorcode' in res:
|
||||
exitcode = res['errorcode']
|
||||
sys.stderr.write('Error: ' + res['error'] + '\n')
|
||||
sys.exit(exitcode)
|
||||
elif hasattr(options, 'environment') and options.environment:
|
||||
for key in updateargs[1:]:
|
||||
key = key.replace('.', '_')
|
||||
value = os.environ.get(
|
||||
key, os.environ[key.upper()])
|
||||
# Let's do one pass to make sure that there's not a usage problem
|
||||
for key in updateargs[1:]:
|
||||
key = key.replace('.', '_')
|
||||
value = os.environ.get(
|
||||
key, os.environ[key.upper()])
|
||||
if (nodetype == "nodegroups"):
|
||||
exitcode = await session.simple_nodegroups_command(noderange,
|
||||
'attributes/all',
|
||||
value, key)
|
||||
else:
|
||||
exitcode = await session.simple_noderange_command(noderange,
|
||||
'attributes/all',
|
||||
value, key)
|
||||
sys.exit(exitcode)
|
||||
elif dictassign:
|
||||
for key in dictassign:
|
||||
if nodetype == 'nodegroups':
|
||||
exitcode = await session.simple_nodegroups_command(
|
||||
noderange, 'attributes/all', dictassign[key], key)
|
||||
else:
|
||||
exitcode = await session.simple_noderange_command(
|
||||
noderange, 'attributes/all', dictassign[key], key)
|
||||
else:
|
||||
if "=" in updateargs[1]:
|
||||
update_ready = True
|
||||
for arg in updateargs[1:]:
|
||||
if '=' not in arg:
|
||||
update_ready = False
|
||||
exitcode = 1
|
||||
if not update_ready:
|
||||
sys.stderr.write('Error: {0} Can not set and read at the same time!\n'.format(str(updateargs[1:])))
|
||||
sys.exit(exitcode)
|
||||
try:
|
||||
for val in updateargs[1:]:
|
||||
val = val.split('=', 1)
|
||||
if val[0][-1] in (',', '-', '^'):
|
||||
key = val[0][:-1]
|
||||
if val[0][-1] == ',':
|
||||
value = {'prepend': val[1]}
|
||||
elif val[0][-1] in ('-', '^'):
|
||||
value = {'remove': val[1]}
|
||||
else:
|
||||
key = val[0]
|
||||
value = val[1]
|
||||
if (nodetype == "nodegroups"):
|
||||
exitcode = await session.simple_nodegroups_command(noderange, 'attributes/all',
|
||||
value, key)
|
||||
else:
|
||||
exitcode = await session.simple_noderange_command(noderange, 'attributes/all',
|
||||
value, key)
|
||||
except Exception:
|
||||
sys.stderr.write('Error: {0} not a valid expression\n'.format(str(updateargs[1:])))
|
||||
exitcode = 1
|
||||
sys.exit(exitcode)
|
||||
return exitcode
|
||||
|
||||
|
||||
# So we try to prevent bad things from happening when globbing
|
||||
# We tried to head this off at the shell, but the various solutions would end
|
||||
# up breaking the shell in various ways (breaking pipe capability if using
|
||||
# DEBUG, breaking globbing if in pipe, etc)
|
||||
# Then we tried to parse the original commandline instead, however shlex isn't
|
||||
# going to parse full bourne language (e.g. knowing that '|' and '>' and
|
||||
# a world of other things would not be in our command line
|
||||
# so finally, just make sure the noderange appears verbatim in the command line
|
||||
# if we glob to something, then bash will change noderange and this should
|
||||
# detect it and save the user from tragedy
|
||||
def check_globbing(noderange):
|
||||
if not os.path.exists(noderange):
|
||||
return True
|
||||
rawargs = os.environ.get('CURRENT_CMDLINE', None)
|
||||
if rawargs:
|
||||
rawargs = shlex.split(rawargs)
|
||||
for arg in rawargs:
|
||||
if arg.startswith('$'):
|
||||
arg = arg[1:]
|
||||
if arg.endswith(';'):
|
||||
arg = arg[:-1]
|
||||
arg = os.environ.get(arg, '$' + arg)
|
||||
if arg.startswith(noderange):
|
||||
break
|
||||
else:
|
||||
sys.stderr.write(
|
||||
'Shell glob conflict detected, specified target "{0}" '
|
||||
'not in command line, but is a file. You can use "set -f" in '
|
||||
'bash or change directories such that there is no filename '
|
||||
'that would conflict.'
|
||||
'\n'.format(noderange))
|
||||
sys.exit(1)
|
||||
@@ -1,313 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
# Copyright 2015 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.
|
||||
|
||||
import array
|
||||
import asyncio
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import confluent.tlv as tlv
|
||||
import socket
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
|
||||
try:
|
||||
unicode
|
||||
except NameError:
|
||||
unicode = str
|
||||
|
||||
try:
|
||||
range = xrange
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
|
||||
class iovec(ctypes.Structure): # from uio.h
|
||||
_fields_ = [('iov_base', ctypes.c_void_p),
|
||||
('iov_len', ctypes.c_size_t)]
|
||||
|
||||
|
||||
iovec_ptr = ctypes.POINTER(iovec)
|
||||
|
||||
|
||||
class cmsghdr(ctypes.Structure): # also from bits/socket.h
|
||||
_fields_ = [('cmsg_len', ctypes.c_size_t),
|
||||
('cmsg_level', ctypes.c_int),
|
||||
('cmsg_type', ctypes.c_int)]
|
||||
|
||||
@classmethod
|
||||
def init_data(cls, cmsg_len, cmsg_level, cmsg_type, cmsg_data):
|
||||
Data = ctypes.c_ubyte * ctypes.sizeof(cmsg_data)
|
||||
|
||||
class _flexhdr(ctypes.Structure):
|
||||
_fields_ = cls._fields_ + [('cmsg_data', Data)]
|
||||
|
||||
datab = Data(*bytearray(cmsg_data))
|
||||
return _flexhdr(cmsg_len=cmsg_len, cmsg_level=cmsg_level,
|
||||
cmsg_type=cmsg_type, cmsg_data=datab)
|
||||
|
||||
|
||||
def CMSG_LEN(length):
|
||||
sizeof_cmshdr = ctypes.sizeof(cmsghdr)
|
||||
return ctypes.c_size_t(CMSG_ALIGN(sizeof_cmshdr).value + length)
|
||||
|
||||
|
||||
SCM_RIGHTS = 1
|
||||
|
||||
|
||||
class msghdr(ctypes.Structure): # from bits/socket.h
|
||||
_fields_ = [('msg_name', ctypes.c_void_p),
|
||||
('msg_namelen', ctypes.c_uint),
|
||||
('msg_iov', ctypes.POINTER(iovec)),
|
||||
('msg_iovlen', ctypes.c_size_t),
|
||||
('msg_control', ctypes.c_void_p),
|
||||
('msg_controllen', ctypes.c_size_t),
|
||||
('msg_flags', ctypes.c_int)]
|
||||
|
||||
|
||||
def CMSG_ALIGN(length): # bits/socket.h
|
||||
ret = (length + ctypes.sizeof(ctypes.c_size_t) - 1
|
||||
& ~(ctypes.sizeof(ctypes.c_size_t) - 1))
|
||||
return ctypes.c_size_t(ret)
|
||||
|
||||
|
||||
def CMSG_SPACE(length): # bits/socket.h
|
||||
ret = CMSG_ALIGN(length).value + CMSG_ALIGN(ctypes.sizeof(cmsghdr)).value
|
||||
return ctypes.c_size_t(ret)
|
||||
|
||||
|
||||
class ClientFile(object):
|
||||
def __init__(self, name, mode, fd):
|
||||
self.fileobject = os.fdopen(fd, mode)
|
||||
self.filename = name
|
||||
|
||||
|
||||
|
||||
|
||||
def _sendmsg(loop, fut, sock, msg, fds, wfd):
|
||||
if wfd is not None:
|
||||
loop.remove_writer(wfd)
|
||||
if fut.cancelled():
|
||||
return
|
||||
try:
|
||||
retdata = sock.sendmsg(
|
||||
[msg],
|
||||
[(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array("i", fds))])
|
||||
except (BlockingIOError, InterruptedError):
|
||||
fd = sock.fileno()
|
||||
loop.add_writer(fd, _sendmsg, loop, fut, sock, msg, fds, fd)
|
||||
except Exception as exc:
|
||||
fut.set_exception(exc)
|
||||
else:
|
||||
fut.set_result(retdata)
|
||||
|
||||
|
||||
def send_fds(sock, msg, fds):
|
||||
cloop = asyncio.get_running_loop()
|
||||
fut = cloop.create_future()
|
||||
_sendmsg(cloop, fut, sock, msg, fds, None)
|
||||
return fut
|
||||
|
||||
|
||||
def _recvmsg(loop, fut, sock, msglen, maxfds, rfd):
|
||||
if rfd is not None:
|
||||
loop.remove_reader(rfd)
|
||||
if fut.cancelled():
|
||||
return
|
||||
fds = array.array("i") # Array of ints
|
||||
try:
|
||||
msg, ancdata, flags, addr = sock.recvmsg(
|
||||
msglen, socket.CMSG_LEN(maxfds * fds.itemsize))
|
||||
except (BlockingIOError, InterruptedError):
|
||||
fd = sock.fileno()
|
||||
loop.add_reader(fd, _recvmsg, loop, fut, sock, msglen, maxfds, fd)
|
||||
except Exception as exc:
|
||||
fut.set_exception(exc)
|
||||
else:
|
||||
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
||||
if (cmsg_level == socket.SOL_SOCKET
|
||||
and cmsg_type == socket.SCM_RIGHTS):
|
||||
# Append data, ignoring any truncated integers at the end.
|
||||
fds.frombytes(
|
||||
cmsg_data[
|
||||
:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
|
||||
fut.set_result((msg, list(fds)))
|
||||
|
||||
|
||||
def recv_fds(sock, msglen, maxfds):
|
||||
cloop = asyncio.get_running_loop()
|
||||
fut = cloop.create_future()
|
||||
_recvmsg(cloop, fut, sock, msglen, maxfds, None)
|
||||
return fut
|
||||
|
||||
|
||||
def decodestr(value):
|
||||
ret = None
|
||||
try:
|
||||
ret = value.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
try:
|
||||
ret = value.decode('cp437')
|
||||
except UnicodeDecodeError:
|
||||
ret = value
|
||||
except AttributeError:
|
||||
return value
|
||||
return ret
|
||||
|
||||
|
||||
def unicode_dictvalues(dictdata):
|
||||
for key in dictdata:
|
||||
if isinstance(dictdata[key], bytes):
|
||||
dictdata[key] = decodestr(dictdata[key])
|
||||
elif isinstance(dictdata[key], datetime):
|
||||
dictdata[key] = dictdata[key].strftime('%Y-%m-%dT%H:%M:%S')
|
||||
elif isinstance(dictdata[key], list):
|
||||
_unicode_list(dictdata[key])
|
||||
elif isinstance(dictdata[key], dict):
|
||||
unicode_dictvalues(dictdata[key])
|
||||
|
||||
|
||||
def _unicode_list(currlist):
|
||||
for i in range(len(currlist)):
|
||||
if isinstance(currlist[i], str):
|
||||
currlist[i] = decodestr(currlist[i])
|
||||
elif isinstance(currlist[i], dict):
|
||||
unicode_dictvalues(currlist[i])
|
||||
elif isinstance(currlist[i], list):
|
||||
_unicode_list(currlist[i])
|
||||
|
||||
|
||||
async def sendall(handle, data):
|
||||
if isinstance(handle, tuple):
|
||||
handle[1].write(data)
|
||||
return await handle[1].drain()
|
||||
else:
|
||||
cloop = asyncio.get_running_loop()
|
||||
return await cloop.sock_sendall(handle, data)
|
||||
|
||||
def get_socket(handle):
|
||||
if isinstance(handle, tuple):
|
||||
return handle[1].transport.get_extra_info('socket')
|
||||
else:
|
||||
return handle
|
||||
|
||||
async def close(handle):
|
||||
if isinstance(handle, tuple):
|
||||
handle[1].close()
|
||||
try:
|
||||
await handle[1].wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
handle.close()
|
||||
|
||||
async def send(handle, data, filehandle=None):
|
||||
cloop = asyncio.get_running_loop()
|
||||
if isinstance(data, unicode):
|
||||
try:
|
||||
data = data.encode('utf-8')
|
||||
except AttributeError:
|
||||
pass
|
||||
if isinstance(data, bytes) or isinstance(data, unicode):
|
||||
# plain text, e.g. console data
|
||||
tl = len(data)
|
||||
if tl == 0:
|
||||
# if you don't have anything to say, don't say anything at all
|
||||
return
|
||||
if tl < 16777216:
|
||||
# type for string is '0', so we don't need
|
||||
# to xor anything in
|
||||
await sendall(handle, struct.pack("!I", tl))
|
||||
else:
|
||||
raise Exception("String data length exceeds protocol")
|
||||
await sendall(handle, data)
|
||||
elif isinstance(data, dict): # JSON currently only goes to 4 bytes
|
||||
# Some structured message, like what would be seen in http responses
|
||||
unicode_dictvalues(data) # make everything unicode, assuming UTF-8
|
||||
sdata = json.dumps(data, ensure_ascii=False, separators=(',', ':'))
|
||||
sdata = sdata.encode('utf-8')
|
||||
tl = len(sdata)
|
||||
if tl > 16777215:
|
||||
raise Exception("JSON data exceeds protocol limits")
|
||||
# xor in the type (0b1 << 24)
|
||||
if filehandle is None:
|
||||
tl |= 16777216
|
||||
await sendall(handle, struct.pack("!I", tl))
|
||||
await sendall(handle, sdata)
|
||||
elif isinstance(handle, tuple):
|
||||
raise Exception("Cannot send filehandle over network socket")
|
||||
else:
|
||||
tl |= (2 << 24)
|
||||
await cloop.sock_sendall(handle, struct.pack("!I", tl))
|
||||
await send_fds(handle, sdata, [filehandle])
|
||||
|
||||
|
||||
async def _grabhdl(handle, size):
|
||||
if isinstance(handle, tuple):
|
||||
return await handle[0].read(size)
|
||||
else:
|
||||
cloop = asyncio.get_running_loop()
|
||||
return await cloop.sock_recv(handle, size)
|
||||
|
||||
|
||||
async def recvall(handle, size):
|
||||
rd = await _grabhdl(handle, size)
|
||||
while len(rd) < size:
|
||||
nd = await _grabhdl(handle, size - len(rd))
|
||||
if not nd:
|
||||
raise Exception("Error reading data")
|
||||
rd += nd
|
||||
return rd
|
||||
|
||||
|
||||
async def recv(handle):
|
||||
tl = await _grabhdl(handle, 4)
|
||||
if not tl:
|
||||
return None
|
||||
while len(tl) < 4:
|
||||
ndata = await _grabhdl(handle, 4 - len(tl))
|
||||
if not ndata:
|
||||
raise Exception("Error reading data")
|
||||
tl += ndata
|
||||
if len(tl) == 0:
|
||||
return None
|
||||
tl = struct.unpack("!I", tl)[0]
|
||||
if tl & 0b10000000000000000000000000000000:
|
||||
raise Exception("Protocol Violation, reserved bit set")
|
||||
# 4 byte tlv
|
||||
dlen = tl & 16777215 # grab lower 24 bits
|
||||
datatype = (tl & 2130706432) >> 24 # grab 7 bits from near beginning
|
||||
if dlen == 0:
|
||||
return None
|
||||
if datatype == tlv.Types.filehandle:
|
||||
if isinstance(handle, tuple):
|
||||
raise Exception('Filehandle not supported over TLS socket')
|
||||
msg, filehandles = await recv_fds(handle, dlen, 4)
|
||||
data = json.loads(bytes(msg))
|
||||
return ClientFile(data['filename'], data['mode'], filehandles[0])
|
||||
else:
|
||||
data = await _grabhdl(handle, dlen)
|
||||
while len(data) < dlen:
|
||||
ndata = await _grabhdl(handle, dlen - len(data))
|
||||
if not ndata:
|
||||
raise Exception("Error reading data")
|
||||
data += ndata
|
||||
if datatype == tlv.Types.text:
|
||||
return data
|
||||
elif datatype == tlv.Types.json:
|
||||
return json.loads(data)
|
||||
@@ -280,7 +280,6 @@ class Command(object):
|
||||
def stop_if_noderange_over(self, noderange, maxnodes):
|
||||
if maxnodes is None:
|
||||
return
|
||||
maxnodes = int(maxnodes)
|
||||
nsize = self.get_noderange_size(noderange)
|
||||
if nsize > maxnodes:
|
||||
if nsize == 1:
|
||||
@@ -473,21 +472,7 @@ def _sort_attrib(k):
|
||||
return sortutil.naturalize_string('{}'.format(k[1]['sortid']))
|
||||
return sortutil.naturalize_string(k[0])
|
||||
|
||||
def _is_nondefault(currattr):
|
||||
if currattr.get('default', None) is None:
|
||||
return False # no default specified cannot tell
|
||||
if 'value' not in currattr: # can't compare if no value specified
|
||||
return False
|
||||
if currattr['value'] != currattr['default']:
|
||||
return True
|
||||
# ok, the potentially pending value is the same, but check active
|
||||
if 'active' not in currattr:
|
||||
return False
|
||||
if currattr['active'] != currattr['default']:
|
||||
return True
|
||||
return False
|
||||
|
||||
def print_attrib_path(path, session, requestargs, options, rename=None, attrprefix=None, showpending=False):
|
||||
def print_attrib_path(path, session, requestargs, options, rename=None, attrprefix=None):
|
||||
exitcode = 0
|
||||
seenattributes = NestedDict()
|
||||
allnodes = set([])
|
||||
@@ -515,26 +500,12 @@ def print_attrib_path(path, session, requestargs, options, rename=None, attrpref
|
||||
currattr = res['databynode'][node][attr]
|
||||
if show_attr(attr, requestargs, seenattributes, options, node):
|
||||
if 'value' in currattr:
|
||||
currval = currattr.get('value', None)
|
||||
if isinstance(currval, list):
|
||||
currval = ','.join(currval)
|
||||
activeval = currattr.get('active', currval)
|
||||
if isinstance(activeval, list):
|
||||
activeval = ','.join(activeval)
|
||||
if activeval != currval:
|
||||
if currval is None:
|
||||
currval = ''
|
||||
if activeval is None:
|
||||
activeval = ''
|
||||
if showpending:
|
||||
attrout = f'{node}: {printattr}: {activeval} -> {currval}'
|
||||
else:
|
||||
attrout = f'{node}: {printattr}: {currval} (Pending)'
|
||||
elif showpending:
|
||||
continue
|
||||
elif currval is not None:
|
||||
if currattr['value'] is not None:
|
||||
val = currattr['value']
|
||||
if isinstance(val, list):
|
||||
val = ','.join(val)
|
||||
attrout = '{0}: {1}: {2}'.format(
|
||||
node, printattr, currval).strip()
|
||||
node, printattr, val).strip()
|
||||
else:
|
||||
attrout = '{0}: {1}:'.format(node, printattr)
|
||||
elif 'isset' in currattr:
|
||||
@@ -582,19 +553,15 @@ def print_attrib_path(path, session, requestargs, options, rename=None, attrpref
|
||||
except AttributeError:
|
||||
exclude = False
|
||||
if ((requestargs and not exclude) or
|
||||
_is_nondefault(currattr)):
|
||||
(currattr.get('default', None) is not None and
|
||||
currattr.get('value', None) is not None and
|
||||
currattr['value'] != currattr['default'])):
|
||||
cval = ','.join(currattr['value']) if isinstance(
|
||||
currattr['value'], list) else currattr['value']
|
||||
activeval = ','.join(currattr['active']) if isinstance(
|
||||
currattr.get('active'), list) else currattr.get('active')
|
||||
dval = ','.join(currattr['default']) if isinstance(
|
||||
currattr['default'], list) else currattr['default']
|
||||
outmsg = '{0}: {1}: {2} (Default: {3}'.format(
|
||||
node, printattr, cval, dval)
|
||||
if 'active' in currattr:
|
||||
outmsg += ', Pending change from: {0}'.format(activeval)
|
||||
outmsg += ')'
|
||||
cprint(outmsg)
|
||||
cprint('{0}: {1}: {2} (Default: {3})'.format(
|
||||
node, printattr, cval, dval))
|
||||
else:
|
||||
|
||||
try:
|
||||
@@ -715,31 +682,23 @@ def updateattrib(session, updateargs, nodetype, noderange, options, dictassign=N
|
||||
sys.stderr.write('Error: ' + res['error'] + '\n')
|
||||
sys.exit(exitcode)
|
||||
elif hasattr(options, 'environment') and options.environment:
|
||||
# The environment variable name substitutes '_' for '.'
|
||||
attrvalues = {}
|
||||
# Let's do one pass to make sure that there's not a usage problem
|
||||
for attrib in updateargs[1:]:
|
||||
envkey = attrib.replace('.', '_')
|
||||
if envkey in os.environ:
|
||||
attrvalues[attrib] = os.environ[envkey]
|
||||
elif envkey.upper() in os.environ:
|
||||
attrvalues[attrib] = os.environ[envkey.upper()]
|
||||
else:
|
||||
sys.stderr.write(
|
||||
'Error: {0} requested, but neither {1} nor {2} is set in '
|
||||
'the environment\n'.format(attrib, envkey, envkey.upper()))
|
||||
sys.exit(1)
|
||||
for attrib in updateargs[1:]:
|
||||
for key in updateargs[1:]:
|
||||
key = key.replace('.', '_')
|
||||
value = os.environ.get(
|
||||
key, os.environ[key.upper()])
|
||||
# Let's do one pass to make sure that there's not a usage problem
|
||||
for key in updateargs[1:]:
|
||||
key = key.replace('.', '_')
|
||||
value = os.environ.get(
|
||||
key, os.environ[key.upper()])
|
||||
if (nodetype == "nodegroups"):
|
||||
exitcode = session.simple_nodegroups_command(noderange,
|
||||
'attributes/all',
|
||||
attrvalues[attrib],
|
||||
attrib)
|
||||
value, key)
|
||||
else:
|
||||
exitcode = session.simple_noderange_command(noderange,
|
||||
'attributes/all',
|
||||
attrvalues[attrib],
|
||||
attrib)
|
||||
value, key)
|
||||
sys.exit(exitcode)
|
||||
elif dictassign:
|
||||
for key in dictassign:
|
||||
@@ -753,7 +712,7 @@ def updateattrib(session, updateargs, nodetype, noderange, options, dictassign=N
|
||||
if "=" in updateargs[1]:
|
||||
update_ready = True
|
||||
for arg in updateargs[1:]:
|
||||
if '=' not in arg:
|
||||
if not '=' in arg:
|
||||
update_ready = False
|
||||
exitcode = 1
|
||||
if not update_ready:
|
||||
|
||||
@@ -175,10 +175,6 @@ class LogReplay(object):
|
||||
|
||||
|
||||
def _replay_to_console(txtfile, binfile):
|
||||
if not sys.stdin.isatty():
|
||||
# Interactive replay needs a terminal to put in raw mode and to take
|
||||
# navigation keys from, so without one just write the log out
|
||||
return dump_to_console(txtfile)
|
||||
replay = LogReplay(txtfile, binfile)
|
||||
oldtcattr = termios.tcgetattr(sys.stdin.fileno())
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
|
||||
@@ -16,55 +16,11 @@ import fcntl
|
||||
import sys
|
||||
import struct
|
||||
import termios
|
||||
import select
|
||||
|
||||
def get_screengeom(escfallback=False):
|
||||
def get_screengeom():
|
||||
# returns height in cells, width in cells, width in pixels, height in pixels
|
||||
geom = list(struct.unpack('hhhh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ,
|
||||
b'........')))
|
||||
if escfallback and (geom[0] == 0 or geom[1] == 0):
|
||||
cellgeom = get_cell_geometry_using_esc_18t()
|
||||
geom[0], geom[1] = cellgeom
|
||||
if escfallback and (geom[2] == 0 or geom[3] == 0):
|
||||
pixgeom = get_pixel_geometry_using_esc_14t()
|
||||
geom[2], geom[3] = pixgeom
|
||||
return geom
|
||||
|
||||
def get_pixel_geometry_using_esc_14t():
|
||||
sys.stdout.write('\x1b[14t')
|
||||
sys.stdout.flush()
|
||||
rlist, _, _ = select.select([sys.stdin], [], [], 1)
|
||||
if not rlist:
|
||||
return 0, 0
|
||||
response = ''
|
||||
while True:
|
||||
c = sys.stdin.read(1)
|
||||
if c == 't':
|
||||
break
|
||||
response += c
|
||||
if not response.startswith('\x1b[4;'):
|
||||
return 0, 0
|
||||
pixgeom = response[4:].split(';')
|
||||
return int(pixgeom[1]), int(pixgeom[0])
|
||||
|
||||
def get_cell_geometry_using_esc_18t():
|
||||
sys.stdout.write('\x1b[18t')
|
||||
sys.stdout.flush()
|
||||
rlist, _, _ = select.select([sys.stdin], [], [], 1)
|
||||
if not rlist:
|
||||
return 0, 0
|
||||
response = ''
|
||||
while True:
|
||||
c = sys.stdin.read(1)
|
||||
if c == 't':
|
||||
break
|
||||
response += c
|
||||
if not response.startswith('\x1b[8;'):
|
||||
return 0, 0
|
||||
cellgeom = response[4:].split(';')
|
||||
return int(cellgeom[0]), int(cellgeom[1])
|
||||
|
||||
|
||||
return struct.unpack('hhhh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ,
|
||||
b'........'))
|
||||
class ScreenPrinter(object):
|
||||
|
||||
def __init__(self, noderange, client, textlen=4):
|
||||
|
||||
@@ -138,6 +138,8 @@ class GroupedData(object):
|
||||
def print_all(self, output=sys.stdout, skipmodal=False, reverse=False,
|
||||
count=False):
|
||||
self.generate_byoutput()
|
||||
modaloutput = None
|
||||
ismodal = True
|
||||
|
||||
if reverse:
|
||||
outdatalist = sorted(
|
||||
|
||||
@@ -19,8 +19,12 @@ import array
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import confluent.tlv as tlv
|
||||
import socket
|
||||
import select
|
||||
try:
|
||||
import eventlet.green.socket as socket
|
||||
import eventlet.green.select as select
|
||||
except ImportError:
|
||||
import socket
|
||||
import select
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
|
||||
@@ -1,346 +0,0 @@
|
||||
import asyncio
|
||||
from PIL import Image
|
||||
import io
|
||||
import numpy as np
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
import zlib
|
||||
|
||||
# This results in an RGBA organization of pixels
|
||||
MYPIXFORMAT = bytearray([
|
||||
32, # bits per pixel
|
||||
24, # depth
|
||||
0, # big endian
|
||||
1, # true color
|
||||
0, 255, # red max
|
||||
0, 255, # green max
|
||||
0, 255, # blue max
|
||||
0, 8, 16, # red shift, green shift, blue shift
|
||||
0, 0, 0 # padding
|
||||
])
|
||||
|
||||
class ByteStream:
|
||||
def __init__(self):
|
||||
self.buffer = b''
|
||||
|
||||
def add_number(self, number, num_bytes):
|
||||
data = number.to_bytes(num_bytes, byteorder='big', signed=True)
|
||||
self.buffer += data
|
||||
|
||||
def extend(self, data):
|
||||
self.buffer += data
|
||||
|
||||
def get_bytes(self):
|
||||
return self.buffer
|
||||
|
||||
def clear(self):
|
||||
self.buffer = b''
|
||||
|
||||
def flush(self, writer):
|
||||
writer.write(self.buffer)
|
||||
self.clear()
|
||||
|
||||
class VNCClient:
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.close()
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def create(cls, url, outputfile=None, fps=15):
|
||||
self = cls()
|
||||
self.outputfile = outputfile
|
||||
self.fps = fps
|
||||
self.video_writer = None
|
||||
self._video_size = None
|
||||
self._last_frame = None
|
||||
self._last_frame_time = None
|
||||
self._video_queue = None
|
||||
self._video_thread = None
|
||||
self._cv2 = None
|
||||
if outputfile:
|
||||
try:
|
||||
import cv2
|
||||
except ImportError:
|
||||
raise ImportError("OpenCV is required for video output but is not installed.")
|
||||
self._cv2 = cv2
|
||||
self._video_queue = queue.Queue()
|
||||
self._video_thread = threading.Thread(
|
||||
target=self._video_worker, daemon=True)
|
||||
self._video_thread.start()
|
||||
if url.startswith('unix://'):
|
||||
url = url.replace('unix://', '')
|
||||
if url.startswith('/'):
|
||||
self.reader, self.writer = await asyncio.open_unix_connection(url)
|
||||
elif url.startswith('@'):
|
||||
url = '\0' + url[1:]
|
||||
self.reader, self.writer = await asyncio.open_unix_connection(url)
|
||||
elif url.startswith('tcp://'):
|
||||
url = url.replace('tcp://', '')
|
||||
host, port = url.split(':')
|
||||
self.reader, self.writer = await asyncio.open_connection(host, int(port))
|
||||
else:
|
||||
raise ValueError('Unsupported URL: {}'.format(url))
|
||||
self.receiver = None
|
||||
self.framebuffer = None
|
||||
self.copytext = None
|
||||
self._updating = True
|
||||
self.decompressor = zlib.decompressobj()
|
||||
self._input_queue = asyncio.Queue()
|
||||
self._input_task = asyncio.create_task(self._input_worker())
|
||||
await self._do_vnc_handshake()
|
||||
return self
|
||||
|
||||
async def _input_worker(self):
|
||||
while True:
|
||||
keys, modifierkeys = await self._input_queue.get()
|
||||
payload = ByteStream()
|
||||
for modkey in (modifierkeys or []):
|
||||
payload.add_number(4, 1) # Key event
|
||||
payload.add_number(1, 1) # Down
|
||||
payload.add_number(0, 2) # Padding
|
||||
payload.add_number(modkey.value, 4)
|
||||
payload.flush(self.writer)
|
||||
await self.writer.drain()
|
||||
for key in keys:
|
||||
keynumber = key.value if hasattr(key, 'value') else key
|
||||
payload.add_number(4, 1) # Key event
|
||||
payload.add_number(1, 1) # Down
|
||||
payload.add_number(0, 2) # Padding
|
||||
payload.add_number(keynumber, 4)
|
||||
payload.add_number(4, 1) # Key event
|
||||
payload.add_number(0, 1) # Up
|
||||
payload.add_number(0, 2) # Padding
|
||||
payload.add_number(keynumber, 4)
|
||||
payload.flush(self.writer)
|
||||
await self.writer.drain()
|
||||
for modkey in (modifierkeys or []):
|
||||
payload.add_number(4, 1) # Key event
|
||||
payload.add_number(0, 1) # Up
|
||||
payload.add_number(0, 2) # Padding
|
||||
payload.add_number(modkey.value, 4)
|
||||
payload.flush(self.writer)
|
||||
await self.writer.drain()
|
||||
await asyncio.sleep(0.01) # Have to slow down keypresses for some servers
|
||||
# Still shouldn't be noticable interactively, but does slow down paste to a fast typist...
|
||||
self._input_queue.task_done()
|
||||
|
||||
async def send_keypresses(self, keys, modifierkeys=None):
|
||||
await self._input_queue.put((keys, modifierkeys))
|
||||
|
||||
async def _read_number(self, num_bytes):
|
||||
data = await self.reader.readexactly(num_bytes)
|
||||
return int.from_bytes(data, byteorder='big', signed=True)
|
||||
|
||||
def _write_number(self, number, num_bytes):
|
||||
data = number.to_bytes(num_bytes, byteorder='big', signed=True)
|
||||
self.writer.write(data)
|
||||
return data
|
||||
|
||||
async def get_screenshot(self):
|
||||
while self._updating:
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.sleep(0)
|
||||
if self.framebuffer is None:
|
||||
raise Exception('No framebuffer data available')
|
||||
self._updating = True
|
||||
return self.framebuffer.copy()
|
||||
|
||||
async def _do_vnc_handshake(self):
|
||||
rfbver = await self.reader.readline()
|
||||
if not rfbver.startswith(b'RFB 003.008'):
|
||||
self.writer.close()
|
||||
await self.writer.wait_closed()
|
||||
raise Exception('Unsupported RFB version')
|
||||
self.writer.write(b'RFB 003.008\n')
|
||||
numsectypes = await self._read_number(1)
|
||||
if not numsectypes:
|
||||
self.writer.close()
|
||||
await self.writer.wait_closed()
|
||||
raise Exception('No security types supported by the server')
|
||||
sectypes = await self.reader.readexactly(numsectypes)
|
||||
sectypes = bytearray(sectypes)
|
||||
secresult = 1
|
||||
if 1 in sectypes:
|
||||
self.writer.write(b'\x01')
|
||||
await self.writer.drain()
|
||||
secresult = await self._read_number(4) # Security result
|
||||
if secresult != 0:
|
||||
self.writer.close()
|
||||
await self.writer.wait_closed()
|
||||
raise Exception('VNC authentication failed')
|
||||
self.writer.write(b'\x01') # Share display
|
||||
self.width = await self._read_number(2)
|
||||
self.height = await self._read_number(2)
|
||||
pixformat = await self.reader.readexactly(16)
|
||||
name_length = await self._read_number(4)
|
||||
self.name = await self.reader.readexactly(name_length)
|
||||
payload = ByteStream()
|
||||
if pixformat != MYPIXFORMAT:
|
||||
payload.add_number(0, 1) # Set pixel format
|
||||
payload.add_number(0, 3) # Padding
|
||||
payload.extend(MYPIXFORMAT)
|
||||
payload.flush(self.writer)
|
||||
self.receiver = asyncio.create_task(self._receive_loop())
|
||||
payload.add_number(2, 1) # Set encodings
|
||||
payload.add_number(0, 1) # Padding
|
||||
payload.add_number(4, 2) # Number of encodings
|
||||
payload.add_number(6, 4) # zlib
|
||||
payload.add_number(7, 4) # tight
|
||||
payload.add_number(-223, 4) # desktopsize
|
||||
payload.add_number(-308, 4) # extended desktopsize
|
||||
payload.flush(self.writer)
|
||||
self._request_screen_update(incremental=False)
|
||||
|
||||
def _request_screen_update(self, incremental=True):
|
||||
incremental = 1 if incremental else 0
|
||||
payload = ByteStream()
|
||||
payload.add_number(3, 1) # Framebuffer update request
|
||||
payload.add_number(incremental, 1) # Incremental
|
||||
payload.add_number(0, 2) # x position
|
||||
payload.add_number(0, 2) # y position
|
||||
payload.add_number(self.width, 2) # width
|
||||
payload.add_number(self.height, 2) # height
|
||||
payload.flush(self.writer)
|
||||
|
||||
async def _receive_loop(self):
|
||||
while True:
|
||||
try:
|
||||
message_type = await self._read_number(1)
|
||||
if message_type == 0: # Framebuffer update
|
||||
await self._handle_framebuffer_update()
|
||||
elif message_type == 1: # Set color map entries
|
||||
raise NotImplementedError('Set color map entries not implemented')
|
||||
elif message_type == 2: # Bell
|
||||
pass
|
||||
elif message_type == 3: # Server cut text
|
||||
padding = await self._read_number(3)
|
||||
length = await self._read_number(4)
|
||||
self.copytext = await self.reader.readexactly(length)
|
||||
else:
|
||||
raise Exception(f'Unknown message type: {message_type}')
|
||||
except Exception as e:
|
||||
print(f"Error in receive loop: {e}")
|
||||
break
|
||||
|
||||
async def _handle_framebuffer_update(self):
|
||||
_ = await self._read_number(1) # Padding
|
||||
num_rects = await self._read_number(2)
|
||||
self._updating = True
|
||||
for _ in range(num_rects):
|
||||
await self._handle_rectangle()
|
||||
self._updating = False
|
||||
self._write_video_frame()
|
||||
self._request_screen_update(incremental=True)
|
||||
|
||||
def _write_video_frame(self):
|
||||
if not self._cv2 or self.framebuffer is None:
|
||||
return
|
||||
# Snapshot the framebuffer now and hand it to the writer thread. Frames
|
||||
# captured while a write is in progress simply queue up behind it.
|
||||
frame = np.ascontiguousarray(
|
||||
np.array(self.framebuffer.convert('RGB'))[:, :, ::-1])
|
||||
self._video_queue.put((frame, time.monotonic()))
|
||||
|
||||
def _video_worker(self):
|
||||
cv2 = self._cv2
|
||||
while True:
|
||||
frame, now = self._video_queue.get()
|
||||
if frame is None:
|
||||
# Flush the final frame for the time it stayed on screen
|
||||
if self.video_writer is not None and self._last_frame is not None:
|
||||
nframes = max(1, round(
|
||||
(now - self._last_frame_time) * self.fps))
|
||||
for _ in range(nframes):
|
||||
self.video_writer.write(self._last_frame)
|
||||
if self.video_writer is not None:
|
||||
self.video_writer.release()
|
||||
self.video_writer = None
|
||||
return
|
||||
if self.video_writer is None:
|
||||
self._video_size = (frame.shape[1], frame.shape[0])
|
||||
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
||||
self.video_writer = cv2.VideoWriter(
|
||||
self.outputfile, fourcc, self.fps, self._video_size)
|
||||
if (frame.shape[1], frame.shape[0]) != self._video_size:
|
||||
frame = cv2.resize(frame, self._video_size)
|
||||
if self._last_frame is None:
|
||||
self._last_frame = frame
|
||||
self._last_frame_time = now
|
||||
continue
|
||||
# Hold the previous frame for the real time it was displayed
|
||||
nframes = max(1, round((now - self._last_frame_time) * self.fps))
|
||||
for _ in range(nframes):
|
||||
self.video_writer.write(self._last_frame)
|
||||
self._last_frame = frame
|
||||
self._last_frame_time = now
|
||||
|
||||
async def _handle_rectangle(self):
|
||||
if self.framebuffer is None:
|
||||
self.framebuffer = Image.new('RGBA', (self.width, self.height))
|
||||
x = await self._read_number(2)
|
||||
y = await self._read_number(2)
|
||||
width = await self._read_number(2)
|
||||
height = await self._read_number(2)
|
||||
encoding_type = await self._read_number(4)
|
||||
pixel_data = None
|
||||
if encoding_type == 6:
|
||||
compressed_data_length = await self._read_number(4)
|
||||
compressed_data = await self.reader.readexactly(compressed_data_length)
|
||||
# Decompress the data using zlib and store it in the framebuffer
|
||||
pixel_data = self.decompressor.decompress(compressed_data)
|
||||
elif encoding_type == 0:
|
||||
pixel_data = await self.reader.readexactly(width * height * 4) # Assuming 32 bits per pixel
|
||||
if encoding_type in (-223, -308): # desktopsize
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.framebuffer = Image.new('RGBA', (self.width, self.height))
|
||||
if encoding_type == -308:
|
||||
nscreens = await self._read_number(1)
|
||||
_ = await self._read_number(3) # padding
|
||||
for _ in range(nscreens):
|
||||
_ = await self.reader.readexactly(16) # screen info
|
||||
elif pixel_data:
|
||||
pixel_data = np.frombuffer(pixel_data, dtype=np.uint8).reshape((height, width, 4)).copy()
|
||||
pixel_data[:, :, 3] = 0xff
|
||||
img = Image.fromarray(pixel_data, 'RGBA')
|
||||
self.framebuffer.paste(img, (x, y))
|
||||
elif encoding_type == 7: # tight
|
||||
# Best document I could see was:
|
||||
# https://github.com/TurboVNC/tightvnc/blob/main/vnc_winsrc/rfb/rfbproto.h
|
||||
tightheader = await self._read_number(1)
|
||||
streamid = tightheader & 0x0F
|
||||
if streamid:
|
||||
raise NotImplementedError('tight encoding with streamid not implemented')
|
||||
comptype = (tightheader >> 4) & 0x0F
|
||||
if comptype != 9:
|
||||
raise NotImplementedError(f'tight encoding with comptype {comptype} not implemented')
|
||||
compressed_data_length = await self._read_tight_length()
|
||||
compressed_data = await self.reader.readexactly(compressed_data_length)
|
||||
with io.BytesIO(compressed_data) as jpgimg:
|
||||
img = Image.open(jpgimg)
|
||||
img.load()
|
||||
self.framebuffer.paste(img, (x, y))
|
||||
else:
|
||||
raise Exception(f'Unsupported encoding type: {encoding_type}')
|
||||
|
||||
async def _read_tight_length(self):
|
||||
length = 0
|
||||
for i in range(3):
|
||||
byte = await self._read_number(1)
|
||||
length |= ((byte & 0x7F) << (i * 7))
|
||||
if not (byte & 0x80):
|
||||
break
|
||||
return length
|
||||
async def close(self):
|
||||
if self._video_thread is not None:
|
||||
# Signal the writer thread to flush and finalize the file
|
||||
self._video_queue.put((None, time.monotonic()))
|
||||
await asyncio.to_thread(self._video_thread.join)
|
||||
self._video_thread = None
|
||||
self.writer.close()
|
||||
await self.writer.wait_closed()
|
||||
@@ -11,7 +11,7 @@ Name: %{name}
|
||||
Version: %{version}
|
||||
Release: %{release}
|
||||
Source0: %{name}-%{fversion}.tar.gz
|
||||
License: Apache-2.0
|
||||
License: Apache2
|
||||
Group: Development/Libraries
|
||||
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
|
||||
Prefix: %{_prefix}
|
||||
@@ -19,13 +19,6 @@ BuildArch: noarch
|
||||
Vendor: Lenovo
|
||||
Url: http://github.com/lenovo/confluent
|
||||
Obsoletes: confluent_common
|
||||
# SUSE keeps Pillow's upstream capitalisation and rpm capability names are
|
||||
# case sensitive, so the lowercase EL spelling resolves to nothing there.
|
||||
%if 0%{?suse_version}
|
||||
Requires: python3-numpy python3-Pillow python3-matplotlib
|
||||
%else
|
||||
Requires: python3-numpy python3-pillow python3-matplotlib
|
||||
%endif
|
||||
|
||||
%description
|
||||
This package enables python development and command line access to
|
||||
@@ -48,12 +41,6 @@ python2 setup.py install --single-version-externally-managed -O1 --root=$RPM_BUI
|
||||
python3 setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-scripts=/opt/confluent/bin --install-purelib=/opt/confluent/lib/python
|
||||
%endif
|
||||
|
||||
%if 0%{?suse_version}
|
||||
# openSUSE patches setuptools to write a literal "#!python" placeholder into
|
||||
# installed scripts and expects the packager to rewrite it. The stock
|
||||
# %%python3_fix_shebang only looks at %%{_bindir}, so do it for our prefix.
|
||||
sed -i '1s|^#!python|#!/usr/bin/python3|' $RPM_BUILD_ROOT/opt/confluent/bin/*
|
||||
%endif
|
||||
|
||||
%clean
|
||||
rm -rf $RPM_BUILD_ROOT
|
||||
|
||||
@@ -19,7 +19,6 @@ alias nodebmcreset='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export
|
||||
alias nodeboot='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeboot'
|
||||
alias nodeconfig='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeconfig'
|
||||
alias nodeconsole='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeconsole'
|
||||
alias nodecertutil='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodecertutil'
|
||||
alias nodedeploy='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodedeploy'
|
||||
alias nodedefine='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodedefine'
|
||||
alias nodeeventlog='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeeventlog'
|
||||
@@ -53,8 +52,6 @@ _confluent_get_args()
|
||||
CMPARGS+=("")
|
||||
fi
|
||||
GENNED=""
|
||||
# Whitespace separates candidate groups, while commas group synonyms.
|
||||
# shellcheck disable=SC2068
|
||||
for CAND in ${COMP_CANDIDATES[@]}; do
|
||||
candarray=(${CAND//,/ })
|
||||
matched=0
|
||||
@@ -156,22 +153,14 @@ _confluent_osimage_completion()
|
||||
{
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS == 2 ]; then
|
||||
COMPREPLY=($(compgen -W "initialize import importcheck updateboot rebase fetch" -- ${COMP_WORDS[COMP_CWORD]}))
|
||||
COMPREPLY=($(compgen -W "initialize import importcheck updateboot rebase" -- ${COMP_WORDS[COMP_CWORD]}))
|
||||
return
|
||||
elif [ ${CMPARGS[1]} == 'initialize' ]; then
|
||||
COMPREPLY=($(compgen -W "-h -a -g -u -s -k -t -p -i -l -r" -- ${COMP_WORDS[COMP_CWORD]}))
|
||||
COMPREPLY=($(compgen -W "-h -u -s -t -i" -- ${COMP_WORDS[COMP_CWORD]}))
|
||||
elif [ ${CMPARGS[1]} == 'import' ] || [ ${CMPARGS[1]} == 'importcheck' ]; then
|
||||
compopt -o default
|
||||
COMPREPLY=()
|
||||
return
|
||||
elif [ ${CMPARGS[1]} == 'fetch' ]; then
|
||||
if [ $NUMARGS == 3 ]; then
|
||||
COMPREPLY=($(compgen -W "$(osdeploy getfetchable)" -- "${COMP_WORDS[COMP_CWORD]}"))
|
||||
return
|
||||
fi
|
||||
compopt -o dirnames
|
||||
COMPREPLY=()
|
||||
return
|
||||
elif [ ${CMPARGS[1]} == 'updateboot' -o ${CMPARGS[1]} == 'rebase' ]; then
|
||||
COMPREPLY=($(compgen -W "-n $(confetty show /deployment/profiles|sed -e 's/\///')" -- "${COMP_WORDS[COMP_CWORD]}"))
|
||||
return
|
||||
@@ -304,30 +293,6 @@ _confluent_nodegroupattrib_completion()
|
||||
COMP_CANDIDATES=$(nodegroupattrib 'everything' all | awk '{print $2}'|sed -e 's/://')
|
||||
_confluent_generic_ng_completion
|
||||
}
|
||||
|
||||
_confluent_nodecertutil_completion()
|
||||
{
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS -lt 3 ]; then
|
||||
_confluent_nr_completion
|
||||
return
|
||||
fi
|
||||
if [ $NUMARGS == 3 ]; then
|
||||
COMPREPLY=($(compgen -W "installbmccacert removebmccacert listbmccacerts signbmccert" -- ${COMP_WORDS[COMP_CWORD]}))
|
||||
return
|
||||
fi
|
||||
if [ ${CMPARGS[2]} == 'installbmccacert' ]; then
|
||||
compopt -o default
|
||||
COMPREPLY=()
|
||||
return
|
||||
fi
|
||||
if [ ${CMPARGS[2]} == 'signbmccert' ]; then
|
||||
COMP_CANDIDATES=("--days --added-names")
|
||||
_confluent_get_args
|
||||
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[COMP_CWORD]}))
|
||||
fi
|
||||
}
|
||||
|
||||
_confluent_nn_completion()
|
||||
{
|
||||
_confluent_get_args
|
||||
@@ -343,22 +308,6 @@ _confluent_nn_completion()
|
||||
|
||||
COMPREPLY=($(compgen -W "$(nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[COMP_CWORD]}"))
|
||||
}
|
||||
|
||||
_confluent_nodeconsole_completion()
|
||||
{
|
||||
_confluent_get_args
|
||||
if [ ${CMPARGS[-2]} == '-a' ] || [ ${CMPARGS[-2]} == '--automation' ]; then
|
||||
compopt -o default
|
||||
COMPREPLY=()
|
||||
return
|
||||
fi
|
||||
if [[ ${COMP_WORDS[COMP_CWORD]} == -* ]]; then
|
||||
COMPREPLY=($(compgen -W "-a --automation -e --headless -t --tile -l --log -T --Timestamp -s --screenshot -i --interval -v --video -w --windowed" -- "${COMP_WORDS[COMP_CWORD]}"))
|
||||
return
|
||||
fi
|
||||
_confluent_nn_completion
|
||||
}
|
||||
|
||||
_confluent_nr_completion()
|
||||
{
|
||||
CMPARGS=($COMP_LINE)
|
||||
@@ -413,7 +362,7 @@ complete -F _confluent_nodeattrib_completion nodeattrib
|
||||
complete -F _confluent_nr_completion nodebmcreset
|
||||
complete -F _confluent_nodesetboot_completion nodeboot
|
||||
complete -F _confluent_nr_completion nodeconfig
|
||||
complete -F _confluent_nodeconsole_completion nodeconsole
|
||||
complete -F _confluent_nn_completion nodeconsole
|
||||
complete -F _confluent_define_completion nodedefine
|
||||
complete -F _confluent_define_completion nodegroupdefine
|
||||
complete -F _confluent_nr_completion nodeeventlog
|
||||
@@ -426,7 +375,6 @@ complete -F _confluent_ng_completion nodegroupremove
|
||||
complete -F _confluent_nr_completion nodehealth
|
||||
complete -F _confluent_nodeidentify_completion nodeidentify
|
||||
complete -F _confluent_nr_completion nodeinventory
|
||||
complete -F _confluent_nodecertutil_completion nodecertutil
|
||||
complete -F _confluent_nodeattrib_completion nodelist
|
||||
complete -F _confluent_nodemedia_completion nodemedia
|
||||
complete -F _confluent_nodepower_completion nodepower
|
||||
|
||||
@@ -42,7 +42,7 @@ commands.
|
||||
* `show` **ELEMENT**, `cat` **ELEMENT**:
|
||||
Display the result of reading a specific element (by full or relative path)
|
||||
* `unset` **ELEMENT** **ATTRIBUTE**
|
||||
For an element with attributes, request to clear the value of the attribute
|
||||
For an element with attributes, request to clear the value of the attribue
|
||||
* `set` **ELEMENT** **ATTRIBUTE**=**VALUE**
|
||||
Set the specified attribute to the given value
|
||||
* `start` **ELEMENT**
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
confluent2ansible(8) -- Export confluent node inventory to an Ansible hosts file
|
||||
================================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`confluent2ansible <noderange> -o <ansible.hosts>` [`-a`]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`confluent2ansible` reads the nodes matched by `<noderange>` from confluent and
|
||||
writes a corresponding Ansible inventory (hosts) file, allowing an existing
|
||||
confluent inventory to be used directly by Ansible.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-o FILE`, `--output=FILE`:
|
||||
Write the Ansible hosts file to FILE.
|
||||
|
||||
* `-a`, `--append`:
|
||||
Append to an existing hosts file rather than overwriting it.
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Write an Ansible hosts file for the compute group:
|
||||
`# confluent2ansible compute -o /etc/ansible/hosts`
|
||||
|
||||
* Append a rack to an existing hosts file:
|
||||
`# confluent2ansible rack3 -o /etc/ansible/hosts -a`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
confluent2xcat(8), confluent2lxca(8), nodelist(8)
|
||||
@@ -1,243 +0,0 @@
|
||||
confluent2dnsmasq(8) -- Generate dnsmasq static DHCP reservations for nodes
|
||||
===============================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`confluent2dnsmasq [options] <noderange>`
|
||||
`confluent2dnsmasq <noderange>`
|
||||
`confluent2dnsmasq --all-options <noderange>`
|
||||
`confluent2dnsmasq -n <noderange>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`confluent2dnsmasq` generates a dnsmasq configuration fragment of static
|
||||
DHCP reservations for a noderange, using the confluent database as the source
|
||||
of truth. Each `net.<network>.<attribute>` group is pulled together and one
|
||||
`dhcp-host=` reservation is emitted for **every network that has a hardware
|
||||
address** (`net.hwaddr`, `net.bmc.hwaddr`, `net.eth.mynetwork.hwaddr`, and so
|
||||
on; the network component may itself contain several dotted parts). Networks
|
||||
without a `hwaddr` (for example InfiniBand interfaces identified only by a
|
||||
GUID) are skipped.
|
||||
|
||||
For each reservation, `net.<network>.ipv4_address` supplies the reserved
|
||||
address. The `/prefixlen` suffix on that address is used to compute the
|
||||
enclosing subnet; if the address carries no `/prefixlen`, the prefix length is
|
||||
filled in from the matching directly-attached route in the host's routing
|
||||
table. One `dhcp-range=<network>,static,<netmask>,<lease>` line is emitted per
|
||||
distinct subnet. dnsmasq will not answer DHCP requests on a subnet that has no
|
||||
covering `dhcp-range`, even when every address on it is statically reserved, so
|
||||
these ranges are emitted by default (see `--no-range`). An address that has
|
||||
neither an explicit prefix nor a matching local route still yields a
|
||||
`dhcp-host` reservation, but no subnet or `dhcp-range` is derived for it and a
|
||||
warning is printed.
|
||||
|
||||
The reservation name is the first token of `net.<network>.hostname`, falling
|
||||
back to the node name itself for the primary (unnamed) network, and omitted for
|
||||
a named network that has no hostname. Only IPv4 node addresses are used for
|
||||
reservations and ranges.
|
||||
|
||||
By default the fragment begins with a `bind-dynamic` directive so that dnsmasq
|
||||
can bind DHCP to networks confluent is using, too (see `--no-bind-dynamic`).
|
||||
|
||||
Also by default, a `listen-address` line is emitted for `127.0.0.1`, for
|
||||
`::1`, and for this host's own address on each managed subnet (see
|
||||
`--no-listen-address`). The per-subnet addresses are read from the routing
|
||||
table of the host the command runs on, so run it on the dnsmasq host; only
|
||||
subnets the host is directly attached to are added. Besides telling dnsmasq
|
||||
where to listen, the presence of any `listen-address` line disables the default
|
||||
`local-service` (or `local-service=host`) restriction in `dnsmasq.conf` that
|
||||
would otherwise limit dnsmasq to localhost.
|
||||
|
||||
Additional data can optionally be added to the DHCP reply from the confluent
|
||||
database (see OPTIONS; all are off by default). Each such option is scoped to
|
||||
the subnet it belongs to using a dnsmasq tag: the `dhcp-host` lines for that
|
||||
subnet are given a `set:<tag>` and the corresponding `dhcp-option` lines a
|
||||
matching `tag:<tag>`. Values are aggregated per subnet; if two nodes on the
|
||||
same subnet disagree (for example two different gateways), a warning is printed
|
||||
and the first value is used.
|
||||
|
||||
The configuration is written to `/etc/dnsmasq.d/confluent-dhcp.conf` (override
|
||||
with `--target`) and regenerated on each run, replacing the file atomically.
|
||||
If the existing file was generated with different content-affecting arguments
|
||||
(the preview and confirmation flags `-n` and `-y` are ignored for this
|
||||
comparison), you are asked to confirm before it is overwritten; the prompt is
|
||||
skipped with `-y` and is refused when there is no controlling terminal. If a
|
||||
run produces no reservations at all (for example an empty noderange or a failed
|
||||
read), `confluent2dnsmasq` refuses to overwrite the existing file unless
|
||||
`--allow-empty` is given.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `--target PATH`:
|
||||
Path of the configuration file to write. Defaults to
|
||||
`/etc/dnsmasq.d/confluent-dhcp.conf`.
|
||||
|
||||
* `--tag-prefix PREFIX`:
|
||||
Prefix used when generating dnsmasq tag names for per-subnet options.
|
||||
Defaults to `cdhcp`, yielding tags such as `cdhcp_10_28_104_0_21`.
|
||||
|
||||
* `--no-bind-dynamic`:
|
||||
Do not emit the `bind-dynamic` line. It is emitted by default. Note that
|
||||
`bind-dynamic` is a global dnsmasq directive and must not be combined with
|
||||
a `bind-interfaces` directive set elsewhere. Without it, dnsmasq may conflict
|
||||
with confluent's own DHCP unless `bind-dynamic` is set elsewhere in the
|
||||
dnsmasq configuration; a warning is printed.
|
||||
|
||||
* `--no-listen-address`:
|
||||
Do not autodetect or emit `listen-address` lines. By default a
|
||||
`listen-address` line is emitted for `127.0.0.1`, for `::1`, and for this
|
||||
host's address on each managed subnet. With this flag you must set
|
||||
`interface`, `except-interface`, or `listen-address` yourself (or disable
|
||||
`local-service`/`local-service=host` in `dnsmasq.conf`) for dnsmasq to serve
|
||||
the cluster networks alongside confluent; a warning is printed as a reminder.
|
||||
|
||||
* `--no-range`:
|
||||
Do not emit `dhcp-range` lines, leaving range and subnet declarations to be
|
||||
managed elsewhere. Remember that dnsmasq will not serve DHCP on a subnet
|
||||
that has no covering `dhcp-range`. Per-subnet options are still scoped via
|
||||
host tags, so they keep working with a range you declare yourself.
|
||||
|
||||
* `--lease TIME`:
|
||||
Lease time applied to the emitted `dhcp-range` lines. Defaults to `24h`.
|
||||
Accepts any dnsmasq lease syntax (for example `1h`, `24h`, `infinite`).
|
||||
Pass an empty string to omit the lease field entirely.
|
||||
|
||||
* `-n`, `--stdout`:
|
||||
Write the configuration to standard output instead of the target file, which
|
||||
is left untouched. Useful for previewing.
|
||||
|
||||
* `-y`, `--yes`:
|
||||
Do not prompt for confirmation when the existing configuration was generated
|
||||
with different settings; regenerate anyway. Without it a settings change is
|
||||
confirmed interactively, and refused when there is no controlling terminal.
|
||||
|
||||
* `--allow-empty`:
|
||||
Write the file even when no reservations were produced. By default an empty
|
||||
result does not overwrite the existing file, to avoid discarding a good
|
||||
configuration after an empty or failed read.
|
||||
|
||||
* `--all-options`:
|
||||
Enable all of the optional reply-data options below at once.
|
||||
|
||||
* `--gateway`, `--router`:
|
||||
Add a router (DHCP option 3) per subnet, taken from
|
||||
`net.<network>.ipv4_gateway`.
|
||||
|
||||
* `--dns`:
|
||||
Add DNS servers (DHCP option 6) per subnet, taken from `dns.servers`.
|
||||
|
||||
* `--domain`:
|
||||
Add a domain name (DHCP option 15) per subnet, taken from `dns.domain`.
|
||||
|
||||
* `--ntp`:
|
||||
Add NTP servers (DHCP option 42) per subnet, taken from `ntp.servers`.
|
||||
|
||||
* `--mtu`:
|
||||
Add an interface MTU (DHCP option 26) per subnet, taken from
|
||||
`net.<network>.mtu`.
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show a help message and exit.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Generate reservations for a noderange and write the default file:
|
||||
`# confluent2dnsmasq everything`
|
||||
|
||||
* Preview the configuration for one node without writing anything:
|
||||
`# confluent2dnsmasq -n node01`
|
||||
|
||||
* Include all optional reply data (gateway, DNS, domain, NTP, MTU):
|
||||
`# confluent2dnsmasq --all-options everything`
|
||||
|
||||
* Include only the gateway, and use an eight hour lease:
|
||||
`# confluent2dnsmasq --gateway --lease 8h everything`
|
||||
|
||||
* Regenerate non-interactively (for example from cron) after changing options, without the confirmation prompt:
|
||||
`# confluent2dnsmasq --all-options -y everything`
|
||||
|
||||
* Emit only reservations (no ranges) to a custom file kept beside your own range and listen interface definitions:
|
||||
`# confluent2dnsmasq --no-listen-address --no-range --target /etc/dnsmasq.d/confluent-reservations.conf everything`
|
||||
|
||||
## GENERATED CONFIGURATION
|
||||
|
||||
Given the following confluent attributes for node `node01`:
|
||||
|
||||
node01: net.hwaddr: 10:ff:e0:af:af:f5
|
||||
node01: net.ipv4_address: 10.28.90.1/21
|
||||
node01: net.ipv4_gateway: 10.28.88.1
|
||||
node01: net.bmc.hwaddr: 10:ff:e0:a4:cf:b6
|
||||
node01: net.bmc.hostname: node01-bmc
|
||||
node01: net.bmc.ipv4_address: 10.28.106.1/21
|
||||
node01: net.bmc.ipv4_gateway: 10.28.104.1
|
||||
node01: net.ib0.hostname: node01-ib0
|
||||
node01: net.ib0.ipv4_address: 10.28.101.1/21
|
||||
|
||||
`confluent2dnsmasq node01`, run on a dnsmasq host whose own addresses on the
|
||||
two managed subnets are `10.28.88.250` and `10.28.104.250`, writes the file
|
||||
below. The `ib0` network is skipped because it has no `hwaddr`; the two
|
||||
remaining networks each yield a subnet (with its `dhcp-range`) and a
|
||||
reservation, and a `listen-address` line is emitted for localhost and for this
|
||||
host's address on each subnet:
|
||||
|
||||
# Managed by confluent2dnsmasq -- DO NOT EDIT BY HAND.
|
||||
# Regenerate: confluent2dnsmasq node01
|
||||
# Generated 2026-06-28 12:00:00 +0000
|
||||
|
||||
# Allow confluent and dnsmasq to share the same network for DHCP
|
||||
bind-dynamic
|
||||
|
||||
# Listen on localhost plus this host's address on each managed subnet,
|
||||
# so dnsmasq serves these networks alongside confluent and bind-dynamic
|
||||
# (a listen-address line also overrides local-service in dnsmasq.conf).
|
||||
listen-address=127.0.0.1
|
||||
listen-address=::1
|
||||
listen-address=10.28.88.250
|
||||
listen-address=10.28.104.250
|
||||
|
||||
# subnet 10.28.88.0/21
|
||||
dhcp-range=10.28.88.0,static,255.255.248.0,24h
|
||||
dhcp-host=10:ff:e0:af:af:f5,10.28.90.1,node01
|
||||
|
||||
# subnet 10.28.104.0/21
|
||||
dhcp-range=10.28.104.0,static,255.255.248.0,24h
|
||||
dhcp-host=10:ff:e0:a4:cf:b6,10.28.106.1,node01-bmc
|
||||
|
||||
Adding `--gateway` scopes a router option to each subnet through a tag, and the
|
||||
matching `dhcp-host` lines gain a `set:` tag so the option reaches them:
|
||||
|
||||
# subnet 10.28.88.0/21
|
||||
dhcp-range=10.28.88.0,static,255.255.248.0,24h
|
||||
dhcp-option=tag:cdhcp_10_28_88_0_21,option:router,10.28.88.1
|
||||
dhcp-host=10:ff:e0:af:af:f5,set:cdhcp_10_28_88_0_21,10.28.90.1,node01
|
||||
|
||||
# subnet 10.28.104.0/21
|
||||
dhcp-range=10.28.104.0,static,255.255.248.0,24h
|
||||
dhcp-option=tag:cdhcp_10_28_104_0_21,option:router,10.28.104.1
|
||||
dhcp-host=10:ff:e0:a4:cf:b6,set:cdhcp_10_28_104_0_21,10.28.106.1,node01-bmc
|
||||
|
||||
## NOTES
|
||||
|
||||
`bind-dynamic` is a global dnsmasq option; if it is set here it must not be
|
||||
contradicted by a `bind-interfaces` directive elsewhere in the configuration.
|
||||
|
||||
The autodetected `listen-address` lines double as the mechanism that relaxes
|
||||
dnsmasq's default `local-service`/`local-service=host` restriction (any
|
||||
`interface`, `except-interface`, or `listen-address` line does), which is what
|
||||
lets dnsmasq answer on the cluster networks instead of only localhost.
|
||||
Detection only covers subnets the host running the command is directly attached
|
||||
to, so run it there; with `--no-listen-address` you must supply `interface`,
|
||||
`except-interface`, or `listen-address` yourself, or disable `local-service`.
|
||||
|
||||
After (re)generating the file, validate it with `dnsmasq --test` and restart
|
||||
dnsmasq (for example `systemctl restart dnsmasq`); a reload (SIGHUP) does not
|
||||
re-read `listen-address` or interface bindings.
|
||||
|
||||
## FILES
|
||||
|
||||
* `/etc/dnsmasq.d/confluent-dhcp.conf`:
|
||||
Default output file (override with `--target`).
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodeattrib(8), noderange(5), dnsmasq(8)
|
||||
@@ -1,30 +0,0 @@
|
||||
confluent2lxca(8) -- Export confluent nodes to a Lenovo XClarity Administrator bulk import file
|
||||
==============================================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`confluent2lxca <noderange> -o <bulkimport.csv>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`confluent2lxca` reads the nodes matched by `<noderange>` from confluent and
|
||||
writes a CSV file suitable for bulk import into Lenovo XClarity Administrator
|
||||
(LXCA), so that hardware already known to confluent can be brought under LXCA
|
||||
management.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-o FILE`, `--output=FILE`:
|
||||
Write the XClarity Administrator bulk import CSV to FILE.
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Generate a bulk import file for all nodes:
|
||||
`# confluent2lxca everything -o bulkimport.csv`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
confluent2xcat(8), confluent2ansible(8), nodelist(8)
|
||||
@@ -1,36 +0,0 @@
|
||||
confluent2xcat(8) -- Export confluent nodes to an xCAT stanza definition
|
||||
========================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`confluent2xcat <noderange> -o <xcatnodes.def>` [`-m <macs.csv>`]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`confluent2xcat` reads the nodes matched by `<noderange>` from confluent and
|
||||
writes an xCAT object definition stanza file, easing interoperation with or
|
||||
migration to/from an xCAT environment. Optionally it can also write an xCAT
|
||||
`macs.csv` file capturing the MAC addresses of the nodes.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-o FILE`, `--output=FILE`:
|
||||
Write the xCAT stanza definition to FILE.
|
||||
|
||||
* `-m FILE`, `--macs=FILE`:
|
||||
Write an xCAT macs.csv file to FILE.
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Export all nodes to an xCAT stanza file:
|
||||
`# confluent2xcat everything -o xcatnodes.def`
|
||||
|
||||
* Also capture MAC addresses:
|
||||
`# confluent2xcat everything -o xcatnodes.def -m macs.csv`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
confluent2ansible(8), confluent2lxca(8), nodelist(8)
|
||||
@@ -3,26 +3,16 @@ confluentdbutil(8) -- Backup or restore confluent database
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`confluentdbutil [options] [dump|restore|merge] <path>`
|
||||
`confluentdbutil [-u] [-v] showattrib <noderange> <attribute>...`
|
||||
`confluentdbutil [options] [dump|restore] <path>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
**confluentdbutil** is a utility to export/import the confluent attributes
|
||||
to/from json files. The path is a directory that holds the json version.
|
||||
In order to perform restore, the confluent service must not be running. It
|
||||
is required to indicate how the usernames/passwords are treated in
|
||||
is required to indicate how to treat the usernames/passwords are treated in
|
||||
the json files (password protected, removed from the files, or unprotected).
|
||||
|
||||
The `showattrib` subcommand prints the stored value of any attribute for the
|
||||
given noderange, reading directly from the configuration store. Because it
|
||||
does not talk to the confluent daemon, it can be used to read attribute values
|
||||
even while confluent is stopped. It must be run directly on the confluent
|
||||
server as root. By default, `secret.*` and `crypted.*` values are masked as
|
||||
`********`, just as nodeattrib(8) shows them. With `-u`, they are revealed:
|
||||
`secret.*` values decrypted to plaintext and `crypted.*` values as their
|
||||
stored one-way hashes.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-p PASSWORD`, `--password=PASSWORD`:
|
||||
@@ -34,13 +24,10 @@ stored one-way hashes.
|
||||
|
||||
* `-r`, `--redact`:
|
||||
Indicates to replace usernames and passwords with a dummy string rather
|
||||
than including them.
|
||||
than included.
|
||||
|
||||
* `-u`, `--unprotected`:
|
||||
With `dump`, the keys.json file will include the encryption keys without
|
||||
any protection. With `showattrib`, show `secret.*` values decrypted to
|
||||
plaintext and `crypted.*` values as their stored hashes rather than
|
||||
masking them as `********`.
|
||||
The keys.json file will include the encryption keys without any protection.
|
||||
|
||||
* `-s`, `--skipkeys`:
|
||||
This specifies to dump the encrypted data without
|
||||
@@ -48,46 +35,11 @@ stored one-way hashes.
|
||||
suitable for an automated incremental backup, where an
|
||||
earlier password protected dump has a protected
|
||||
keys.json file, and only the protected data is needed.
|
||||
Keys do not change and as such they do not require
|
||||
keys do not change and as such they do not require
|
||||
incremental backup.
|
||||
|
||||
* `-x ATTRIBUTE`, `--exclude=ATTRIBUTE`:
|
||||
Exclude matching node and node group attributes from `dump`, `restore`,
|
||||
or `merge`.
|
||||
The option may be specified multiple times. Attribute names may use
|
||||
shell-style wildcards such as `net.*`.
|
||||
A bare namespace such as `net` excludes all attributes below that namespace.
|
||||
An exclusion omits the attribute from the data being written, it does not
|
||||
preserve the value already in the database. A `restore` replaces the
|
||||
database outright, so attributes excluded there are missing from the
|
||||
restored configuration entirely; use `merge` to leave existing objects
|
||||
untouched.
|
||||
Node `groups`, node `id.index`, and node-group `noderange` are retained
|
||||
so that restore can reconstruct node-group membership and preserve
|
||||
node index assignments.
|
||||
During merge, existing nodes and node groups are skipped as whole objects;
|
||||
exclusions affect only new objects imported from the backup.
|
||||
|
||||
* `-y`, `--yaml`:
|
||||
|
||||
* `-y`, `--yaml
|
||||
Use YAML instead of JSON as file format
|
||||
|
||||
* `-v`, `--value-only`:
|
||||
With `showattrib`, print only the values, without node and attribute names
|
||||
(useful for scripting).
|
||||
|
||||
* `-h`, `--help`:
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Show the decrypted BMC password confluent uses for a node (run on the
|
||||
server as root):
|
||||
`# confluentdbutil -u showattrib n1 secret.hardwaremanagementpassword`
|
||||
`n1: secret.hardwaremanagementpassword: mypassword123`
|
||||
|
||||
* Get just the value, for use in a script:
|
||||
`# confluentdbutil -u -v showattrib n1 secret.hardwaremanagementpassword`
|
||||
`mypassword123`
|
||||
|
||||
* Dump configuration without (dynamic) deployment state:
|
||||
`# confluentdbutil -u -x 'deployment.state*' dump /root/confluent-backup`
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
dir2img(8) -- Create a disk image from a directory for nodemedia upload
|
||||
=======================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`dir2img` [`-e <extra_mb>`] [`-s <total_mb>`] `<directory> <imagefile>` [`<label>`]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`dir2img` packages the contents of `<directory>` into a FAT-formatted "big
|
||||
floppy" disk image written to `<imagefile>`. The resulting image is suitable
|
||||
for upload with nodemedia(8) so that its files are presented to a node as
|
||||
removable media. An optional volume `<label>` may be supplied.
|
||||
|
||||
By default the image is sized to fit the directory contents. The `-e` and `-s`
|
||||
options allow reserving additional free space.
|
||||
|
||||
This command relies on `mtools` (specifically `mformat` and `mcopy`) being
|
||||
installed.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-e EXTRA_MB`:
|
||||
Reserve EXTRA_MB megabytes of free space in the image in addition to the
|
||||
space required for the directory contents.
|
||||
|
||||
* `-s TOTAL_MB`:
|
||||
Make the image TOTAL_MB megabytes in total, rather than sizing it to the
|
||||
directory contents.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Create an image from a directory:
|
||||
`# dir2img /var/tmp/drivers drivers.img`
|
||||
|
||||
* Create a 64 MB labelled image:
|
||||
`# dir2img -s 64 /var/tmp/drivers drivers.img DRIVERS`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodemedia(8)
|
||||
@@ -43,16 +43,6 @@
|
||||
* `-s`, `--source` <directory>:
|
||||
Directory to pull installation from, typically a subdirectory of `/var/lib/confluent/distributions`. By default, the repositories for the build system are used. For Ubuntu, this is not supported; the build system repositories are always used.
|
||||
|
||||
* `--arch` <architecture>:
|
||||
Target architecture to build for (`x86_64` or `aarch64`). For Ubuntu, the
|
||||
Debian-style names `amd64` and `arm64` are accepted as aliases. Building
|
||||
for a foreign architecture is supported on EL and Ubuntu build hosts and
|
||||
requires a statically linked qemu-user emulator registered in binfmt_misc
|
||||
with the F (fix-binary) flag; on Ubuntu this is provided by the
|
||||
qemu-user-static package (qemu-user-binfmt as of Ubuntu 26.04). For EL,
|
||||
the architecture is also detected automatically when building from a `-s`
|
||||
source tree.
|
||||
|
||||
* `-y`, `--non-interactive`:
|
||||
Avoid prompting for confirmation.
|
||||
|
||||
@@ -109,15 +99,6 @@ Build a diskless image from a distribution:
|
||||
|
||||
imgutil build -s alma-9.6-x86_64 /tmp/myimage
|
||||
|
||||
Build an aarch64 EL diskless image on an x86_64 host (the architecture is
|
||||
detected from the source tree):
|
||||
|
||||
imgutil build -s alma-9.6-aarch64 /tmp/myimage
|
||||
|
||||
Build an aarch64 Ubuntu diskless image on an amd64 Ubuntu host:
|
||||
|
||||
imgutil build --arch aarch64 /tmp/myimage
|
||||
|
||||
Execute a shell in an unpacked image:
|
||||
|
||||
imgutil exec /tmp/myimage
|
||||
|
||||
@@ -9,7 +9,7 @@ nodeapply(8) -- Execute command on many nodes in a noderange through ssh
|
||||
|
||||
Provides shortcut access to a number of common operations against deployed
|
||||
nodes. These operations include refreshing ssh certificates and configuration,
|
||||
rerunning syncfiles, and executing specified postscripts.
|
||||
rerunning syncflies, and executing specified postscripts.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
|
||||
@@ -26,11 +26,6 @@ This is different from setting the value to an empty string.
|
||||
|
||||
Arbitrary custom attributes can also be created with the `custom.` prefix.
|
||||
|
||||
Network attributes (`net.*`) may similarly be qualified with an arbitrary name, or dotted chain of names, between
|
||||
`net` and the attribute, for example `net.compute.ipv4_address` or `net.eth.compute.ipv4_address` and
|
||||
`net.eth.management.ipv4_address`. Any such `net.<name*>.*` attributes together describe one logical network
|
||||
interface, so custom networks may be defined freely by choosing your own name(s).
|
||||
|
||||
Attributes may be specified by wildcard, for example `net.*switch` will report
|
||||
all attributes that begin with `net.` and end with `switch`.
|
||||
|
||||
@@ -38,15 +33,11 @@ If the word all is specified, then all available attributes are given.
|
||||
Omitting any attribute name or the word 'all' will display only attributes
|
||||
that are currently set.
|
||||
|
||||
The values of `secret.*` and `crypted.*` attributes are never shown here; they
|
||||
can only be retrieved on the confluent server with
|
||||
`confluentdbutil -u showattrib`.
|
||||
|
||||
For the `groups` attribute, it is possible to add a group by doing
|
||||
`groups,=<newgroup>` and to remove by doing `groups^=<oldgroup>`
|
||||
|
||||
Note that `nodeattrib <group>` will likely not provide the expected behavior.
|
||||
See the nodegroupattrib(8) command for how to manage attributes on a group level. Running
|
||||
See nodegroupattrib(8) command on how to manage attributes on a group level. Running
|
||||
nodeattrib on a group will simply set node-specific attributes on each individual
|
||||
member of the group.
|
||||
|
||||
@@ -72,8 +63,8 @@ to a blank value will allow masking a group defined attribute with an empty valu
|
||||
or environment variables.
|
||||
|
||||
* `-s`, `--set`:
|
||||
Set attributes using a batch file rather than the command line. The attributes in the batch file
|
||||
can be specified as one line of key=value pairs similar to command line or each attribute can
|
||||
Set attributes using a batch file rather than the command line. The attributes in the batch file
|
||||
can be specified as one line of key=value pairs simmilar to command line or each attribute can
|
||||
be in its own line. Lines that start with # sign will be read as a comment. See EXAMPLES for batch
|
||||
file syntax.
|
||||
|
||||
@@ -122,29 +113,6 @@ to a blank value will allow masking a group defined attribute with an empty valu
|
||||
`n2: console.method: serial`
|
||||
`n2: hardwaremanagement.manager: 172.30.3.2`
|
||||
|
||||
* Setting up a named network interface as a bond (e.g. a two-port LACP bond used for compute traffic), using an
|
||||
expression to calculate the IP address:
|
||||
`# nodeattrib node12 net.compute.team_mode=lacp net.compute.connection_name=bond0 net.compute.interface_names=enp33s0f0np0,enp33s0f1np1 net.compute.ipv4_address='172.17.0.{n1}/16'`
|
||||
`node12: net.compute.connection_name: bond0`
|
||||
`node12: net.compute.interface_names: enp33s0f0np0,enp33s0f1np1`
|
||||
`node12: net.compute.ipv4_address: 172.17.0.12/16`
|
||||
`node12: net.compute.team_mode: lacp`
|
||||
|
||||
* Passing additional settings to the network backend of the deployed OS with `net.extra_settings`
|
||||
(semicolon-delimited key=value pairs, keys in the native syntax of the respective backend).
|
||||
On a NetworkManager based OS (e.g. Enterprise Linux), keys are nmcli properties:
|
||||
`# nodeattrib node12 net.mgmt.extra_settings='connection.zone=internal;ipv4.routes=10.0.0.0/8 192.168.1.254, 172.16.0.0/12 192.168.1.254;ipv4.route-metric=200'`
|
||||
`node12: net.mgmt.extra_settings: connection.zone=internal;ipv4.routes=10.0.0.0/8 192.168.1.254, 172.16.0.0/12 192.168.1.254;ipv4.route-metric=200`
|
||||
|
||||
* On a netplan based OS (e.g. Ubuntu), keys are netplan YAML paths (nested keys dotted, values in YAML flow syntax).
|
||||
Since Confluent uses braces for attribute expressions, literal braces must be escaped as `{{` and `}}` when setting the attribute:
|
||||
`# nodeattrib node13 net.mgmt.extra_settings='routes=[{{to: 10.0.0.0/8, via: 192.168.1.254}}];nameservers.search=[lab.example.com]'`
|
||||
`node13: net.mgmt.extra_settings: routes=[{to: 10.0.0.0/8, via: 192.168.1.254}];nameservers.search=[lab.example.com]`
|
||||
|
||||
* On a wicked based OS (e.g. SUSE), keys are ifcfg variables (routes are not supported through this mechanism on wicked):
|
||||
`# nodeattrib node14 net.mgmt.extra_settings='ZONE=internal;ETHTOOL_OPTIONS=-K iface tso off'`
|
||||
`node14: net.mgmt.extra_settings: ZONE=internal;ETHTOOL_OPTIONS=-K iface tso off`
|
||||
|
||||
* Clear attribute on nodes of a simple noderange, if you want to retain the variable set the attribute to "":
|
||||
`# nodeattrib n1-n2 -c console.method`
|
||||
`# nodeattrib n1-n2 console.method`
|
||||
|
||||
@@ -44,7 +44,7 @@ It is sometimes the case that the number must be formatted a different way,
|
||||
either specifying 0 padding or converting to hexadecimal. This can be done by a
|
||||
number of operators at the end to indicate formatting changes.
|
||||
|
||||
`{n1:02x} - Zero pad to two decimal places, and convert to hexadecimal, as might be used for generating MAC addresses`
|
||||
`{n1:02x} - Zero pad to two decimal places, and convert to hexadecimal, as mightbe used for generating MAC addresses`
|
||||
`{n1:x} - Hexadecimal without padding, as may be used in a generated IPv6 address`
|
||||
`{n1:X} - Uppercase hexadecimal`
|
||||
`{n1:02d} - Zero pad a normal numeric representation of the number.`
|
||||
|
||||
@@ -20,9 +20,9 @@ nodebmcpassword(8) -- Change management controller password for a specified user
|
||||
|
||||
## EXAMPLES:
|
||||
|
||||
* Change the management controller password for user USERID on nodes n1 through n4:
|
||||
`# nodebmcpassword n1-n4 USERID newp4ssw0rd`
|
||||
`n1: Password Change Successful`
|
||||
`n2: Password Change Successful`
|
||||
`n3: Password Change Successful`
|
||||
`n4: Password Change Successful`
|
||||
* Reset the management controller for nodes n1 through n4:
|
||||
`# nodebmcreset n1-n4`
|
||||
`n1: Password Change Successful`
|
||||
`n2: Password Change Successful`
|
||||
`n3: Password Change Successful`
|
||||
`n4: Password Change Successful`
|
||||
@@ -1,50 +0,0 @@
|
||||
nodecertutil(8) -- Manage BMC CA certificates and sign BMC certificates
|
||||
=======================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodecertutil <noderange> <command>` [<args>]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodecertutil` manages the certificate authority (CA) certificates trusted by
|
||||
the baseboard management controllers (BMCs) of the nodes matched by
|
||||
`<noderange>`, and can sign a BMC certificate. One of the subcommands below
|
||||
must be supplied.
|
||||
|
||||
## COMMANDS
|
||||
|
||||
* `installbmccacert <filename>`:
|
||||
Install the CA certificate contained in `<filename>` onto the BMCs.
|
||||
|
||||
* `removebmccacert <id>`:
|
||||
Remove the CA certificate identified by `<id>` from the BMCs. Use
|
||||
`listbmccacerts` to discover the identifiers.
|
||||
|
||||
* `listbmccacerts`:
|
||||
List the CA certificates currently installed on the BMCs.
|
||||
|
||||
* `signbmccert` [`--days <days>`] [`--added-names <names>`]:
|
||||
Sign a BMC certificate. `--days` is required and sets the number of days
|
||||
the certificate is valid for; `--added-names` adds additional subject
|
||||
alternative names to the certificate.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* List the CA certificates installed on a node's BMC:
|
||||
`# nodecertutil n1 listbmccacerts`
|
||||
|
||||
* Install a CA certificate on a range of nodes:
|
||||
`# nodecertutil n1-n4 installbmccacert /etc/confluent/myca.pem`
|
||||
|
||||
* Sign a BMC certificate valid for one year:
|
||||
`# nodecertutil n1 signbmccert --days 365`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodeconfig(8), nodeattrib(8)
|
||||
@@ -51,10 +51,6 @@ actually be in effect until a reboot.
|
||||
Request that the specified component of the targeted nodes will have its
|
||||
configuration reset to default. The component may be "uefi" or "bmc".
|
||||
|
||||
* `-p, --pending`:
|
||||
When possible, indicate only the settings that are awaiting a reboot to take effect. Note that not all nodes will differentiate between
|
||||
pending and current settings, and for such platforms this will come up empty.
|
||||
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to configure, prompting if over
|
||||
the threshold
|
||||
@@ -72,7 +68,7 @@ actually be in effect until a reboot.
|
||||
`s4: bmc.ipv4_method: DHCP`
|
||||
`s4: bmc.ipv4_gateway: 172.30.0.6`
|
||||
|
||||
* Changing nodes `s3` and `s4` to have the IP addresses 10.1.2.3 and 10.1.2.4 with a 16 bit subnet mask:
|
||||
* Changing nodes `s3` and `s4` to have the ip addressess 10.1.2.3 and 10.1.2.4 with a 16 bit subnet mask:
|
||||
`# nodeconfig s3,s4 bmc.ipv4_address=10.1.2.{n1}/16`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
@@ -37,7 +37,7 @@ console process which will result in the console window closing.
|
||||
manager at this time.
|
||||
|
||||
* `-T`, `--Timestamp`:
|
||||
Dump the log with Timestamps on the current, local log in /var/log/confluent/consoles.
|
||||
Dump the log with Timpstamps on the current, local log in /var/log/confluent/consoles.
|
||||
If in collective mode, this only makes sense to use on the current collective
|
||||
manager at this time.
|
||||
|
||||
@@ -65,9 +65,9 @@ console process which will result in the console window closing.
|
||||
environment, to open a set of consoles for a range of
|
||||
nodes in separate Windows Terminal windows, with the
|
||||
title set for each node, set **NODECONSOLE_WINDOWED_COMMAND**
|
||||
to `wt.exe wsl.exe -d AlmaLinux-8 --shell-type login`. If the
|
||||
to `wt.exe wsl.exe -d AlmaLinux-8 --shell-type login. If the
|
||||
NODECONSOLE_WINDOWED_COMMAND environment variable isn't set,
|
||||
xterm will be used by default.
|
||||
xterm will be used bydefault.
|
||||
|
||||
## ESCAPE SEQUENCE COMMANDS
|
||||
|
||||
@@ -102,7 +102,7 @@ keystroke will be interpreted as a command. The following commands are availabl
|
||||
Request immediate boot to network
|
||||
* `r`:
|
||||
[send Resize]
|
||||
This queries the current terminal and sends stty commands to advertise the user terminal
|
||||
This queries the current terminal and sends stty commands to advertise the user termineal
|
||||
size to the remote console
|
||||
* `?`:
|
||||
Get a list of supported commands
|
||||
@@ -112,10 +112,10 @@ keystroke will be interpreted as a command. The following commands are availabl
|
||||
|
||||
## PASSTHROUGH OPTIONS
|
||||
|
||||
While opening a windowed console with xterm or any other console of choice. The
|
||||
nodeconsole command gives capability to specify passthrough options targeted at
|
||||
the console. All options after the -- will be passed to the console program. For
|
||||
example, opening a windowed console using xterm with a black background.
|
||||
`nodeconsole -w n1 -- -bg black`
|
||||
While opening a windowed console with xterm or any other console of choice. The
|
||||
nodeconsole command gives capality to specify passthrough options targeted at
|
||||
the console. All options after the -- will be parsed the console program. For
|
||||
example, opening a windowed console using xterm with a black background.
|
||||
`nodeconconsole -w n1 -- -bg black`
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ nodedefine(8) -- Define new confluent nodes
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodedefine <noderange> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
|
||||
`nodedefine <noderange> [nodeattribute1=value1> <nodeattribute2=value2> ...]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -26,4 +26,4 @@ that `nodeattrib(8)` will error if a node does not exist.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
noderange(5), nodeattribexpressions(5)
|
||||
noderange(5), nodeattribexpressions(8)
|
||||
|
||||
@@ -27,14 +27,14 @@ deployment status.
|
||||
Prepare the network services for deployment, but do not interact with BMCs. This is intended for scenarios where
|
||||
the boot device control and server restart will be handled outside of confluent.
|
||||
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to be deployed.
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specifiy a maximum nodes to be deployed.
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
* Begin the installation of a profile of CentOS 8.2:
|
||||
* Begin the instalalation of a profile of CentOS 8.2:
|
||||
`# nodedeploy d4 -n centos-8.2-x86_64-default`
|
||||
`d4: network`
|
||||
`d4: reset`
|
||||
|
||||
@@ -35,8 +35,8 @@ devices. Generally every effort is made to passively detect devices as they
|
||||
become available (as they boot or are plugged in), however sometimes an active
|
||||
scan is the best approach to catch something that appears to be missing.
|
||||
|
||||
**nodediscover clear** requests the server to forget about a selection of
|
||||
detected devices. It takes the same arguments as **nodediscover list**.
|
||||
**nodedsicover clear** requests the server forget about a selection of
|
||||
detected device. It takes the same arguments as **nodediscover list**.
|
||||
|
||||
**nodediscover subscribe** and **unsubscribe** instructs confluent to subscribe to or
|
||||
unsubscribe from the designated switch running affluent with system discovery support.
|
||||
@@ -75,9 +75,6 @@ the nodes.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-a`, `--aggressive`:
|
||||
Perform more aggressive scanning and fingerprinting. This will probe as many addresses as it can find with ssh and https connections. By default more targeted measures are used
|
||||
that will only tend to probe things that are easily detectable as devices implementing explicit mechanisms for detection and identification.
|
||||
* `-m MODEL`, `--model=MODEL`:
|
||||
Operate with nodes matching the specified model number
|
||||
* `-s SERIAL`, `--serial=SERIAL`:
|
||||
|
||||
@@ -3,28 +3,13 @@ nodeeventlog(8) -- Pull eventlog from confluent nodes
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeeventlog [options] [-s <sources>] <noderange> [clear]`
|
||||
`nodeeventlog [options] <noderange> [clear]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodeeventlog` pulls and optionally clears the event log from the requested
|
||||
noderange.
|
||||
|
||||
A platform may keep its events in more than one log, and some of those are
|
||||
noisier than others. Reading gathers every log the platform describes as an
|
||||
event log, wherever it keeps them, and `-s` narrows the output to the ones
|
||||
asked for. `-s list` names the logs a node offers rather than showing entries.
|
||||
|
||||
A source is named by the entries that come from it, so a log that holds no
|
||||
entries is not among them, and a node whose logs are all empty names none. An
|
||||
ipmi managed node keeps its events in the one log the spec gives it, which is
|
||||
reported as `SEL`.
|
||||
|
||||
Clearing is deliberately narrower than reading: it only touches the logs the
|
||||
platform's own manager publishes, so a log that only a read reaches is never
|
||||
destroyed by one. A selection cannot be cleared, so `-s` and `clear` are
|
||||
refused together.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
@@ -35,31 +20,15 @@ refused together.
|
||||
return the last <n> entries for each node in the eventlog.
|
||||
|
||||
* `-t TIMEFRAME`, `--timeframe=TIMEFRAME`:
|
||||
return entries within a specified timeframe for each node's event log.
|
||||
This will return entries from the last <n> hours or days. 1h would be
|
||||
entries from within the last one hour.
|
||||
return entries within a specified timeframe for each node's event log.
|
||||
This will return entries from the last <n> hours or days. 1h would be
|
||||
entries from with the last one hour.
|
||||
|
||||
|
||||
* `-s SOURCES`, `--source=SOURCES`:
|
||||
only show entries from the named logs, comma delimited and matched without
|
||||
regard to case. `-s list` names the logs the node offers instead of showing
|
||||
entries. A platform that named a log "list" would be shadowed by that.
|
||||
Asking for a log the node named nothing from is reported on stderr and
|
||||
exits non-zero, rather than looking like an empty log.
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
* Ask which logs a node keeps:
|
||||
`# nodeeventlog n2 -s list`
|
||||
`n2: AuditLog,EventLog,Logs,SEL`
|
||||
|
||||
* Pull only the sel and the chassis log, leaving the audit noise out:
|
||||
`# nodeeventlog n2 -s SEL,Logs`
|
||||
`n2: 07/23/2026 13:03:12 SEL: Processor - Thermal Trip`
|
||||
`n2: 08/05/2026 04:30:26 Logs: PowerUnit - Power Off`
|
||||
|
||||
* Pull the event log from n2 and n3:
|
||||
`# nodeeventlog n2,n3`
|
||||
`n2: 05/03/2017 11:44:25 Event Log Disabled - SEL Fullness - Log clear`
|
||||
|
||||
@@ -3,7 +3,7 @@ nodefirmware(8) -- Report firmware information on confluent nodes
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodefirmware <noderange> [list][updatestatus][updatetypes][update [--backup] [--parameterfile <file>] <file>]|[<components>]`
|
||||
`nodefirmware <noderange> [list][updatestatus][update [--backup <file>]]|[<components>]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -20,10 +20,6 @@ the FPGA version where applicable).
|
||||
The updatestatus argument will describe the state of firmware updates on the
|
||||
nodes.
|
||||
|
||||
The updatetypes argument will name the kinds of firmware image the platform has
|
||||
to be told about, for use in a parameter file. Platforms that read the kind of
|
||||
firmware from the image itself have none to name and say so.
|
||||
|
||||
In the update form, it accepts a single file and attempts to update it using
|
||||
the out of band facilities. Firmware updates can end in one of three states:
|
||||
|
||||
@@ -40,42 +36,14 @@ the out of band facilities. Firmware updates can end in one of three states:
|
||||
When updating, prompt if more than the specified number of servers will
|
||||
be affected
|
||||
|
||||
* `-p PARAMETERFILE`, `--parameterfile=PARAMETERFILE`:
|
||||
For updating, a parameter file to provide along with the update payload. See
|
||||
PARAMETER FILE below
|
||||
* `-p PARAMETERFILE`, `--paramaterfile=PARAMETERFILE`:
|
||||
For updating, a parameter file to provife along with the update payload
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## PARAMETER FILE
|
||||
|
||||
The parameter file given with `-p` holds JSON that is sent alongside the image.
|
||||
Its keys are the parts a Redfish multipart update accepts, so `UpdateParameters`
|
||||
for the standard fields and `OemParameters` for whatever a vendor asks for on top
|
||||
of them. Without one, an update names no targets, which means "whatever this
|
||||
image is for", and that is what most platforms want.
|
||||
|
||||
Some platforms will not take an image without being told what kind of firmware it
|
||||
holds, and refuse the update rather than guess. On an AMI MegaRAC that is an
|
||||
`ImageType` under `OemParameters`:
|
||||
|
||||
{"OemParameters": {"ImageType": "BIOS"}}
|
||||
|
||||
`nodefirmware <noderange> updatetypes` names the accepted types without
|
||||
attempting an update. The value is also checked against the same list before
|
||||
anything is uploaded, so a name the platform does not accept is refused rather
|
||||
than sent.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Ask what kinds of firmware image a node has to be told about:
|
||||
`# nodefirmware n1 updatetypes`
|
||||
`n1: BMC,BIOS,MB_CPLD,SCM_CPLD,BPB_CPLD,HPM_BMC,HPM_BIOS,HPM_SCP,HPM_BIOS2`
|
||||
|
||||
* Update the BIOS on a platform that has to be told:
|
||||
`# echo '{"OemParameters": {"ImageType": "BIOS"}}' > /tmp/bios.json`
|
||||
`# nodefirmware n1 update -p /tmp/bios.json /tmp/image.bin`
|
||||
|
||||
* Pull firmware from a node:
|
||||
`# nodefirmware r1`
|
||||
`r1: IMM: 3.70 (TCOO26H 2016-11-29T05:09:51)`
|
||||
|
||||
@@ -23,10 +23,6 @@ node after using the nodeattrib(8) command will not have attributes change autom
|
||||
It's easiest to see by using the `nodeattrib <noderange> -b` to understand how
|
||||
the attributes are set on the node versus a group to which a node belongs.
|
||||
|
||||
The values of `secret.*` and `crypted.*` attributes are never shown here; they
|
||||
can only be retrieved on the confluent server with
|
||||
`confluentdbutil -u showattrib`.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-b`, `--blame`:
|
||||
|
||||
@@ -3,7 +3,7 @@ nodegroupdefine(8) -- Define new confluent node group
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodegroupdefine <groupname> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
|
||||
`nodegroupdefine <groupname> [nodeattribute1=value1> <nodeattribute2=value2> ...]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -21,4 +21,4 @@ that `nodegroupattrib(8)` will error if a node group does not exist.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodeattribexpressions(5), nodegroupattrib(8), nodegroupremove(8)
|
||||
nodeattribexpressions(8), nodegroupattrib(8), nodegroupremove(8)
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
nodegrouprename(8) -- Rename a node group in the confluent management service
|
||||
=============================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodegrouprename <group> <newname>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodegrouprename` changes the name of an existing node group from `<group>` to
|
||||
`<newname>`. Group membership and the attributes defined on the group are
|
||||
preserved under the new name.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Rename a group:
|
||||
`# nodegrouprename compute computenodes`
|
||||
`compute: computenodes`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodegroupdefine(8), nodegroupremove(8), nodegroupattrib(8), nodegrouplist(8)
|
||||
@@ -3,11 +3,11 @@ nodeidentify(8) -- Control the identify LED of confluent nodes
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeidentify <noderange> [on|off|blink]`
|
||||
`nodidentify <noderange> [on|off|blink]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodeidentify` allows you to turn on or off the location LED of confluent nodes,
|
||||
`nodeidentify` allows you to turn on or off the location LED of conflueunt nodes,
|
||||
making it easier to determine the physical location of the nodes. The following
|
||||
options are supported:
|
||||
|
||||
@@ -24,7 +24,7 @@ options are supported:
|
||||
`n3: on`
|
||||
`n4: on`
|
||||
|
||||
* Turn off the identify LED on nodes n1 through n4:
|
||||
* Turn off the identify LED on nodes n1 thorugh n4:
|
||||
`# nodeidentify n1-n4 off`
|
||||
`n1: off`
|
||||
`n2: off`
|
||||
|
||||
@@ -9,7 +9,7 @@ nodeinventory(8) -- Get hardware inventory of confluent node
|
||||
|
||||
`nodeinventory` pulls information about hardware of a node. This includes
|
||||
information such as adapters, serial numbers, processors, and memory modules,
|
||||
as supported by the platform's hardware management implementation. It accepts
|
||||
as supported by the platforms hardware management implementation. It accepts
|
||||
arguments such as serial or model or others as listed above to filter
|
||||
output to specific data.
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
nodel2traceroute(8) -- Returns the layer 2 route through an Ethernet network managed by confluent given 2 end points.
|
||||
nodel2traceroute(8) -- returns the layer 2 route through an Ethernet network managed by confluent given 2 end points.
|
||||
==============================
|
||||
## SYNOPSIS
|
||||
`nodel2traceroute [options] <start_node> <end_noderange>`
|
||||
|
||||
## DESCRIPTION
|
||||
**nodel2traceroute** is a command that returns the layer 2 route for the configured interfaces in nodeattrib.
|
||||
**nodel2traceroute** is a command that returns the layer 2 route for the configered interfaces in nodeattrib.
|
||||
It can also be used with the -i and -e options to check against specific interfaces on the endpoints. If the
|
||||
--interface or --eface option are not used then the command will check for routes against all the defined
|
||||
interfaces in nodeattrib (net.*.switch) for the nodes.
|
||||
@@ -17,7 +17,7 @@ interfaces in nodeattrib (net.*.switch) for the nodes.
|
||||
|
||||
## OPTIONS
|
||||
* ` -e` EFACE, --eface=INTERFACE
|
||||
interface to check against for the second end point or end points if checking against multiple nodes
|
||||
interface to check against for the second end point or end points if using checking against multiple nodes
|
||||
* ` -i` INTERFACE, --interface=INTERFACE
|
||||
interface to check against for the first end point
|
||||
* ` -c` CUMULUS, --cumulus=CUMULUS
|
||||
|
||||
@@ -8,7 +8,7 @@ nodelicense(8) -- Manage license keys on BMC
|
||||
## DESCRIPTION
|
||||
|
||||
`nodelicense` manages license keys on supported BMCs. Without an argument, the command
|
||||
lists currently installed licenses. Using `delete` will remove the specified license name
|
||||
lists currently installed license. Using `delete` will remove the specified license name
|
||||
from the BMC. The `save` subcommand will take the passed directory (which may be in the form
|
||||
of /path/to/{node}/ to have the node name substituted for each node) and back up installed licenses
|
||||
to that directory. The `install` command will take the specified filename and install. The filename
|
||||
|
||||
@@ -15,7 +15,7 @@ matching nodes, one line at a time.
|
||||
If a list of node attribute names are given, the value of those are also
|
||||
displayed. If `-b` is specified, it will also display information on
|
||||
how inherited and expression based attributes are defined. There is more
|
||||
information on node attributes in the nodeattrib(8) man page.
|
||||
information on node attributes in nodeattributes(5) man page.
|
||||
|
||||
Attributes may be specified by wildcard, for example `net.*switch` will report
|
||||
all attributes that begin with `net.` and end with `switch`.
|
||||
@@ -25,7 +25,7 @@ all attributes that begin with `net.` and end with `switch`.
|
||||
* `-b`, `--blame`:
|
||||
Annotate inherited and expression based attributes to show their base value.
|
||||
* `-d`, `--delim`:
|
||||
Choose a delimiter to separate the values. Default - ENTER.
|
||||
Choose a delimiter to separat the values. Default - ENTER.
|
||||
## EXAMPLES
|
||||
* Listing matching nodes of a simple noderange:
|
||||
`# nodelist n1-n4`
|
||||
@@ -40,7 +40,7 @@ all attributes that begin with `net.` and end with `switch`.
|
||||
`n2: hardwaremanagement.manager: 172.30.3.2`
|
||||
|
||||
* Getting a group of attributes while determining what group defines them:
|
||||
`# nodelist n1,n2 hardwaremanagement --blame`
|
||||
`# nodelist n1,n2 hardwaremanegement --blame`
|
||||
`n1: hardwaremanagement.manager: 172.30.3.1`
|
||||
`n1: hardwaremanagement.method: ipmi (inherited from group everything)`
|
||||
`n1: hardwaremanagement.switch: r8e1`
|
||||
|
||||
@@ -14,8 +14,8 @@ It can also be used with the `-s` flag to change the ping location to something
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
* `-s` SUBSTITUTENAME, --substitutename=SUBSTITUTENAME
|
||||
Use a different name other than the nodename for ping. This may be an
|
||||
expression, such as {bmc} or, if no { character is present, it is treated as a suffix. -s -eth1 would make n1 become n1-eth1, for example.
|
||||
Use a different name other than the nodename for ping. This may be a
|
||||
expression, such as {bmc} or, if no { character is present, it is treated as a suffix. -s -eth1 would make n1 become n1-eth1, for example.
|
||||
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
noderename(8) -- Rename nodes in the confluent management service
|
||||
=================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`noderename <noderange> <newname>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`noderename` changes the name of the node or nodes matched by the given
|
||||
noderange to `<newname>`. As with other attribute changes, the rename is
|
||||
applied through the confluent datastore and does not by itself alter the
|
||||
operating system hostname of a running node.
|
||||
|
||||
When renaming more than one node, `<newname>` is normally an expression so that
|
||||
each matched node receives a distinct name (see noderange(5) and the attribute
|
||||
expression syntax).
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Rename a single node:
|
||||
`# noderename n1 n2`
|
||||
|
||||
* Rename a range of nodes using an expression:
|
||||
`# noderename n1-n4 'compute{n1}'`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodedefine(8), noderemove(8), nodeattrib(8), noderange(5)
|
||||
@@ -24,7 +24,7 @@ interval of 1 second is used.
|
||||
the results, which may have a different ordering than non-CSV usage of nodesensors.
|
||||
|
||||
* `-i`, `--interval`=**SECONDS**:
|
||||
Repeat data gathering, waiting the specified time between samples. Unless `-n` is
|
||||
Repeat data gathering waiting, waiting the specified time between samples. Unless `-n` is
|
||||
specified, indefinite retrieval is assumed.
|
||||
|
||||
* `-n`, `--numreadings`=**SAMPLES**:
|
||||
|
||||
@@ -10,7 +10,7 @@ nodesetboot(8) -- Check or set next boot device for noderange
|
||||
Requests that the next boot occur from the specified device. Unless otherwise
|
||||
specified, this is a one time boot option, and does not change the normal boot
|
||||
behavior of the system. This is useful for taking a system that normally boots
|
||||
to the hard drive and starting a network install, or to go into the firmware
|
||||
to the hard drive and startking a network install, or to go into the firmware
|
||||
setup menu without having to hit a keystroke at the correct time on the console.
|
||||
|
||||
Generally, it's a bit more convenient and direct to use the nodeboot(8) command,
|
||||
|
||||
@@ -30,16 +30,13 @@ as stderr, unlike psh which combines all stdout and stderr into stdout.
|
||||
* `-p PORT`, `--port=PORT`
|
||||
Specify a custom port for ssh
|
||||
|
||||
* `-s SUBSTITUTION`, `--substitutename=SUBSTITUTION`
|
||||
* `-s SUBSTITUTION`, `--substitutename=SUBSTITITUTION`
|
||||
Specify a substitution name instead of the nodename. If no {} are in the substitution,
|
||||
it is considered to be an append. For example, '-s -ib' would produce 'node1-ib' from 'node1'.
|
||||
Full expression syntax is supported, in which case the substitution is considered to be the entire
|
||||
new name. {node}-ib would be equivalent to -ib. For example, nodeshell -s {bmc} node1
|
||||
new name. {node}-ib would be equivalent to -ib. For example, nodeshell -s {bmc} node1
|
||||
would ssh to the BMC instead of the node.
|
||||
|
||||
* `-t TIMEOUT`, `--timeout=TIMEOUT`
|
||||
Timeout in seconds for each node ssh connection attempt
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Running `echo hi` on for nodes:
|
||||
|
||||
@@ -3,7 +3,7 @@ stats(8) -- Common basic statistics on typical numeric data in output
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`<other command> | stats [-c N] [-d D] [-x|-g|-t|-o image.png] [-s N] [-v] [-b N]`
|
||||
`<other command> | stats [-c N] [-d D] [-x|-g|-t|-o image.png] [-s N] [-v] [-b N]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -11,11 +11,11 @@ The **stats** command helps analyze common numerical data such as performance nu
|
||||
or temperatures or any other numerical value.
|
||||
|
||||
By default it looks for the last numerical output on the first line to identify the numerical column
|
||||
and analyze that number. This can be overridden by **-c COLUMN** to indicate a column. By default,
|
||||
and analyze that number. This can be overriden by **-c COLUMN** to indicate a column. By default,
|
||||
whitespace and commas are treated to delimit columns, and **-d DELIMITER** can be used to override.
|
||||
|
||||
By default it outputs basic statistics, but a histogram is available either text or through X11 output
|
||||
or sixel or output to an image file depending on whether **-x**, **-g**, **-t**, or **-o image.png** is
|
||||
or sixel or output to an image file depending on whether **-x**, **-g**, **-t*, or **-o image.png** is
|
||||
used.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
@@ -5,20 +5,7 @@ cd `dirname $0`/doc/man
|
||||
mkdir -p ../../man/man1
|
||||
mkdir -p ../../man/man5
|
||||
mkdir -p ../../man/man8
|
||||
for ronn in *.ronn; do
|
||||
# The first heading line is of the form "name(section) -- description"
|
||||
header=`grep -m1 -E '\([0-9]+\) *--+ ' "$ronn"`
|
||||
name=`echo "$header" | sed -E 's/^#* *([A-Za-z0-9._-]+)\(([0-9]+)\).*/\1/'`
|
||||
section=`echo "$header" | sed -E 's/^#* *([A-Za-z0-9._-]+)\(([0-9]+)\).*/\2/'`
|
||||
# Strip the ronn title heading (and any setext underline) and promote the
|
||||
# remaining section headings so pandoc emits them as man .SH sections.
|
||||
sed -E '/\([0-9]+\) *--+ /d; /^=+$/d' "$ronn" | \
|
||||
pandoc --standalone --from markdown --to man \
|
||||
--shift-heading-level-by=-1 \
|
||||
--metadata title="$name" \
|
||||
--metadata section="$section" \
|
||||
-o "$name.$section"
|
||||
done
|
||||
ronn -r *.ronn
|
||||
mv *.1 ../../man/man1/
|
||||
mv *.5 ../../man/man5/
|
||||
mv *.8 ../../man/man8/
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
import confluent.client as cl
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
c = cl.Command()
|
||||
macs = []
|
||||
interface = sys.argv[1]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1.0.1
|
||||
@@ -1,14 +1,21 @@
|
||||
cd $(dirname $0)
|
||||
VERSION=`../mkversion --tilde` || exit 1
|
||||
VERSION=`git describe|cut -d- -f 1`
|
||||
NUMCOMMITS=`git describe|cut -d- -f 2`
|
||||
if [ "$NUMCOMMITS" != "$VERSION" ]; then
|
||||
LASTNUM=$(echo $VERSION|rev|cut -d . -f 1|rev)
|
||||
LASTNUM=$((LASTNUM+1))
|
||||
FIRSTPART=$(echo $VERSION|rev|cut -d . -f 2- |rev)
|
||||
VERSION=${FIRSTPART}.${LASTNUM}
|
||||
VERSION=$VERSION~dev$NUMCOMMITS+`git describe|cut -d- -f 3`
|
||||
fi
|
||||
sed -e "s/#VERSION#/$VERSION/" confluent_osdeploy.spec.tmpl > confluent_osdeploy.spec
|
||||
cp ../LICENSE .
|
||||
cd ..
|
||||
cp ../LICENSE .
|
||||
tar Jcvf confluent_osdeploy.tar.xz confluent_osdeploy
|
||||
mv confluent_osdeploy.tar.xz ~/rpmbuild/SOURCES/
|
||||
cd -
|
||||
mkdir -p el9bin/opt/confluent/bin
|
||||
mkdir -p el9bin/stateless-bin
|
||||
podman run --privileged --rm -v $(pwd)/utils:/buildutils -i -t el9build make -C /buildutils
|
||||
podman run --privileged --rm -v $(pwd)/utils:/buildutils -i -t fedorabuild make -C /buildutils
|
||||
cd utils
|
||||
cp confluent_imginfo copernicus clortho autocons ../el9bin/opt/confluent/bin
|
||||
cp start_root urlmount ../el9bin/stateless-bin/
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
cd $(dirname $0)
|
||||
VERSION=`../mkversion --tilde` || exit 1
|
||||
VERSION=`git describe|cut -d- -f 1`
|
||||
NUMCOMMITS=`git describe|cut -d- -f 2`
|
||||
if [ "$NUMCOMMITS" != "$VERSION" ]; then
|
||||
LASTNUM=$(echo $VERSION|rev|cut -d . -f 1|rev)
|
||||
LASTNUM=$((LASTNUM+1))
|
||||
FIRSTPART=$(echo $VERSION|rev|cut -d . -f 2- |rev)
|
||||
VERSION=${FIRSTPART}.${LASTNUM}
|
||||
VERSION=$VERSION~dev$NUMCOMMITS+`git describe|cut -d- -f 3`
|
||||
fi
|
||||
sed -e "s/#VERSION#/$VERSION/" confluent_osdeploy-aarch64.spec.tmpl > confluent_osdeploy-aarch64.spec
|
||||
cp ../LICENSE .
|
||||
cd ..
|
||||
cp ../LICENSE .
|
||||
tar Jcvf confluent_osdeploy.tar.xz confluent_osdeploy
|
||||
mv confluent_osdeploy.tar.xz ~/rpmbuild/SOURCES/
|
||||
cd -
|
||||
|
||||
@@ -6,12 +6,14 @@ except ImportError:
|
||||
import base64
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import glob
|
||||
import os
|
||||
import select
|
||||
import socket
|
||||
import subprocess
|
||||
import ssl
|
||||
import sys
|
||||
import struct
|
||||
import time
|
||||
import re
|
||||
import hashlib
|
||||
@@ -22,10 +24,6 @@ except ImportError:
|
||||
json = None
|
||||
hmac = None
|
||||
|
||||
# Ignore proxy-based environment variables
|
||||
for proxy_var in ['http_proxy', 'https_proxy', 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'all_proxy', 'NO_PROXY', 'no_proxy']:
|
||||
os.environ.pop(proxy_var, None)
|
||||
|
||||
class InvalidApiKey(Exception):
|
||||
pass
|
||||
|
||||
@@ -161,26 +159,12 @@ def scan_confluents(confuuid=None):
|
||||
srvs = {}
|
||||
s6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
s6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
||||
try:
|
||||
s6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
except Exception:
|
||||
# REUSPORT is ideal, but if python is missing it, just live with the limitation
|
||||
pass
|
||||
try: # We would like to bind for firewall rules, however if not possible then leave it alone
|
||||
s6.bind(('::', 1900))
|
||||
except Exception:
|
||||
pass
|
||||
s6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
s6.bind(('::', 1900))
|
||||
s4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s4.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
try:
|
||||
s4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
except Exception:
|
||||
# REUSPORT is ideal, but if python is missing it, just live with the limitation
|
||||
pass
|
||||
try:
|
||||
s4.bind(('0.0.0.0', 1900))
|
||||
except Exception:
|
||||
pass
|
||||
s4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
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 and os.path.exists('/etc/confluent/confluent.deploycfg'):
|
||||
|
||||
@@ -36,36 +36,6 @@ def add_lla(iface, mac):
|
||||
return None
|
||||
return lla
|
||||
|
||||
def create_systemd_link(nic, altnames):
|
||||
if not os.path.exists('/etc/systemd/network'):
|
||||
return
|
||||
for disallowed in ['..', ' ', '/', '\n', '=']:
|
||||
if disallowed in nic:
|
||||
raise ValueError('Invalid nic name')
|
||||
|
||||
if not isinstance(altnames, list):
|
||||
altnames = altnames.split(',')
|
||||
filteredaltnames = []
|
||||
for altname in altnames:
|
||||
altname = altname.strip()
|
||||
for disallowed in ['..', ' ', '/', '\n', '=']:
|
||||
if disallowed in altname:
|
||||
break
|
||||
else:
|
||||
if altname and altname not in filteredaltnames:
|
||||
filteredaltnames.append(altname)
|
||||
with open(f'/sys/class/net/{nic}/address') as macin:
|
||||
macaddr = macin.read().strip()
|
||||
with open(f'/etc/systemd/network/00-{nic}.link', 'w') as linkout:
|
||||
linkout.write('[Match]\n')
|
||||
linkout.write(f'MACAddress={macaddr}\n\n')
|
||||
linkout.write('[Link]\n')
|
||||
linkout.write('NamePolicy=kernel database onboard slot path\n')
|
||||
linkout.write('AlternativeNamesPolicy=database onboard slot path mac\n')
|
||||
for altname in filteredaltnames:
|
||||
linkout.write(f'AlternativeName={altname}\n')
|
||||
subprocess.check_call(['udevadm', 'trigger', '--action=add', '--subsystem-match=net'])
|
||||
|
||||
#cli = apiclient.HTTPSClient(json=True)
|
||||
#c = cli.grab_url_with_status('/confluent-api/self/netcfg')
|
||||
def add_missing_llas():
|
||||
@@ -144,14 +114,6 @@ def get_interface_name(iname, settings):
|
||||
return iname
|
||||
return None
|
||||
|
||||
def parse_extra_settings(stgs):
|
||||
extras = {}
|
||||
for kv in stgs.get('extra_settings', '').split(';'):
|
||||
k, _, v = kv.partition('=')
|
||||
if k.strip():
|
||||
extras[k.strip()] = v.strip()
|
||||
return extras
|
||||
|
||||
class NetplanManager(object):
|
||||
def __init__(self, deploycfg):
|
||||
self.cfgbydev = {}
|
||||
@@ -189,9 +151,8 @@ class NetplanManager(object):
|
||||
currinfo[devname]['routes'] = routeinfo
|
||||
currcfg[devname] = currinfo[devname]
|
||||
|
||||
def apply_configuration(self, cfg, lastchance=False):
|
||||
def apply_configuration(self, cfg):
|
||||
devnames = cfg['interfaces']
|
||||
teammode = None
|
||||
if len(devnames) > 1:
|
||||
teammode = cfg['settings'].get('team_mode', None)
|
||||
if not teammode:
|
||||
@@ -268,29 +229,8 @@ class NetplanManager(object):
|
||||
if dnsdomain not in currdnsdomain:
|
||||
needcfgwrite = True
|
||||
currdnsdomain.append(dnsdomain)
|
||||
extras = parse_extra_settings(stgs)
|
||||
if extras:
|
||||
currcfg = self.cfgbybond if devname in self.cfgbybond else self.cfgbydev
|
||||
devdict = currcfg.setdefault(devname, {})
|
||||
for key in extras:
|
||||
try:
|
||||
val = yaml.safe_load(extras[key])
|
||||
except yaml.YAMLError:
|
||||
val = extras[key]
|
||||
keyptr = devdict
|
||||
keypath = key.split('.')
|
||||
for k in keypath[:-1]:
|
||||
if not isinstance(keyptr.get(k, None), dict):
|
||||
keyptr[k] = {}
|
||||
keyptr = keyptr[k]
|
||||
if keyptr.get(keypath[-1], None) != val:
|
||||
needcfgwrite = True
|
||||
keyptr[keypath[-1]] = val
|
||||
prune_from_cloudinit = []
|
||||
if needcfgwrite:
|
||||
connname = cfg['settings'].get('connection_name', None)
|
||||
if not teammode and connname and connname != devname:
|
||||
create_systemd_link(devname, [connname])
|
||||
needcfgapply = True
|
||||
oumask = os.umask(0o77)
|
||||
if devname in self.cfgbydev:
|
||||
@@ -303,11 +243,9 @@ class NetplanManager(object):
|
||||
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}}}}))
|
||||
os.chmod('/etc/netplan/10-{0}-confluentcfg.yaml'.format(iface), 0o600)
|
||||
cfgfile = '/etc/netplan/30-{0}-confluentcfg.yaml'.format(devname)
|
||||
with open(cfgfile, 'w') as planout:
|
||||
planout.write(yaml.dump(newcfg))
|
||||
os.chmod(cfgfile, 0o600)
|
||||
os.umask(oumask)
|
||||
if prune_from_cloudinit:
|
||||
prunecfgs = ['/etc/netplan/00-installer-config.yaml',
|
||||
@@ -319,7 +257,7 @@ class NetplanManager(object):
|
||||
continue
|
||||
with open(defcfg, 'r') as cloudinit:
|
||||
cloudinfo = yaml.safe_load(cloudinit)
|
||||
if not cloudinfo or 'network' not in cloudinfo:
|
||||
if 'network' not in cloudinfo:
|
||||
continue
|
||||
for clouddev in list(cloudinfo.get('network', {}).get('ethernets', {})):
|
||||
if clouddev in prune_from_cloudinit:
|
||||
@@ -378,7 +316,7 @@ class WickedManager(object):
|
||||
v = v.strip()
|
||||
currcfg[k] = v
|
||||
|
||||
def apply_configuration(self, cfg, lastchance=False):
|
||||
def apply_configuration(self, cfg):
|
||||
stgs = cfg['settings']
|
||||
ipcfg = 'STARTMODE=auto\n'
|
||||
routecfg = ''
|
||||
@@ -398,9 +336,6 @@ class WickedManager(object):
|
||||
if stgs.get('ipv6_address', None):
|
||||
ipcfg += 'IPADDR_V6=' + stgs['ipv6_address'] + '\n'
|
||||
v6gw = stgs.get('ipv6_gateway', None)
|
||||
extras = parse_extra_settings(stgs)
|
||||
for key in extras:
|
||||
ipcfg += '{0}={1}\n'.format(key, shlex.quote(extras[key]))
|
||||
cname = None
|
||||
if len(cfg['interfaces']) > 1: # creating new team
|
||||
if not stgs.get('team_mode', None):
|
||||
@@ -540,7 +475,6 @@ class NetworkManager(object):
|
||||
cmdargs['ipv4.dns'] = ','.join(dns4)
|
||||
if dns6:
|
||||
cmdargs['ipv6.dns'] = ','.join(dns6)
|
||||
cmdargs.update(parse_extra_settings(stgs))
|
||||
if len(cfg['interfaces']) > 1: # team time.. should be..
|
||||
if not cfg['settings'].get('team_mode', None):
|
||||
sys.stderr.write("Warning, multiple interfaces ({0}) without a team_mode, skipping setup\n".format(','.join(cfg['interfaces'])))
|
||||
@@ -584,8 +518,6 @@ class NetworkManager(object):
|
||||
return
|
||||
cname = iname if not cname else cname
|
||||
u = self.uuidbyname.get(cname, None)
|
||||
if cname != iname:
|
||||
create_systemd_link(iname, [cname])
|
||||
cargs = []
|
||||
for arg in cmdargs:
|
||||
cargs.append(arg)
|
||||
@@ -663,18 +595,18 @@ if __name__ == '__main__':
|
||||
time.sleep(1)
|
||||
continue
|
||||
dc = json.loads(dc)
|
||||
inames = get_interface_name(idxmap[curridx], nc.get('default', {}))
|
||||
if inames:
|
||||
for iname in inames.split(','):
|
||||
iname = get_interface_name(idxmap[curridx], nc.get('default', {}))
|
||||
if iname:
|
||||
for iname in iname.split(','):
|
||||
if 'default' in netname_to_interfaces:
|
||||
netname_to_interfaces['default']['interfaces'].add(iname)
|
||||
else:
|
||||
netname_to_interfaces['default'] = {'interfaces': set([iname]), 'settings': nc['default']}
|
||||
for netname in nc.get('extranets', {}):
|
||||
uname = '_' + netname
|
||||
inames = get_interface_name(idxmap[curridx], nc['extranets'][netname])
|
||||
if inames:
|
||||
for iname in inames.split(','):
|
||||
iname = get_interface_name(idxmap[curridx], nc['extranets'][netname])
|
||||
if iname:
|
||||
for iname in iname.split(','):
|
||||
if uname in netname_to_interfaces:
|
||||
netname_to_interfaces[uname]['interfaces'].add(iname)
|
||||
else:
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
import json
|
||||
import subprocess
|
||||
import os
|
||||
import shutil
|
||||
|
||||
|
||||
class SilentException(Exception):
|
||||
pass
|
||||
|
||||
# List of models that are generally used for OS install disk (generally 2-disk mirroring)
|
||||
PRIORITY_MODELS = ('m.2 nvme 2-bay raid kit',
|
||||
'thinksystem_m.2_vd',
|
||||
'thinksystem m.2',
|
||||
'thinksystem_m.2',
|
||||
'thinksystem 7mm',
|
||||
'thinksystem_7mm',
|
||||
'thinksystem_7mm_vd',
|
||||
'b540p-2hs', # 7mm or M.2 in rear
|
||||
'b540d-2hs', # M.2 in E3.S or 2.5inch
|
||||
'b550d-2hs', # Front M.2
|
||||
'b550p-2hs', # Rear M.2
|
||||
'raid b540p-2hs',
|
||||
'raid b540d-2hs',
|
||||
'raid b550d-2hs',
|
||||
'raid b550p-2hs',
|
||||
'raid b540i-2i',
|
||||
'raid_b540i-2i',
|
||||
'raid b545i-2i',
|
||||
'raid b545i-2i',
|
||||
'raid_b545-2i')
|
||||
|
||||
BOOT_SLOT_KEYWORDS = ('m.2 socket', '7mm')
|
||||
|
||||
class DiskInfo(object):
|
||||
def __init__(self, devname, slotinfo=None):
|
||||
if devname.startswith('nvme') and 'c' in devname:
|
||||
raise Exception("Skipping multipath devname")
|
||||
self.name = devname
|
||||
self.wwn = None
|
||||
self.path = None
|
||||
self.model = ''
|
||||
self.size = 0
|
||||
self.driver = ''
|
||||
self.mdcontainer = ''
|
||||
self.subsystype = ''
|
||||
self.kernnames = []
|
||||
self.vrocmembers = []
|
||||
self.is_m2 = False
|
||||
self.in_vroc = False
|
||||
devnode = '/dev/{0}'.format(devname)
|
||||
qprop = subprocess.check_output(
|
||||
['udevadm', 'info', '--query=property', devnode])
|
||||
if not isinstance(qprop, str):
|
||||
qprop = qprop.decode('utf8')
|
||||
for prop in qprop.split('\n'):
|
||||
if '=' not in prop:
|
||||
continue
|
||||
k, v = prop.split('=', 1)
|
||||
if k == 'DEVTYPE' and v != 'disk':
|
||||
if v == 'partition':
|
||||
raise SilentException('Partition')
|
||||
raise Exception('Not a disk')
|
||||
elif k == 'DM_NAME':
|
||||
raise SilentException('Device Mapper')
|
||||
elif k == 'ID_MODEL':
|
||||
self.model = v
|
||||
elif k == 'DEVPATH':
|
||||
self.path = v
|
||||
elif k == 'ID_WWN':
|
||||
self.wwn = v
|
||||
elif k == 'MD_CONTAINER':
|
||||
self.mdcontainer = v
|
||||
attrs = subprocess.check_output(['udevadm', 'info', '-a', devnode])
|
||||
if not isinstance(attrs, str):
|
||||
attrs = attrs.decode('utf8')
|
||||
for attr in attrs.split('\n'):
|
||||
if '==' not in attr:
|
||||
continue
|
||||
k, v = attr.split('==', 1)
|
||||
k = k.strip()
|
||||
if k == 'ATTRS{size}':
|
||||
self.size = v.replace('"', '')
|
||||
elif (k == 'DRIVERS' and not self.driver
|
||||
and v not in ('"sd"', '""')):
|
||||
self.driver = v.replace('"', '')
|
||||
elif k == 'ATTRS{subsystype}':
|
||||
self.subsystype = v.replace('"', '')
|
||||
elif k == 'ATTR{ro}' and v == '"1"':
|
||||
raise Exception("Device is read-only")
|
||||
elif k == 'ATTRS{removable}' and v == '"1"':
|
||||
raise Exception("Device is removable")
|
||||
elif k == 'ATTRS{address}':
|
||||
self.busaddr = v.replace('"', '')
|
||||
elif k == 'KERNELS':
|
||||
self.kernnames.append(v.replace('"', ''))
|
||||
if not getattr(self, 'busaddr', None):
|
||||
for kernname in getattr(self, 'kernnames', []):
|
||||
if kernname in slotinfo:
|
||||
self.is_m2 = True
|
||||
break
|
||||
if getattr(self, 'busaddr', None) and self.busaddr in slotinfo:
|
||||
self.is_m2 = True
|
||||
if not self.driver and 'imsm' not in self.mdcontainer and self.subsystype != 'nvm':
|
||||
raise Exception("No driver detected")
|
||||
if 'imsm' in self.mdcontainer:
|
||||
try:
|
||||
mdinfo = subprocess.check_output(
|
||||
['mdadm', '--detail', '-Y', devnode],
|
||||
stderr=subprocess.DEVNULL)
|
||||
if not isinstance(mdinfo, str):
|
||||
mdinfo = mdinfo.decode('utf8', errors='ignore')
|
||||
for line in mdinfo.splitlines():
|
||||
key, separator, value = line.partition('=')
|
||||
if separator and key.endswith('_DEV'):
|
||||
member = value.strip()
|
||||
member = member.replace('/dev/', '')
|
||||
if member and member not in self.vrocmembers:
|
||||
self.vrocmembers.append(member)
|
||||
except Exception:
|
||||
pass
|
||||
if self.driver == 'sr':
|
||||
raise Exception('cd/dvd')
|
||||
if os.path.exists('/sys/block/{0}/size'.format(self.name)):
|
||||
with open('/sys/block/{0}/size'.format(self.name), 'r') as sizesrc:
|
||||
self.size = int(sizesrc.read()) * 512
|
||||
if int(self.size) < 2147483648:
|
||||
raise Exception("Device too small for install ({}MiB)".format(int(self.size)/1024/1024))
|
||||
|
||||
@property
|
||||
def priority(self):
|
||||
if self.in_vroc:
|
||||
return 99 # prefer the assembled array over the member disks
|
||||
if self.is_m2:
|
||||
return 0
|
||||
if self.model.lower() in PRIORITY_MODELS:
|
||||
return 0
|
||||
if 'imsm' in self.mdcontainer:
|
||||
return 1
|
||||
if self.driver == 'ahci':
|
||||
return 2
|
||||
if self.driver.startswith('megaraid'):
|
||||
return 3
|
||||
if self.driver.startswith('mpt'):
|
||||
return 4
|
||||
return 99
|
||||
|
||||
def __repr__(self):
|
||||
return repr({
|
||||
'name': self.name,
|
||||
'path': self.path,
|
||||
'wwn': self.wwn,
|
||||
'driver': self.driver,
|
||||
'size': self.size,
|
||||
'model': self.model,
|
||||
})
|
||||
|
||||
|
||||
def get_dmi_type9_info():
|
||||
dmidecode = shutil.which('dmidecode')
|
||||
if not dmidecode:
|
||||
return {}
|
||||
try:
|
||||
dmio = subprocess.check_output(
|
||||
[dmidecode, '--type', '9'], stderr=subprocess.DEVNULL)
|
||||
except Exception:
|
||||
return {}
|
||||
if not isinstance(dmio, str):
|
||||
dmio = dmio.decode('utf8', errors='ignore')
|
||||
slots = {}
|
||||
currentslot = None
|
||||
for line in dmio.split('\n'):
|
||||
stripped = line.strip()
|
||||
if not stripped:
|
||||
if currentslot:
|
||||
busaddr = currentslot.get('bus_address', None)
|
||||
if busaddr:
|
||||
slots[busaddr] = currentslot
|
||||
currentslot = None
|
||||
continue
|
||||
if stripped.startswith('Handle ') and ', DMI type 9,' in stripped:
|
||||
if currentslot:
|
||||
busaddr = currentslot.get('bus_address', None)
|
||||
if busaddr:
|
||||
slots[busaddr] = currentslot
|
||||
currentslot = {'handle': stripped.split(',', 1)[0].split(' ', 1)[1]}
|
||||
continue
|
||||
if currentslot is None:
|
||||
continue
|
||||
if ':' not in stripped:
|
||||
continue
|
||||
key, val = stripped.split(':', 1)
|
||||
key = key.strip().lower().replace(' ', '_')
|
||||
currentslot[key] = val.strip()
|
||||
if currentslot:
|
||||
busaddr = currentslot.get('bus_address', None)
|
||||
if busaddr:
|
||||
slots[busaddr] = currentslot
|
||||
return slots
|
||||
|
||||
def get_m2_slot_info():
|
||||
slots = get_dmi_type9_info()
|
||||
m2slots = {}
|
||||
for slot in slots.values():
|
||||
label = '{0} {1}'.format(
|
||||
slot.get('type', ''), slot.get('designation', '')).lower()
|
||||
if not any(x in label for x in BOOT_SLOT_KEYWORDS):
|
||||
continue
|
||||
busaddr = slot.get('bus_address', None)
|
||||
if not busaddr:
|
||||
continue
|
||||
m2slots[busaddr] = slot
|
||||
return m2slots
|
||||
|
||||
|
||||
def get_deployment_storage():
|
||||
apiclient = None
|
||||
for clientpath in ('/opt/confluent/bin/apiclient', '/etc/confluent/apiclient'):
|
||||
if os.path.exists(clientpath):
|
||||
apiclient = clientpath
|
||||
break
|
||||
if not apiclient:
|
||||
return None
|
||||
for attriburl in ('/confluent-api/self/myattrib', '/confluent-api/self/myattribs'):
|
||||
try:
|
||||
attribs = subprocess.check_output(
|
||||
['python3', apiclient, attriburl, '-j'], stderr=subprocess.DEVNULL)
|
||||
if not isinstance(attribs, str):
|
||||
attribs = attribs.decode('utf8')
|
||||
attribs = json.loads(attribs)
|
||||
if not isinstance(attribs, dict):
|
||||
continue
|
||||
storage = attribs.get('deployment.storage', None)
|
||||
if isinstance(storage, str) and storage.strip():
|
||||
return storage.strip()
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def filter_deployment_storage(disks, storagespec, slotinfo=None):
|
||||
if not storagespec:
|
||||
return disks
|
||||
spec = storagespec.strip().lower()
|
||||
for disk in list(disks):
|
||||
if spec == 'm2':
|
||||
if disk.is_m2:
|
||||
continue
|
||||
if disk.model.lower() in PRIORITY_MODELS:
|
||||
continue
|
||||
if slotinfo is None:
|
||||
slotinfo = get_m2_slot_info()
|
||||
if not getattr(disk, 'busaddr', None):
|
||||
for kernname in getattr(disk, 'kernnames', []):
|
||||
if kernname in slotinfo:
|
||||
break
|
||||
else:
|
||||
disks.remove(disk)
|
||||
continue
|
||||
if disk.busaddr not in slotinfo:
|
||||
disks.remove(disk)
|
||||
elif spec.startswith('/dev/'):
|
||||
bspec = os.path.basename(spec)
|
||||
if disk.name.lower() == bspec:
|
||||
return [disk]
|
||||
else:
|
||||
raise Exception("Unknown deployment.storage specification: {0}".format(storagespec))
|
||||
|
||||
if not disks:
|
||||
raise Exception("No disks match deployment.storage specification: {0}".format(storagespec))
|
||||
return disks
|
||||
|
||||
def attach_member_disks(disks):
|
||||
for disk in disks:
|
||||
num_vroc_members = len(getattr(disk, 'vrocmembers', []))
|
||||
for vrocmember in getattr(disk, 'vrocmembers', []):
|
||||
for memberdisk in disks:
|
||||
if memberdisk.name == vrocmember:
|
||||
memberdisk.in_vroc = True
|
||||
if memberdisk.is_m2:
|
||||
disk.is_m2 = True
|
||||
else:
|
||||
if num_vroc_members == 2 and 'nvme' in vrocmember:
|
||||
# This is not assured, but is likely
|
||||
# Unfortunately, some VMD configurations blow past any way to detect form factor
|
||||
# as DMI table may not include
|
||||
disk.is_m2 = True
|
||||
|
||||
|
||||
def main():
|
||||
disks = []
|
||||
slotinfo = get_m2_slot_info()
|
||||
for disk in sorted(os.listdir('/sys/class/block')):
|
||||
try:
|
||||
disk = DiskInfo(disk, slotinfo=slotinfo)
|
||||
disks.append(disk)
|
||||
except SilentException:
|
||||
pass
|
||||
except Exception as e:
|
||||
print("Skipping {0}: {1}".format(disk, str(e)))
|
||||
attach_member_disks(disks)
|
||||
disks = filter_deployment_storage(disks, get_deployment_storage(), slotinfo=slotinfo)
|
||||
nd = [x.name for x in sorted(disks, key=lambda x: [x.priority, x.size])]
|
||||
if nd:
|
||||
open('/tmp/installdisk', 'w').write(nd[0])
|
||||
else:
|
||||
raise Exception("No suitable install disk found")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -2,8 +2,6 @@
|
||||
[ -f /etc/confluent/functions ] && . /etc/confluent/functions
|
||||
[ -f /opt/confluent/bin/apiclient ] && confapiclient=/opt/confluent/bin/apiclient
|
||||
[ -f /etc/confluent/apiclient ] && confapiclient=/etc/confluent/apiclient
|
||||
unset http_proxy
|
||||
unset https_proxy
|
||||
for pubkey in /etc/ssh/ssh_host*key.pub; do
|
||||
if [ "$pubkey" = /etc/ssh/ssh_host_key.pub ]; then
|
||||
continue
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Resealing to allow more practical use of, for example, PCR1, which frequently gets changed booting from install environment
|
||||
# to local disk
|
||||
|
||||
get_requested_pcrs() {
|
||||
local encryptboot=$(grep ^encryptboot: /etc/confluent/confluent.deploycfg | awk '{print $2}')
|
||||
IFS=':' read -ra params <<< "$encryptboot"
|
||||
for param in "${params[@]}"; do
|
||||
if [[ "$param" =~ ^pcrs= ]]; then
|
||||
echo "${param#pcrs=}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
wipe_slots() {
|
||||
local device="$1"
|
||||
|
||||
if [[ -z "$device" ]]; then
|
||||
echo "Error: No device specified" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if device exists
|
||||
if [[ ! -e "$device" ]]; then
|
||||
echo "Error: Device $device not found" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Wipe existing TPM2 slots using clevis
|
||||
echo "Wiping TPM2 slots for $device"
|
||||
if [ -e /bin/systemd-cryptenroll ]; then
|
||||
systemd-cryptenroll --wipe-slot=tpm2 "$device"
|
||||
else
|
||||
clevis luks unbind -d "$device" tpm2 || true
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
seal_tpm() {
|
||||
local device="$1"
|
||||
if [[ -z "$device" ]]; then
|
||||
echo "Error: No device specified" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -e "$device" ]]; then
|
||||
echo "Error: Device $device not found" >&2
|
||||
return 1
|
||||
fi
|
||||
local pcrs=$(get_requested_pcrs)
|
||||
if [ -e /bin/systemd-cryptenroll ]; then
|
||||
PASSWORD=$(cat /etc/confluent/luks.key) systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs="$pcrs" "$device"
|
||||
else
|
||||
if [ -n "$pcrs" ]; then
|
||||
clevispcrs=$(echo "$pcrs" | sed -e 's/,/, /g')
|
||||
clevis luks bind -f -d "$device" -k - tpm2 "{\"pcr_ids\":\"$clevispcrs\"}" < /etc/confluent/luks.key
|
||||
else
|
||||
clevis luks bind -f -d "$device" -k - tpm2 "{}" < /etc/confluent/luks.key
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $? -eq 0 ]]; then
|
||||
echo "Successfully enrolled TPM2 for $device"
|
||||
return 0
|
||||
else
|
||||
echo "Failed to enroll TPM2 for $device" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
reseal_luks() {
|
||||
local device="$1"
|
||||
|
||||
if [[ -z "$device" ]]; then
|
||||
echo "Error: No device specified" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if device exists
|
||||
if [[ ! -e "$device" ]]; then
|
||||
echo "Error: Device $device not found" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Wipe existing TPM2 slots
|
||||
wipe_slots "$device"
|
||||
|
||||
# Enroll TPM2 key
|
||||
|
||||
seal_tpm "$device"
|
||||
|
||||
}
|
||||
|
||||
while IFS= read -r line; do
|
||||
# Skip comments and empty lines
|
||||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
[[ -z "$line" ]] && continue
|
||||
|
||||
|
||||
device=$(echo "$line" | awk '{print $2}')
|
||||
|
||||
# If it's a UUID, convert to device name
|
||||
if [[ "$device" =~ ^UUID= ]]; then
|
||||
device=$(blkid -U "${device#UUID=}")
|
||||
fi
|
||||
|
||||
if [ -e /bin/systemd-cryptenroll ]; then
|
||||
systemd-cryptenroll "$device" | grep -q "tpm2" || continue
|
||||
else
|
||||
clevis luks list -d "$device" | grep -q "tpm2" || continue
|
||||
fi
|
||||
|
||||
# Process the device
|
||||
if [[ -n "$device" ]]; then
|
||||
reseal_luks "$device"
|
||||
else
|
||||
echo "Warning: Could not determine device for line: $line" >&2
|
||||
fi
|
||||
done < /etc/crypttab
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ Version: #VERSION#
|
||||
Release: 1
|
||||
Summary: OS Deployment support for confluent
|
||||
|
||||
License: Apache-2.0
|
||||
License: Apache2
|
||||
URL: https://hpc.lenovo.com/
|
||||
Source0: confluent_osdeploy.tar.xz
|
||||
Source1: confluent_el9bin.tar.xz
|
||||
Source2: confluent_el8bin.tar.xz
|
||||
BuildArch: noarch
|
||||
Requires: confluent_ipxe-aarch64 mtools tar
|
||||
Requires: confluent_ipxe mtools tar
|
||||
BuildRoot: /tmp
|
||||
|
||||
%description
|
||||
@@ -31,7 +31,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 suse16 debian debian13 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.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
|
||||
@@ -41,13 +41,13 @@ for os in rhvh4 el7 genesis el8 suse15 suse16 debian debian13 ubuntu20.04 ubuntu
|
||||
else
|
||||
cp -a ../opt .
|
||||
fi
|
||||
cp -a ../common/initramfs/* .
|
||||
cp -a ../${os}/initramfs/* .
|
||||
cp -a ../common/initramfs/* .
|
||||
find . | cpio -H newc -o > ../addons.cpio
|
||||
mv ../addons.cpio .
|
||||
cd ..
|
||||
done
|
||||
for os in el7 el8 suse15 suse16 el9 el10 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.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
|
||||
@@ -57,8 +57,8 @@ for os in el7 el8 suse15 suse16 el9 el10 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubu
|
||||
else
|
||||
cp -a ../opt .
|
||||
fi
|
||||
cp -a ../common/initramfs/* .
|
||||
cp -a ../${os}-diskless/initramfs/* .
|
||||
cp -a ../common/initramfs/* .
|
||||
if [ -d ../${os}bin ]; then
|
||||
cp -a ../${os}bin/stateless-bin/* opt/confluent/bin
|
||||
else
|
||||
@@ -71,8 +71,8 @@ done
|
||||
mkdir esxi7out
|
||||
cd esxi7out
|
||||
cp -a ../opt .
|
||||
cp -a ../common/initramfs/* .
|
||||
cp -a ../esxi7/initramfs/* .
|
||||
cp -a ../common/initramfs/* .
|
||||
chmod +x bin/* opt/confluent/bin/*
|
||||
tar zcvf ../addons.tgz *
|
||||
mv ../addons.tgz .
|
||||
@@ -88,7 +88,7 @@ mkdir -p %{buildroot}/opt/confluent/share/licenses/confluent_osdeploy/
|
||||
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 suse16 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04 esxi6 esxi7 esxi8 coreos; do
|
||||
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
|
||||
|
||||
@@ -3,7 +3,7 @@ Version: #VERSION#
|
||||
Release: 1
|
||||
Summary: OS Deployment support for confluent
|
||||
|
||||
License: Apache-2.0
|
||||
License: Apache2
|
||||
URL: https://hpc.lenovo.com/
|
||||
Source0: confluent_osdeploy.tar.xz
|
||||
Source1: confluent_el9bin.tar.xz
|
||||
@@ -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 suse16 debian debian13 ubuntu18.04 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.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
|
||||
@@ -43,13 +43,13 @@ for os in rhvh4 el7 genesis el8 suse15 suse16 debian debian13 ubuntu18.04 ubuntu
|
||||
else
|
||||
cp -a ../el8bin/opt .
|
||||
fi
|
||||
cp -a ../common/initramfs/* .
|
||||
cp -a ../${os}/initramfs/* .
|
||||
cp -a ../common/initramfs/* .
|
||||
find . | cpio -H newc -o > ../addons.cpio
|
||||
mv ../addons.cpio .
|
||||
cd ..
|
||||
done
|
||||
for os in el7 el8 suse15 suse16 el9 el10 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.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
|
||||
@@ -59,8 +59,8 @@ for os in el7 el8 suse15 suse16 el9 el10 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubu
|
||||
else
|
||||
cp -a ../el8bin/opt .
|
||||
fi
|
||||
cp -a ../common/initramfs/* .
|
||||
cp -a ../${os}-diskless/initramfs/* .
|
||||
cp -a ../common/initramfs/* .
|
||||
if [ -d ../${os}bin ]; then
|
||||
cp -a ../${os}bin/stateless-bin/* opt/confluent/bin
|
||||
else
|
||||
@@ -73,8 +73,8 @@ done
|
||||
mkdir esxi7out
|
||||
cd esxi7out
|
||||
cp -a ../el8bin/opt .
|
||||
cp -a ../common/initramfs/* .
|
||||
cp -a ../esxi7/initramfs/* .
|
||||
cp -a ../common/initramfs/* .
|
||||
chmod +x bin/* opt/confluent/bin/*
|
||||
tar zcvf ../addons.tgz *
|
||||
mv ../addons.tgz .
|
||||
@@ -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 suse16 ubuntu20.04 debian debian13 ubuntu18.04 ubuntu22.04 ubuntu24.04 ubuntu26.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
|
||||
@@ -97,7 +97,6 @@ for os in rhvh4 el7 el8 el9 el10 genesis suse15 suse16 ubuntu20.04 debian debian
|
||||
for targ in %{buildroot}/opt/confluent/lib/osdeploy/$os/profiles/*; do
|
||||
cp -a common/profile/* $targ
|
||||
done
|
||||
cp -a $os/profiles/* %{buildroot}/opt/confluent/lib/osdeploy/$os/profiles
|
||||
if [ -d ${os}disklessout ]; then
|
||||
mkdir -p %{buildroot}/opt/confluent/lib/osdeploy/${os}-diskless/initramfs
|
||||
mkdir -p %{buildroot}/opt/confluent/lib/osdeploy/${os}-diskless/profiles
|
||||
@@ -106,7 +105,6 @@ for os in rhvh4 el7 el8 el9 el10 genesis suse15 suse16 ubuntu20.04 debian debian
|
||||
for targ in %{buildroot}/opt/confluent/lib/osdeploy/$os-diskless/profiles/*; do
|
||||
cp -a common/profile/* $targ
|
||||
done
|
||||
cp -a ${os}-diskless/profiles/* %{buildroot}/opt/confluent/lib/osdeploy/$os-diskless/profiles
|
||||
fi
|
||||
done
|
||||
find %{buildroot}/opt/confluent/lib/osdeploy/ -name .gitignore -exec rm -f {} +
|
||||
|
||||
@@ -111,14 +111,6 @@ def get_interface_name(iname, settings):
|
||||
return iname
|
||||
return None
|
||||
|
||||
def parse_extra_settings(stgs):
|
||||
extras = {}
|
||||
for kv in stgs.get('extra_settings', '').split(';'):
|
||||
k, _, v = kv.partition('=')
|
||||
if k.strip():
|
||||
extras[k.strip()] = v.strip()
|
||||
return extras
|
||||
|
||||
class NetplanManager(object):
|
||||
def __init__(self, deploycfg):
|
||||
self.cfgbydev = {}
|
||||
@@ -151,7 +143,7 @@ class NetplanManager(object):
|
||||
nicinfo[devname]['routes'] = routeinfo
|
||||
self.cfgbydev[devname] = nicinfo[devname]
|
||||
|
||||
def apply_configuration(self, cfg, lastchance=False):
|
||||
def apply_configuration(self, cfg):
|
||||
devnames = cfg['interfaces']
|
||||
if len(devnames) != 1:
|
||||
raise Exception('Multi-nic team/bonds not yet supported')
|
||||
@@ -200,23 +192,6 @@ class NetplanManager(object):
|
||||
if dnsdomain not in currdnsdomain:
|
||||
needcfgwrite = True
|
||||
currdnsdomain.append(dnsdomain)
|
||||
extras = parse_extra_settings(stgs)
|
||||
if extras:
|
||||
devdict = self.cfgbydev.setdefault(devname, {})
|
||||
for key in extras:
|
||||
try:
|
||||
val = yaml.safe_load(extras[key])
|
||||
except yaml.YAMLError:
|
||||
val = extras[key]
|
||||
keyptr = devdict
|
||||
keypath = key.split('.')
|
||||
for k in keypath[:-1]:
|
||||
if not isinstance(keyptr.get(k, None), dict):
|
||||
keyptr[k] = {}
|
||||
keyptr = keyptr[k]
|
||||
if keyptr.get(keypath[-1], None) != val:
|
||||
needcfgwrite = True
|
||||
keyptr[keypath[-1]] = val
|
||||
if needcfgwrite:
|
||||
needcfgapply = True
|
||||
newcfg = {'network': {'version': 2, 'ethernets': {devname: self.cfgbydev[devname]}}}
|
||||
@@ -266,7 +241,7 @@ class WickedManager(object):
|
||||
v = v.strip()
|
||||
currcfg[k] = v
|
||||
|
||||
def apply_configuration(self, cfg, lastchance=False):
|
||||
def apply_configuration(self, cfg):
|
||||
stgs = cfg['settings']
|
||||
ipcfg = 'STARTMODE=auto\n'
|
||||
routecfg = ''
|
||||
@@ -286,9 +261,6 @@ class WickedManager(object):
|
||||
if stgs.get('ipv6_address', None):
|
||||
ipcfg += 'IPADDR_V6=' + stgs['ipv6_address'] + '\n'
|
||||
v6gw = stgs.get('ipv6_gateway', None)
|
||||
extras = parse_extra_settings(stgs)
|
||||
for key in extras:
|
||||
ipcfg += '{0}={1}\n'.format(key, shlex.quote(extras[key]))
|
||||
cname = None
|
||||
if len(cfg['interfaces']) > 1: # creating new team
|
||||
if not stgs.get('team_mode', None):
|
||||
@@ -428,7 +400,6 @@ class NetworkManager(object):
|
||||
cmdargs['ipv4.dns'] = ','.join(dns4)
|
||||
if dns6:
|
||||
cmdargs['ipv6.dns'] = ','.join(dns6)
|
||||
cmdargs.update(parse_extra_settings(stgs))
|
||||
if len(cfg['interfaces']) > 1: # team time.. should be..
|
||||
if not cfg['settings'].get('team_mode', None):
|
||||
sys.stderr.write("Warning, multiple interfaces ({0}) without a team_mode, skipping setup\n".format(','.join(cfg['interfaces'])))
|
||||
@@ -545,18 +516,18 @@ if __name__ == '__main__':
|
||||
time.sleep(1)
|
||||
continue
|
||||
dc = json.loads(dc)
|
||||
inames = get_interface_name(idxmap[curridx], nc.get('default', {}))
|
||||
if inames:
|
||||
for iname in inames.split(','):
|
||||
iname = get_interface_name(idxmap[curridx], nc.get('default', {}))
|
||||
if iname:
|
||||
for iname in iname.split(','):
|
||||
if 'default' in netname_to_interfaces:
|
||||
netname_to_interfaces['default']['interfaces'].add(iname)
|
||||
else:
|
||||
netname_to_interfaces['default'] = {'interfaces': set([iname]), 'settings': nc['default']}
|
||||
for netname in nc.get('extranets', {}):
|
||||
uname = '_' + netname
|
||||
inames = get_interface_name(idxmap[curridx], nc['extranets'][netname])
|
||||
if inames:
|
||||
for iname in inames.split(','):
|
||||
iname = get_interface_name(idxmap[curridx], nc['extranets'][netname])
|
||||
if iname:
|
||||
for iname in iname.split(','):
|
||||
if uname in netname_to_interfaces:
|
||||
netname_to_interfaces[uname]['interfaces'].add(iname)
|
||||
else:
|
||||
|
||||
+9
-115
@@ -4,7 +4,7 @@ function test_mgr() {
|
||||
if [[ "$whost" == *:* ]] && [[ "$whost" != *[* ]] ; then
|
||||
whost="[$whost]"
|
||||
fi
|
||||
if curl -gs -x "" https://${whost}/confluent-api/ > /dev/null; then
|
||||
if curl -gs https://${whost}/confluent-api/ > /dev/null; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
@@ -64,9 +64,9 @@ function set_confluent_vars() {
|
||||
}
|
||||
|
||||
fetch_remote() {
|
||||
curlargs=(-x "")
|
||||
curlargs=""
|
||||
if [ -f /etc/confluent/ca.pem ]; then
|
||||
curlargs=(-x "" --cacert /etc/confluent/ca.pem)
|
||||
curlargs=" --cacert /etc/confluent/ca.pem"
|
||||
fi
|
||||
set_confluent_vars
|
||||
mkdir -p $(dirname $1)
|
||||
@@ -74,7 +74,7 @@ fetch_remote() {
|
||||
if [[ "$whost" == *:* ]] && [[ "$whost" != *[* ]] ; then
|
||||
whost="[$whost]"
|
||||
fi
|
||||
curl -gf -sS "${curlargs[@]}" https://$whost/confluent-public/os/$confluent_profile/scripts/$1 > $1
|
||||
curl -gf -sS $curlargs https://$whost/confluent-public/os/$confluent_profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo $1 failed to download; return 1; fi
|
||||
}
|
||||
|
||||
@@ -108,12 +108,6 @@ run_remote_parts() {
|
||||
unset confluentscripttmpdir
|
||||
}
|
||||
|
||||
set_selinux_context() {
|
||||
if [ -x /usr/bin/chcon ]; then
|
||||
/usr/bin/chcon "$1" "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
source_remote() {
|
||||
set_confluent_vars
|
||||
unsettmpdir=0
|
||||
@@ -156,7 +150,9 @@ run_remote() {
|
||||
if [ $? != 0 ]; then echo $requestedcmd failed to download; return 1; fi
|
||||
chmod +x $1
|
||||
cmd=$1
|
||||
set_selinux_context system_u:object_r:bin_t:s0 "$cmd"
|
||||
if [ -x /usr/bin/chcon ]; then
|
||||
chcon system_u:object_r:bin_t:s0 $cmd
|
||||
fi
|
||||
shift
|
||||
./$cmd $*
|
||||
retcode=$?
|
||||
@@ -175,9 +171,8 @@ run_remote() {
|
||||
run_remote_python() {
|
||||
echo
|
||||
set_confluent_vars
|
||||
curlargs=(-x "")
|
||||
if [ -f /etc/confluent/ca.pem ]; then
|
||||
curlargs=(-x "" --cacert /etc/confluent/ca.pem)
|
||||
curlargs=" --cacert /etc/confluent/ca.pem"
|
||||
fi
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Running python script "'$*'" from https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/
|
||||
@@ -190,7 +185,7 @@ run_remote_python() {
|
||||
if [[ "$whost" == *:* ]] && [[ "$whost" != *[* ]] ; then
|
||||
whost="[$whost]"
|
||||
fi
|
||||
curl -gf -sS "${curlargs[@]}" https://$whost/confluent-public/os/$confluent_profile/scripts/$1 > $1
|
||||
curl -gf -sS $curlargs https://$whost/confluent-public/os/$confluent_profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo "'$*'" failed to download; return 1; fi
|
||||
confluentpython $*
|
||||
retcode=$?
|
||||
@@ -217,106 +212,5 @@ run_remote_config() {
|
||||
echo '---------------------------------------------------------------------------'
|
||||
return
|
||||
}
|
||||
startlegacyprogress() {
|
||||
[ -n "$progresspid" ] && return
|
||||
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 &
|
||||
progresspid=$!
|
||||
}
|
||||
|
||||
extractmultisquash() {
|
||||
multisquashrc=1
|
||||
while read -r _ partsrc partmount; do
|
||||
normalizedmount=${partmount%/}
|
||||
[ "$normalizedmount" = /mnt/remote ] || continue
|
||||
if unsquashfs -force -d /sysroot "$partsrc"; then
|
||||
multisquashrc=0
|
||||
fi
|
||||
break
|
||||
done < /tmp/mountparts.sh
|
||||
[ $multisquashrc -eq 0 ] || return 1
|
||||
while read -r _ partsrc partmount; do
|
||||
normalizedmount=${partmount%/}
|
||||
[ "$normalizedmount" = /mnt/remote ] && continue
|
||||
partdest=/sysroot${normalizedmount#/mnt/remote}
|
||||
mkdir -p "$partdest"
|
||||
if ! unsquashfs -force -d "$partdest" "$partsrc"; then
|
||||
return 1
|
||||
fi
|
||||
done < /tmp/mountparts.sh
|
||||
}
|
||||
|
||||
cleanupremotemounts() {
|
||||
if [ -f /tmp/mountparts.sh ]; then
|
||||
for partmount in $(awk '{ mounts[NR] = $3 } END { for (idx = NR; idx > 0; idx--) print mounts[idx] }' /tmp/mountparts.sh); do
|
||||
umount "$partmount"
|
||||
done
|
||||
for partdev in $(awk '{ devices[NR] = $NF } END { for (idx = NR; idx > 0; idx--) print devices[idx] }' /tmp/setupmount.sh); do
|
||||
dmsetup remove "$partdev"
|
||||
done
|
||||
else
|
||||
umount /mnt/remote
|
||||
fi
|
||||
}
|
||||
|
||||
# Extract an untethered/uncompressed diskless root image into /sysroot and
|
||||
# tear down the source devices; expects rootimgformat, mountsrc, and loopdev
|
||||
# from imageboot.sh and the image content mounted under /mnt/remote.
|
||||
extract_untethered_rootimg() {
|
||||
extractrc=1
|
||||
progresspid=
|
||||
if [ "$rootimgformat" = squashfs ] && command -v unsquashfs > /dev/null 2>&1; then
|
||||
echo "Decrypting and extracting root filesystem in parallel"
|
||||
if unsquashfs -force -d /sysroot "$mountsrc"; then
|
||||
extractrc=0
|
||||
else
|
||||
echo "Parallel root filesystem extraction failed, retrying with cp"
|
||||
fi
|
||||
elif [ "$rootimgformat" = confluent_multisquash ] && command -v unsquashfs > /dev/null 2>&1; then
|
||||
echo "Decrypting and extracting multipart root filesystem in parallel"
|
||||
if extractmultisquash; then
|
||||
extractrc=0
|
||||
else
|
||||
echo "Parallel multipart root filesystem extraction failed, retrying with cp"
|
||||
fi
|
||||
fi
|
||||
if [ $extractrc -ne 0 ]; then
|
||||
rm -rf /sysroot/* /sysroot/.[!.]* /sysroot/..?*
|
||||
startlegacyprogress
|
||||
if cp -a /mnt/remote/. /sysroot/; then
|
||||
extractrc=0
|
||||
fi
|
||||
fi
|
||||
if [ $extractrc -ne 0 ]; then
|
||||
if [ -n "$progresspid" ]; then
|
||||
kill "$progresspid" 2> /dev/null || true
|
||||
wait "$progresspid" 2> /dev/null || true
|
||||
progresspid=
|
||||
fi
|
||||
echo "Failed to extract the root filesystem"
|
||||
fi
|
||||
cleanupremotemounts
|
||||
if [ -e /dev/mapper/cryptimg ]; then
|
||||
dmsetup remove cryptimg
|
||||
fi
|
||||
losetup -d $loopdev
|
||||
rm /mnt/remoteimg/rootimg.sfs
|
||||
umount /mnt/remoteimg
|
||||
if [ -n "$progresspid" ]; then
|
||||
wait "$progresspid"
|
||||
echo -e "Decrypting and extracting root filesystem: 100%"
|
||||
fi
|
||||
return $extractrc
|
||||
}
|
||||
|
||||
#If invoked as a command, use the arguments to actually run a function
|
||||
(return 0 2>/dev/null) || $1 "${@:2}"
|
||||
@@ -9,7 +9,7 @@ mgr=$(cat /etc/confluent/deployer)
|
||||
cryptboot=$(grep encryptboot: $deploycfg|sed -e 's/^encryptboot: //')
|
||||
if [ "$cryptboot" != "" ] && [ "$cryptboot" != "none" ] && [ "$cryptboot" != "null" ]; then
|
||||
echo "****Encrypted boot requested, but not implemented for this OS, halting install" > /dev/console
|
||||
[ -f '/tmp/autoconsdev' ] && (echo "****Encrypted boot requested, but not implemented for this OS, halting install" >> $(cat /tmp/autoconsdev))
|
||||
[ -f '/tmp/autoconsdev' ] && (echo "****Encryptod boot requested, but not implemented for this OS,halting install" >> $(cat /tmp/autoconsdev))
|
||||
while :; do sleep 86400; done
|
||||
fi
|
||||
cat > /usr/lib/live-installer.d/confluent-certs << EOF
|
||||
@@ -34,8 +34,8 @@ done
|
||||
if [ -e /tmp/installdisk ]; then
|
||||
instdisk=$(cat /tmp/installdisk)
|
||||
else
|
||||
for blockdev in /sys/class/block/*; do
|
||||
shortname=$(basename "$blockdev")
|
||||
for blockdev in $(ls /sys/class/block/); do
|
||||
shortname=$(basename $blockdev)
|
||||
if [ "$shortname" != "${shortname%loop*}" ]; then
|
||||
continue
|
||||
fi
|
||||
@@ -62,7 +62,7 @@ else
|
||||
done
|
||||
fi
|
||||
if [ -z "$instdisk" ]; then
|
||||
if [ ! -z "$sraid" ]; then
|
||||
if [ ! -z "$sraid"]; then
|
||||
instdisk=$sraid
|
||||
elif [ ! -z "$onbdisk" ]; then
|
||||
instdisk=$onbdisk
|
||||
|
||||
+7
-41
@@ -1,31 +1,3 @@
|
||||
|
||||
pcrextendvalue=2fbe96c50dde38ce9cd2764ddb79c216cfbcd3499568b1125450e60c45dd19f2
|
||||
pcrhashalgo=sha256
|
||||
|
||||
set_tpm_hashalgo() {
|
||||
if [ -n "$confluent_tpm_hashalgo" ]; then
|
||||
return 0
|
||||
fi
|
||||
tpm_pcrbanks=$(tpm2_getcap pcrs 2>/dev/null)
|
||||
for algo in sha256 sha512 sha384; do
|
||||
# Match only banks that actually have PCRs allocated (a digit in [ ... ]).
|
||||
if echo "$tpm_pcrbanks" | grep -Eq "$algo:[[:space:]]*\[[^]]*[0-9]"; then
|
||||
confluent_tpm_hashalgo="$algo"
|
||||
pcrhashalgo="$algo"
|
||||
if [[ "$algo" == "sha256" ]]; then
|
||||
pcrextendvalue=2fbe96c50dde38ce9cd2764ddb79c216cfbcd3499568b1125450e60c45dd19f2
|
||||
elif [[ "$algo" == "sha384" ]]; then
|
||||
pcrextendvalue=20aaa1073c215c8bc97ff8dc509dd63ff09eef9d2dfa4d8ef224ce372d80e5417e53840ef2fb72924c195f69396a27b9
|
||||
elif [[ "$algo" == "sha512" ]]; then
|
||||
pcrextendvalue=77113aa32ac249789c0cdcf24e78efdb81d5be0d878e9a9a750446ecf9b9b3b1c084194eb6187cfd890b8f61a7f2e79eb4d33f6f27827b862367897c8123bceb
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
get_remote_apikey() {
|
||||
while [ -z "$confluent_apikey" ]; do
|
||||
/opt/confluent/bin/clortho $nodename $confluent_mgr > /etc/confluent/confluent.apikey
|
||||
@@ -42,10 +14,9 @@ get_remote_apikey() {
|
||||
tmpdir=$(mktemp -d)
|
||||
cd $tmpdir
|
||||
tpm2_startauthsession --session=session.ctx
|
||||
set_tpm_hashalgo
|
||||
tpm2_policypcr -Q --session=session.ctx --pcr-list="${pcrhashalgo}:15" --policy=pcr15.${pcrhashalgo}.policy
|
||||
tpm2_policypcr -Q --session=session.ctx --pcr-list="sha256:15" --policy=pcr15.sha256.policy
|
||||
tpm2_createprimary -G ecc -Q --key-context=prim.ctx
|
||||
(echo -n "CONFLUENT_APIKEY:";cat /etc/confluent/confluent.apikey) | tpm2_create -Q --policy=pcr15.${pcrhashalgo}.policy --public=data.pub --private=data.priv -i - -C prim.ctx
|
||||
(echo -n "CONFLUENT_APIKEY:";cat /etc/confluent/confluent.apikey) | tpm2_create -Q --policy=pcr15.sha256.policy --public=data.pub --private=data.priv -i - -C prim.ctx
|
||||
tpm2_load -Q --parent-context=prim.ctx --public=data.pub --private=data.priv --name=confluent.apikey --key-context=data.ctx
|
||||
tpm2_evictcontrol -Q -c data.ctx
|
||||
tpm2_flushcontext session.ctx
|
||||
@@ -54,7 +25,6 @@ get_remote_apikey() {
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
root=1
|
||||
rootok=1
|
||||
netroot=confluent
|
||||
@@ -72,9 +42,7 @@ if ! grep console= /proc/cmdline >& /dev/null; then
|
||||
autocons=$(/opt/confluent/bin/autocons)
|
||||
autoconsdev=${autocons%,*}
|
||||
autocons=${autocons##*/}
|
||||
if [ ! -z "$autocons" ]; then
|
||||
echo "Automatic console configured for $autocons"
|
||||
fi
|
||||
echo "Automatic console configured for $autocons"
|
||||
fi
|
||||
echo "Initializing confluent diskless environment"
|
||||
echo -n "udevd: "
|
||||
@@ -110,8 +78,7 @@ lasthdl=""
|
||||
if [ -c /dev/tpmrm0 ]; then
|
||||
for hdl in $(tpm2_getcap handles-persistent|awk '{print $2}'); do
|
||||
tpm2_startauthsession --policy-session --session=session.ctx
|
||||
set_tpm_hashalgo
|
||||
tpm2_policypcr -Q --session=session.ctx --pcr-list="${pcrhashalgo}:15" --policy=pcr15.${pcrhashalgo}.policy
|
||||
tpm2_policypcr -Q --session=session.ctx --pcr-list="sha256:15" --policy=pcr15.sha256.policy
|
||||
unsealeddata=$(tpm2_unseal --auth=session:session.ctx -Q -c $hdl 2>/dev/null)
|
||||
tpm2_flushcontext session.ctx
|
||||
if [[ $unsealeddata == "CONFLUENT_APIKEY:"* ]]; then
|
||||
@@ -176,8 +143,8 @@ while [ $ready = "0" ]; do
|
||||
elif grep 'SSL' $tmperr > /dev/null; then
|
||||
confluent_mgr=${confluent_mgr#[}
|
||||
confluent_mgr=${confluent_mgr%]}
|
||||
echo 'Failure establishing TLS connection to '$confluent_mgr' (try `osdeploy initialize -t` on the deployment server)'
|
||||
if [ ! -z "$autoconsdev" ]; then echo 'Failure establishing TLS connection to '$confluent_mgr' (try `osdeploy initialize -t` on the deployment server)' > $autoconsdev; fi
|
||||
echo 'Failure establishing TLS conneection to '$confluent_mgr' (try `osdeploy initialize -t` on the deployment server)'
|
||||
if [ ! -z "$autoconsdev" ]; then echo 'Failure establishing TLS conneection to '$confluent_mgr' (try `osdeploy initialize -t` on the deployment server)' > $autoconsdev; fi
|
||||
sleep 10
|
||||
else
|
||||
ready=1
|
||||
@@ -186,8 +153,7 @@ while [ $ready = "0" ]; do
|
||||
done
|
||||
if [ ! -z "$autocons" ] && grep "textconsole: true" /etc/confluent/confluent.deploycfg > /dev/null; then /opt/confluent/bin/autocons -c > /dev/null; fi
|
||||
if [ -c /dev/tpmrm0 ]; then
|
||||
set_tpm_hashalgo
|
||||
tpm2_pcrextend 15:${pcrhashalgo}=${pcrextendvalue}
|
||||
tpm2_pcrextend 15:sha256=2fbe96c50dde38ce9cd2764ddb79c216cfbcd3499568b1125450e60c45dd19f2
|
||||
fi
|
||||
umask $oldumask
|
||||
mkdir -p /run/NetworkManager/system-connections
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
../../../../el9-diskless/profiles/default/scripts/functions
|
||||
@@ -0,0 +1 @@
|
||||
../../../../el9-diskless/profiles/default/scripts/getinstalldisk
|
||||
@@ -4,7 +4,6 @@ if [[ "$confluent_whost" == *:* ]] && [[ "$confluent_whost" != "["* ]]; then
|
||||
confluent_whost="[$confluent_mgr]"
|
||||
fi
|
||||
mkdir -p /mnt/remoteimg /mnt/remote /mnt/overlay /sysroot
|
||||
set_selinux_context system_u:object_r:bin_t:s0 /opt/confluent/bin/urlmount
|
||||
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
|
||||
@@ -30,10 +29,9 @@ if grep '^Format: confluent_crypted' /tmp/rootimg.info > /dev/null; then
|
||||
mountsrc=/dev/mapper/cryptimg
|
||||
fi
|
||||
|
||||
rootimgformat=$(awk -F': ' '/^Format:/ {print $2; exit}' /tmp/rootimg.info)
|
||||
if [ "$rootimgformat" = squashfs ]; then
|
||||
if grep '^Format: squashfs' /tmp/rootimg.info > /dev/null; then
|
||||
mount -o ro $mountsrc /mnt/remote
|
||||
elif [ "$rootimgformat" = confluent_multisquash ]; then
|
||||
elif grep '^Format: confluent_multisquash' /tmp/rootimg.info; then
|
||||
tail -n +3 /tmp/rootimg.info | awk '{gsub("/", "_"); print "echo 0 " $4 " linear '$mountsrc' " $3 " | dmsetup create mproot" $7}' > /tmp/setupmount.sh
|
||||
. /tmp/setupmount.sh
|
||||
cat /tmp/setupmount.sh |awk '{printf "mount /dev/mapper/"$NF" "; sub("mproot", ""); gsub("_", "/"); print "/mnt/remote"$NF}' > /tmp/mountparts.sh
|
||||
@@ -56,7 +54,27 @@ if [ "untethered" = "$(getarg confluent_imagemethod)" -o "uncompressed" = "$(get
|
||||
else
|
||||
mount -t tmpfs disklessroot /sysroot
|
||||
fi
|
||||
extract_untethered_rootimg
|
||||
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
|
||||
TETHERED=1
|
||||
mount -o discard /dev/zram0 /mnt/overlay
|
||||
@@ -132,9 +150,6 @@ mkdir -p /sysroot/opt/confluent/bin
|
||||
curl -sf https://$confluent_whost/confluent-public/os/$confluent_profile/scripts/onboot.sh > /sysroot/opt/confluent/bin/onboot.sh
|
||||
chmod +x /sysroot/opt/confluent/bin/onboot.sh
|
||||
cp /opt/confluent/bin/apiclient /sysroot/opt/confluent/bin
|
||||
set_selinux_context system_u:object_r:systemd_unit_file_t:s0 /sysroot/etc/systemd/system/onboot.service
|
||||
set_selinux_context system_u:object_r:bin_t:s0 /sysroot/opt/confluent/bin/onboot.sh
|
||||
set_selinux_context system_u:object_r:bin_t:s0 /sysroot/opt/confluent/bin/apiclient
|
||||
ln -s /etc/systemd/system/onboot.service /sysroot/etc/systemd/system/multi-user.target.wants/onboot.service
|
||||
cp /etc/confluent/functions /sysroot/etc/confluent/functions
|
||||
if grep installtodisk /proc/cmdline > /dev/null; then
|
||||
@@ -144,9 +159,7 @@ if grep installtodisk /proc/cmdline > /dev/null; then
|
||||
fi
|
||||
mv /lib/modules/$(uname -r) /lib/modules/$(uname -r)-ramfs
|
||||
ln -s /sysroot/lib/modules/$(uname -r) /lib/modules/
|
||||
if [ -d /lib/firmware ]; then
|
||||
mv /lib/firmware /lib/firmware-ramfs
|
||||
fi
|
||||
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
|
||||
@@ -165,9 +178,9 @@ if [ $TETHERED -eq 1 ]; then
|
||||
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
|
||||
set_selinux_context system_u:object_r:root_t:s0 /sysroot
|
||||
if grep debugssh /proc/cmdline >& /dev/null; then
|
||||
exec /opt/confluent/bin/start_root
|
||||
else
|
||||
exec /opt/confluent/bin/start_root -s # share mount namespace, keep kernel callbacks intact
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
../../../../el9-diskless/profiles/default/scripts/syncfileclient
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user