mirror of
https://github.com/xcat2/confluent.git
synced 2026-09-29 00:31:09 +00:00
Compare commits
203 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d64b3e6a4e | |||
| 637e0f0a81 | |||
| e26218eb92 | |||
| 03eb026b61 | |||
| beab6a3c02 | |||
| 5e0ebce300 | |||
| 85c4ec5654 | |||
| 7957a6abd4 | |||
| 39a7d8d131 | |||
| 54c5bf128e | |||
| ca0c592044 | |||
| 829d1316b2 | |||
| c2c1c85651 | |||
| c5833f1417 | |||
| 07ae3593c3 | |||
| 8ab35b11cd | |||
| 35ef6170ba | |||
| d650f11255 | |||
| b4926b39fe | |||
| 53ccee734f | |||
| fe9d0e84d3 | |||
| 4e7e12f90f | |||
| 8ae68cd34a | |||
| 607d69b1e1 | |||
| 50e62c7b73 | |||
| 363eff831f | |||
| 31820ddfd7 | |||
| 153749aebe | |||
| 7c5dd85e74 | |||
| 7a5bf8a06f | |||
| 9c05057deb | |||
| 8b1902601f | |||
| d94d9d0a78 | |||
| 457b0851c9 | |||
| d2cae9faca | |||
| e27739eafd | |||
| 874302537d | |||
| cd3b67d4cc | |||
| 7aacac2fb7 | |||
| a69fb3fec3 | |||
| 2d9cbe4f33 | |||
| e33ed0afc8 | |||
| ace6544444 | |||
| 1468c56e1f | |||
| b36756ebbc | |||
| a8f378deb7 | |||
| cb4e53c459 | |||
| 3e0b283593 | |||
| 3b4e9fd2e4 | |||
| 6af6195798 | |||
| a8d1f484b5 | |||
| 619c60533d | |||
| c6fd760006 | |||
| 03f88c6e3e | |||
| 697b33ae80 | |||
| 79ef1c4811 | |||
| f442e949aa | |||
| 8050d3f69a | |||
| 66efa1da5c | |||
| df97e808c6 | |||
| 98d14344ce | |||
| a149b0bcd0 | |||
| 0be1e350e9 | |||
| 4818bd57bb | |||
| 7763327a63 | |||
| dac957364f | |||
| 528dd4c3e8 | |||
| e40c362743 | |||
| 7fdb4dccca | |||
| a3c8c305c1 | |||
| aeb1b704b3 | |||
| f2cb6ea535 | |||
| 5a81279b9d | |||
| 9954bf5b51 | |||
| 309ea00f10 | |||
| df328d6812 | |||
| 7da0dfa0bb | |||
| d3a699a8fb | |||
| 82598d02d8 | |||
| 8032ea36be | |||
| 8ad130e14c | |||
| 5aa6fbc019 | |||
| 7026fdfa3a | |||
| 9dbecd4c95 | |||
| d85219d5fa | |||
| 26782811d6 | |||
| e38dbc4470 | |||
| a26624a614 | |||
| 6a88f35fc2 | |||
| 7a27fba94b | |||
| 2a87c32800 | |||
| d99bc37ed3 | |||
| 2c40d2fd0d | |||
| 611927d2ff | |||
| eb9ae61e75 | |||
| 4ae102018a | |||
| 4f01545328 | |||
| 762fb8259d | |||
| 2ca146c1aa | |||
| 16f40efb69 | |||
| a51a428761 | |||
| a5261cc1da | |||
| c8166b7cce | |||
| a11317c994 | |||
| 279f622026 | |||
| 32fa07d9ce | |||
| 2e63ff3aca | |||
| e3c17491e5 | |||
| da7b7c8923 | |||
| 5861af799c | |||
| 2b0353c771 | |||
| bc6c3c5e68 | |||
| 37ec16058f | |||
| b3efd2f31d | |||
| 357ebfca3f | |||
| 7e0cd9d709 | |||
| ba321551e4 | |||
| d96026ef5f | |||
| 08aa667b13 | |||
| 0cb93c4bea | |||
| 7b3ecc6e4f | |||
| 5a5b3f9927 | |||
| 4455226ee0 | |||
| 0881d74731 | |||
| 55d7191573 | |||
| bc9d0a9d3b | |||
| 866f138652 | |||
| ee8ad66959 | |||
| be0e47142b | |||
| 4131f1ba4f | |||
| f2966bfeaf | |||
| eee6330aaf | |||
| aa9ece529f | |||
| 865d18d367 | |||
| b27d07f304 | |||
| 50e60a5a17 | |||
| 8bf7e045d5 | |||
| f0f4427dc3 | |||
| b1f1f15ba8 | |||
| 09ca449eab | |||
| d08ce9e563 | |||
| ce92ea4084 | |||
| 0695f8e827 | |||
| 7609a46594 | |||
| 247838f70c | |||
| 594d419ca0 | |||
| da0c83513a | |||
| 5ed0b50110 | |||
| e01a7cf4fb | |||
| ad92ab13f3 | |||
| 236e5466cb | |||
| cc16e58c3a | |||
| 0ff9d5dc7e | |||
| f176ebe4c2 | |||
| e24852c480 | |||
| 008089c4c0 | |||
| f34e184d31 | |||
| 814257fbf8 | |||
| 83d92ecfcc | |||
| 0fbe39690e | |||
| ec2ad9861a | |||
| 16096ad745 | |||
| 481a70c304 | |||
| facd501100 | |||
| 4c6f41ffb2 | |||
| 3e93ab1966 | |||
| 0b5c4f6f0f | |||
| e8778cb992 | |||
| 2fae35b2c4 | |||
| 08c7bd4e42 | |||
| 1a17bf8fbb | |||
| f21b0570e8 | |||
| 9cac77cac7 | |||
| 3831d409f5 | |||
| 10fcb5fa6d | |||
| ff00f48e58 | |||
| 225a49f05e | |||
| d3c9c90f35 | |||
| 78592bfe2a | |||
| 1b2f5b6019 | |||
| 3c6511a0e7 | |||
| c525a08c17 | |||
| edaaafa059 | |||
| f16e84de32 | |||
| 4c99bc142b | |||
| 8e4f5fcae6 | |||
| c4d3cb409d | |||
| 9eed1378f6 | |||
| 5f282dd40a | |||
| 6e88a44399 | |||
| 94e731274a | |||
| 50c150f4b4 | |||
| a46810ca29 | |||
| 76cdd958ec | |||
| e72292f989 | |||
| ed4db91383 | |||
| 209430ed35 | |||
| 3d2b579f1a | |||
| c99ab2ac73 | |||
| e4591eaf57 | |||
| 695bb3757b | |||
| 4e2767ce9d | |||
| 8cdc9c9479 |
@@ -263,7 +263,11 @@ def list_matching_macs(options, session, node=None, checknode=True):
|
||||
return [options.mac.replace(':', '-')]
|
||||
else:
|
||||
path += 'by-mac/'
|
||||
return [x['item']['href'] for x in session.read(path)]
|
||||
ret = []
|
||||
for x in session.read(path):
|
||||
if 'item' in x and 'href' in x['item']:
|
||||
ret.append(x['item']['href'])
|
||||
return ret
|
||||
|
||||
def assign_discovery(options, session, needid=True):
|
||||
abort = False
|
||||
|
||||
@@ -109,6 +109,12 @@ def update_firmware(session, filename):
|
||||
if options.backup:
|
||||
upargs['bank'] = 'backup'
|
||||
noderrs = {}
|
||||
if session.unixdomain:
|
||||
of = open(filename, 'rb')
|
||||
try:
|
||||
session.add_file(filename, of.fileno(), 'rb')
|
||||
except Exception:
|
||||
pass
|
||||
for res in session.create(resource, upargs):
|
||||
if 'created' not in res:
|
||||
for nodename in res.get('databynode', ()):
|
||||
|
||||
@@ -69,6 +69,12 @@ def install_license(session, filename):
|
||||
'management_controller/licenses/'.format(noderange)
|
||||
filename = os.path.abspath(filename)
|
||||
instargs = {'filename': filename}
|
||||
if session.unixdomain:
|
||||
of = open(filename, 'rb')
|
||||
try:
|
||||
session.add_file(filename, of.fileno(), 'rb')
|
||||
except Exception:
|
||||
pass
|
||||
for res in session.create(resource, instargs):
|
||||
for node in res.get('databynode', []):
|
||||
if 'error' in res['databynode'][node]:
|
||||
|
||||
@@ -111,6 +111,12 @@ def upload_media(noderange, media):
|
||||
resource = '/noderange/{0}/media/uploads/'.format(noderange)
|
||||
upargs = {'filename': filename}
|
||||
noderrs = {}
|
||||
if session.unixdomain:
|
||||
of = open(filename, 'rb')
|
||||
try:
|
||||
session.add_file(filename, of.fileno(), 'rb')
|
||||
except Exception:
|
||||
pass
|
||||
nodeurls = {}
|
||||
for res in session.create(resource, upargs):
|
||||
if 'created' not in res:
|
||||
|
||||
@@ -186,7 +186,7 @@ funmap = {
|
||||
|
||||
def main():
|
||||
argparser = OptParser(
|
||||
usage='Usage: %prog <noderange> [show|create|delete|diskset]',
|
||||
usage='Usage: %prog <noderange> [show|create|delete|diskset] [hotspare|jbod|unconfigured] [options]',
|
||||
epilog='',
|
||||
)
|
||||
argparser.add_option('-r', '--raidlevel', type='int',
|
||||
|
||||
@@ -116,7 +116,7 @@ class OptParser(optparse.OptionParser):
|
||||
def main():
|
||||
argparser = OptParser(
|
||||
usage="Usage: %prog <noderange> servicedata "
|
||||
"<filename>",
|
||||
"<directory or filename>",
|
||||
epilog='\nservicedata will save service data to the given '
|
||||
'directory. It is saved to the location on the relevant '
|
||||
'management server (the confluent server if running remote, '
|
||||
|
||||
@@ -37,6 +37,8 @@ except ImportError:
|
||||
|
||||
def plot(gui, output, plotdata, bins):
|
||||
import matplotlib as mpl
|
||||
if gui and mpl.get_backend() == 'agg':
|
||||
sys.stderr.write('Error: No GUI backend available and -g specified!\n')
|
||||
if not gui:
|
||||
mpl.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
@@ -46,6 +46,13 @@ try:
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
|
||||
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
|
||||
@@ -154,6 +161,7 @@ class Command(object):
|
||||
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']
|
||||
@@ -163,11 +171,13 @@ class Command(object):
|
||||
self.serverloc = server
|
||||
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:
|
||||
self._connect_tls()
|
||||
tlvdata.recv(self.connection)
|
||||
self.protversion = int(tlvdata.recv(self.connection).split(
|
||||
b'--')[1].strip()[1:])
|
||||
authdata = tlvdata.recv(self.connection)
|
||||
if authdata['authpassed'] == 1:
|
||||
self.authenticated = True
|
||||
@@ -178,6 +188,13 @@ class Command(object):
|
||||
passphrase = os.environ['CONFLUENT_PASSPHRASE']
|
||||
self.authenticate(username, passphrase)
|
||||
|
||||
def add_file(self, name, handle, mode):
|
||||
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')
|
||||
tlvdata.send(self.connection, {'filename': name, 'mode': mode}, handle)
|
||||
|
||||
def authenticate(self, username, password):
|
||||
tlvdata.send(self.connection,
|
||||
{'username': username, 'password': password})
|
||||
@@ -417,7 +434,7 @@ def send_request(operation, path, server, parameters=None):
|
||||
result = tlvdata.recv(server)
|
||||
|
||||
|
||||
def attrrequested(attr, attrlist, seenattributes):
|
||||
def attrrequested(attr, attrlist, seenattributes, node=None):
|
||||
for candidate in attrlist:
|
||||
truename = candidate
|
||||
if candidate.startswith('hm'):
|
||||
@@ -425,10 +442,16 @@ def attrrequested(attr, attrlist, seenattributes):
|
||||
if candidate in _attraliases:
|
||||
candidate = _attraliases[candidate]
|
||||
if fnmatch.fnmatch(attr.lower(), candidate.lower()):
|
||||
seenattributes.add(truename)
|
||||
if node is None:
|
||||
seenattributes.add(truename)
|
||||
else:
|
||||
seenattributes[node][truename] = True
|
||||
return True
|
||||
elif attr.lower().startswith(candidate.lower() + '.'):
|
||||
seenattributes.add(truename)
|
||||
if node is None:
|
||||
seenattributes.add(truename)
|
||||
else:
|
||||
seenattributes[node][truename] = 1
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -444,13 +467,15 @@ def _sort_attrib(k):
|
||||
|
||||
def print_attrib_path(path, session, requestargs, options, rename=None, attrprefix=None):
|
||||
exitcode = 0
|
||||
seenattributes = set([])
|
||||
seenattributes = NestedDict()
|
||||
allnodes = set([])
|
||||
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))
|
||||
@@ -458,7 +483,7 @@ def print_attrib_path(path, session, requestargs, options, rename=None, attrpref
|
||||
if attr == 'errorcode':
|
||||
exitcode |= val
|
||||
continue
|
||||
seenattributes.add(attr)
|
||||
seenattributes[node][attr] = True
|
||||
if rename:
|
||||
printattr = rename.get(attr, attr)
|
||||
else:
|
||||
@@ -466,7 +491,7 @@ def print_attrib_path(path, session, requestargs, options, rename=None, attrpref
|
||||
if attrprefix:
|
||||
printattr = attrprefix + printattr
|
||||
currattr = res['databynode'][node][attr]
|
||||
if show_attr(attr, requestargs, seenattributes, options):
|
||||
if show_attr(attr, requestargs, seenattributes, options, node):
|
||||
if 'value' in currattr:
|
||||
if currattr['value'] is not None:
|
||||
val = currattr['value']
|
||||
@@ -547,23 +572,39 @@ def print_attrib_path(path, session, requestargs, options, rename=None, attrpref
|
||||
except TypeError:
|
||||
pass
|
||||
cprint(attrout)
|
||||
somematched = set([])
|
||||
printmissing = set([])
|
||||
badnodes = NestedDict()
|
||||
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
|
||||
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(attr))
|
||||
return exitcode
|
||||
|
||||
|
||||
def show_attr(attr, requestargs, seenattributes, options):
|
||||
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)
|
||||
processattr = attrrequested(attr, requestargs, seenattributes, node)
|
||||
if reverse:
|
||||
processattr = not processattr
|
||||
return processattr
|
||||
|
||||
@@ -17,4 +17,4 @@
|
||||
|
||||
|
||||
class Types(object):
|
||||
text, json = range(2)
|
||||
text, json, filehandle = range(3)
|
||||
|
||||
@@ -15,9 +15,15 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import array
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import confluent.tlv as tlv
|
||||
import eventlet.green.socket as socket
|
||||
import eventlet.green.select as select
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
|
||||
try:
|
||||
@@ -30,6 +36,72 @@ try:
|
||||
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
|
||||
|
||||
libc = ctypes.CDLL(ctypes.util.find_library('c'))
|
||||
recvmsg = libc.recvmsg
|
||||
recvmsg.argtypes = [ctypes.c_int, ctypes.POINTER(msghdr), ctypes.c_int]
|
||||
recvmsg.restype = ctypes.c_int
|
||||
sendmsg = libc.sendmsg
|
||||
sendmsg.argtypes = [ctypes.c_int, ctypes.POINTER(msghdr), ctypes.c_int]
|
||||
sendmsg.restype = ctypes.c_size_t
|
||||
|
||||
def decodestr(value):
|
||||
ret = None
|
||||
try:
|
||||
@@ -65,7 +137,7 @@ def _unicode_list(currlist):
|
||||
_unicode_list(currlist[i])
|
||||
|
||||
|
||||
def send(handle, data):
|
||||
def send(handle, data, filehandle=None):
|
||||
if isinstance(data, unicode):
|
||||
try:
|
||||
data = data.encode('utf-8')
|
||||
@@ -93,9 +165,27 @@ def send(handle, data):
|
||||
if tl > 16777215:
|
||||
raise Exception("JSON data exceeds protocol limits")
|
||||
# xor in the type (0b1 << 24)
|
||||
tl |= 16777216
|
||||
handle.sendall(struct.pack("!I", tl))
|
||||
handle.sendall(sdata)
|
||||
if filehandle is None:
|
||||
tl |= 16777216
|
||||
handle.sendall(struct.pack("!I", tl))
|
||||
handle.sendall(sdata)
|
||||
else:
|
||||
tl |= (2 << 24)
|
||||
handle.sendall(struct.pack("!I", tl))
|
||||
cdtype = ctypes.c_ubyte * len(sdata)
|
||||
cdata = cdtype.from_buffer(bytearray(sdata))
|
||||
ciov = iovec(iov_base=ctypes.addressof(cdata),
|
||||
iov_len=ctypes.c_size_t(ctypes.sizeof(cdata)))
|
||||
fd = ctypes.c_int(filehandle)
|
||||
cmh = cmsghdr.init_data(
|
||||
cmsg_len=CMSG_LEN(
|
||||
ctypes.sizeof(fd)), cmsg_level=socket.SOL_SOCKET,
|
||||
cmsg_type=SCM_RIGHTS, cmsg_data=fd)
|
||||
mh = msghdr(msg_name=None, msg_len=0, msg_iov=iovec_ptr(ciov),
|
||||
msg_iovlen=1, msg_control=ctypes.addressof(cmh),
|
||||
msg_controllen=ctypes.c_size_t(ctypes.sizeof(cmh)))
|
||||
sendmsg(handle.fileno(), mh, 0)
|
||||
|
||||
|
||||
def recvall(handle, size):
|
||||
rd = handle.recv(size)
|
||||
@@ -125,12 +215,42 @@ def recv(handle):
|
||||
datatype = (tl & 2130706432) >> 24 # grab 7 bits from near beginning
|
||||
if dlen == 0:
|
||||
return None
|
||||
data = handle.recv(dlen)
|
||||
while len(data) < dlen:
|
||||
ndata = handle.recv(dlen - len(data))
|
||||
if not ndata:
|
||||
raise Exception("Error reading data")
|
||||
data += ndata
|
||||
if datatype == tlv.Types.filehandle:
|
||||
filehandles = array.array('i')
|
||||
rawbuffer = bytearray(2048)
|
||||
pkttype = ctypes.c_ubyte * 2048
|
||||
data = pkttype.from_buffer(rawbuffer)
|
||||
cmsgsize = CMSG_SPACE(ctypes.sizeof(ctypes.c_int)).value
|
||||
cmsgarr = bytearray(cmsgsize)
|
||||
cmtype = ctypes.c_ubyte * cmsgsize
|
||||
cmsg = cmtype.from_buffer(cmsgarr)
|
||||
cmsg.cmsg_level = socket.SOL_SOCKET
|
||||
cmsg.cmsg_type = SCM_RIGHTS
|
||||
cmsg.cmsg_len = CMSG_LEN(ctypes.sizeof(ctypes.c_int))
|
||||
iov = iovec()
|
||||
iov.iov_base = ctypes.addressof(data)
|
||||
iov.iov_len = 2048
|
||||
msg = msghdr()
|
||||
msg.msg_iov = ctypes.pointer(iov)
|
||||
msg.msg_iovlen = 1
|
||||
msg.msg_control = ctypes.addressof(cmsg)
|
||||
msg.msg_controllen = ctypes.sizeof(cmsg)
|
||||
select.select([handle], [], [])
|
||||
i = recvmsg(handle.fileno(), ctypes.pointer(msg), 0)
|
||||
cdata = cmsgarr[CMSG_LEN(0).value:]
|
||||
data = rawbuffer[:i]
|
||||
if cmsg.cmsg_level == socket.SOL_SOCKET and cmsg.cmsg_type == SCM_RIGHTS:
|
||||
filehandles.fromstring(bytes(
|
||||
cdata[:len(cdata) - len(cdata) % filehandles.itemsize]))
|
||||
data = json.loads(bytes(data))
|
||||
return ClientFile(data['filename'], data['mode'], filehandles[0])
|
||||
else:
|
||||
data = handle.recv(dlen)
|
||||
while len(data) < dlen:
|
||||
ndata = handle.recv(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:
|
||||
|
||||
@@ -3,7 +3,7 @@ collate(1) -- Organize text input by node
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`<other command> | collate [-a] [-d] [-w] [-s] [-c] [-r] [-l lognametemplate]`
|
||||
`<other command> | collate [-a] [-b] [-d] [-w] [-g] [-s] [-c] [-r] [-l lognametemplate]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -21,19 +21,23 @@ node and group names sorted alphanumerically.
|
||||
when output differs along a telling group boundary. For example, if
|
||||
output suggests a large number of nodes are unreachable, abbreviate
|
||||
showing 'rack1' as being unreachable may make more obvious a possible cause.
|
||||
|
||||
* `-b`, `--base`:
|
||||
Use given node as reference for comparison when using
|
||||
-d, instead of using the most common result
|
||||
|
||||
* `-d`, `--diff`:
|
||||
Express all but the most common result group in terms of diff from
|
||||
the most common result group
|
||||
|
||||
* `-l`, `--log`:
|
||||
Save output per node to individual log files, replacing {node} in the name
|
||||
with the nodename of each
|
||||
|
||||
|
||||
* `-w`, `--watch`:
|
||||
Update results dynamically as data becomes available, rather than
|
||||
waiting for the command to fully complete.
|
||||
|
||||
|
||||
* `-g`, `--groupcount`:
|
||||
Show count of output groups rather than the actual
|
||||
output
|
||||
|
||||
* `-s`, `--skipcommon`:
|
||||
Suppress printing of the most common result text group. This is used to
|
||||
focus on stray output against a well known and expected result.
|
||||
@@ -45,6 +49,10 @@ node and group names sorted alphanumerically.
|
||||
* `-r`, `--reverse`:
|
||||
Rather than starting with most common to least common, start with
|
||||
the least common and print the most common last.
|
||||
|
||||
* `-l`, `--log`:
|
||||
Save output per node to individual log files, replacing {node} in the name
|
||||
with the nodename of each
|
||||
|
||||
## EXAMPLES
|
||||
* Organizing power state of multiple nodes:
|
||||
|
||||
@@ -6,7 +6,8 @@ collective(1) -- Check and manage a confluent collective
|
||||
`collective invite <server>`
|
||||
`collective join <server> [-i TOKEN]`
|
||||
`collective show`
|
||||
`collective gencert`
|
||||
`collective gencert`
|
||||
`collective delete`
|
||||
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -15,6 +15,18 @@ be mostly an aid for developing client software, with
|
||||
day to day administration generally being easier with
|
||||
the various function specific commands.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-s`, `--server`:
|
||||
Confluent instance to connect to
|
||||
|
||||
* `-c`, `--control`:
|
||||
Path to offer terminal control
|
||||
|
||||
* `-m`, `--mintime`:
|
||||
Minimum time to run or else pause for input (used to
|
||||
keep a terminal from closing quickly on error)
|
||||
|
||||
## COMMANDS
|
||||
|
||||
The CLI may be navigated by shell commands and some other
|
||||
|
||||
@@ -3,8 +3,7 @@ confluentdbutil(8) -- Backup or restore confluent database
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
|
||||
`confluentdbutil [options] <dump|restore> <path>`
|
||||
`confluentdbutil [options] [dump|restore] <path>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -19,8 +18,25 @@ the json files (password protected, removed from the files, or unprotected).
|
||||
* `-p`, `--password`:
|
||||
If specified, information such as usernames and passwords will be encrypted
|
||||
using the given password.
|
||||
|
||||
* `-i`, `--interactivepassword`:
|
||||
Prompt for password.
|
||||
|
||||
* `-r`, `--redact`:
|
||||
Indicates to replace usernames and passwords with a dummy string rather
|
||||
than included.
|
||||
|
||||
* `-u`, `--unprotected`:
|
||||
The keys.json file will include the encryption keys without any protection.
|
||||
The keys.json file will include the encryption keys without any protection.
|
||||
|
||||
* `-s`, `--skipkeys`:
|
||||
This specifies to dump the encrypted data without
|
||||
dumping the keys needed to decrypt it. This is
|
||||
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
|
||||
incremental backup.
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
@@ -40,12 +40,25 @@ See nodegroupattrib(8) command on how to manage attributes on a group level.
|
||||
|
||||
* `-b`, `--blame`:
|
||||
Annotate inherited and expression based attributes to show their base value.
|
||||
|
||||
* `-c`, `--clear`:
|
||||
Clear specified nodeattributes
|
||||
Clear specified nodeattributes.
|
||||
|
||||
* `-e`, `--environment`:
|
||||
Set specified attributes based on exported environment variable of matching name. Environment variable names may be lower case or all upper case. Replace . with _ as needed (e.g. info.note may be specified as either $info_note or $INFO_NOTE
|
||||
Set specified attributes based on exported environment variable of matching name.
|
||||
Environment variable names may be lower case or all upper case.
|
||||
Replace . with _ as needed (e.g. info.note may be specified as either $info_note or $INFO_NOTE
|
||||
|
||||
* `-p`, `--prompt`:
|
||||
Request interactive prompting to provide values rather than the command line or environment variables.
|
||||
Request interactive prompting to provide values rather than the command line
|
||||
or environment variables.
|
||||
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Prompt if trying to set attributes on more than
|
||||
specified number of nodes.
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
* Listing matching nodes of a simple noderange:
|
||||
|
||||
@@ -9,6 +9,15 @@ nodebmcreset(8) -- Reset management controller
|
||||
|
||||
`nodebmcreset` allows you to reset the management controller of the specified noderange
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Number of nodes to affect before prompting for
|
||||
confirmation
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES:
|
||||
|
||||
* Reset the management controller for nodes n1 through n4:
|
||||
|
||||
@@ -3,8 +3,7 @@ nodeboot(8) -- Reboot a confluent node to a specific device
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeboot <noderange>`
|
||||
`nodeboot [options] <noderange>` [default|cd|network|setup|hd]
|
||||
`nodeboot [options] <noderange> [default|cd|network|setup|hd]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -25,6 +24,13 @@ device without inducing a reboot, see the `nodesetboot` command.
|
||||
* `-p`, `--persist`:
|
||||
For a system that supports it, mark the boot override to persist rather than
|
||||
be a one time change. Many systems do not support this functionality.
|
||||
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to boot, prompting
|
||||
if over the threshold
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
* `default`:
|
||||
Request a normal default boot with no particular device override
|
||||
|
||||
@@ -3,8 +3,7 @@ nodeconfig(8) -- Show or change node configuration
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeconfig <noderange> [options] [<configuration>..]`
|
||||
`nodeconfig <noderange> [options] [<configuration=value>..]`
|
||||
`nodeconfig [options] <noderange> [setting|setting=value]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -14,6 +13,12 @@ running configuration on the node firmware. Calling without '=' will show the
|
||||
current value, and '=' will change the value. Network information can be
|
||||
given as a node expression, as documented in the man page for nodeattribexpressions(5).
|
||||
|
||||
Note that when using nodeconfig to submit changes, it will exit when the change
|
||||
is accepted, but the endpoint may not have fully processed it. Doing a show
|
||||
immediately after doing a set may reflect older information. Also, if changing
|
||||
BIOS/UEFI settings, the change may appear in output, but generally won't
|
||||
actually be in effect until a reboot.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-c`, `--comparedefault`:
|
||||
@@ -21,6 +26,13 @@ given as a node expression, as documented in the man page for nodeattribexpressi
|
||||
no configuration values are specified, it will show only those that differ.
|
||||
If combined with `-x`, will show all differing values except those indicated
|
||||
by `-x`
|
||||
|
||||
* `-b`, `--batch`:
|
||||
Provide arguments as lines of a file, rather than the command line.
|
||||
|
||||
* `-d`, `--detail`:
|
||||
Provide detailed data as available. This can include help text and valid
|
||||
values for a setting.
|
||||
|
||||
* `-e`, `--extra`:
|
||||
Read settings that are generally not needed, but may be slow to retrieve.
|
||||
@@ -31,18 +43,21 @@ given as a node expression, as documented in the man page for nodeattribexpressi
|
||||
Rather than listing only the specified configuration parameters, list all
|
||||
attributes except for the specified ones
|
||||
|
||||
* `-d`, `--detail`:
|
||||
Provide detailed data as available. This can include help text and valid
|
||||
values for a setting.
|
||||
* `-a`, `--advanced`:
|
||||
Include advanced settings, which are normally not intended to be used
|
||||
without direction from the relevant server vendor.
|
||||
|
||||
* `-r`, `--restoredefault`:
|
||||
Request that the specified component of the targeted nodes will have its
|
||||
configuration reset to default. Currently the only component implemented
|
||||
is uefi.
|
||||
|
||||
* `-b`, '--batch':
|
||||
Provide arguments as lines of a file, rather than the command line.
|
||||
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to configure, prompting if over
|
||||
the threshold
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
* Showing the current IP configuration of noderange BMC/IMM/XCC:
|
||||
|
||||
@@ -3,9 +3,7 @@ nodedeploy(8) -- Request preparation and/or initiating a node deployment
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodedeploy <noderange>`
|
||||
`nodedeploy <noderange> -c`
|
||||
`nodedeploy <noderange> [-n] [-p] <name of profile>`
|
||||
`nodedeploy [-h] [-c] [-n] [-p] [-m MAXNODES] <noderange> [profile]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -19,7 +17,7 @@ deployment status.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* '-c', '--clear`:
|
||||
* `-c`, `--clear`:
|
||||
Remove any pending deployment action
|
||||
|
||||
* `-n`, `--network`:
|
||||
@@ -28,6 +26,12 @@ deployment status.
|
||||
* `-p`, `--prepareonly`:
|
||||
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`:
|
||||
Specifiy a maximum nodes to be deployed.
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
* Begin the instalalation of a profile of CentOS 8.2:
|
||||
|
||||
@@ -3,14 +3,22 @@ nodeeventlog(8) -- Pull eventlog from confluent nodes
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeeventlog <noderange>`
|
||||
`nodeeventlog <noderange> [clear]`
|
||||
`nodeeventlog [options] <noderange> [clear]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodeeventlog` pulls and optionally clears the event log from the requested
|
||||
noderange.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to clear if clearing log, prompting if
|
||||
over the threshold
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
* Pull the event log from n2 and n3:
|
||||
`# nodeeventlog n2,n3`
|
||||
|
||||
@@ -3,9 +3,7 @@ nodefirmware(8) -- Report firmware information on confluent nodes
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodefirmware <noderange>`
|
||||
`nodefirmware <noderange> list|<components>|core`
|
||||
`nodefirmware <noderange> update [--backup] <filename>`
|
||||
`nodefirmware <noderange> [list][update [--backup <file>]]|[<components>]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -26,6 +24,18 @@ the out of band facilities. Firmware updates can end in one of three states:
|
||||
* `pending`: The firmware update process has completed, but the firmware will not be active until the relevant component next resets. Generally speaking, for UEFI update the system will need a reboot, and for BMC updates, the `nodebmcreset` command will begin the process to activate the firmware.
|
||||
* `complete`: The firmware update process has completed and activation has proceeded. Note that while the activation process has commenced, the component may still be in the process of rebooting when nodefirmware exits.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-b`, `--backup`:
|
||||
Target a backup bank rather than primary
|
||||
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
When updating, prompt if more than the specified number of servers will
|
||||
be affected
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Pull firmware from a node:
|
||||
|
||||
@@ -25,8 +25,17 @@ the attributes are set on the node versus a group to which a node belongs.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-b`, `--blame`:
|
||||
Show information about how attributes inherited
|
||||
|
||||
* `-e`, `--environment`:
|
||||
Set attributes, but from environment variable of same name
|
||||
|
||||
* `-c`, `--clear`:
|
||||
Clear specified nodeattributes.
|
||||
|
||||
* `-p`, `--prompt`:
|
||||
Prompt for attribute values interactively
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
|
||||
@@ -15,3 +15,7 @@ to that directory. The `install` command will take the specified filename and in
|
||||
argument may be of the form xcc_fod_0034_7X21{id.serial}.key to have the serial number substituted
|
||||
to allow unique licenses to be specified in a single command.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
Specify a maximum number of nodes to delete licenses from, prompting if over the threshold
|
||||
|
||||
@@ -34,8 +34,8 @@ the host platform attempts.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-c`:
|
||||
Specify the maximum number of instances to run concurrently.
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
* Listing currently mounted media:
|
||||
|
||||
@@ -30,6 +30,9 @@ off will not react to this request.
|
||||
|
||||
* `-p`, '--showprevious':
|
||||
Show previous power state for all directives that may change power state.
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
* Get power state of nodes n1 through n4:
|
||||
|
||||
@@ -9,6 +9,11 @@ noderemove(8) -- Remove nodes from the confluent management service
|
||||
|
||||
`noderemove` simply removes the given noderange from the confluent database.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
Specify a maximum number of nodes to delete, prompting if over the
|
||||
threshold
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
|
||||
@@ -11,9 +11,13 @@ nodereseat(8) -- Request a reseat of a node
|
||||
node's slot. This should be equivalent to removing the system entirely from
|
||||
the chassis and putting it back in, but without actually having to do so.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
Specify a maximum number of nodes to reseat, prompting if over the threshold
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Reseating the node `s1`:
|
||||
`# nodereseat s1`
|
||||
`s1: Reseat successful`
|
||||
|
||||
|
||||
@@ -12,10 +12,12 @@ noderange. This will present progress as percentage for all nodes.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-m`:
|
||||
Specify maximum number of nodes for noderange max.
|
||||
|
||||
* `-c`:
|
||||
* `-c`, `-f`, `--count`:
|
||||
Specify how many rsync executions to do concurrently. If noderange
|
||||
exceeds the count, then excess nodes will wait until one of the
|
||||
active count completes.
|
||||
active count completes.
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
Specify a maximum number of nodes to run rsync to, prompting if over the
|
||||
threshold
|
||||
|
||||
|
||||
@@ -13,6 +13,21 @@ nodeattribexpressions(5) are expanded prior to execution of the command. For
|
||||
noderun, the commands are locally executed. To execute commands on the nodes
|
||||
themselves, see nodeshell(8).
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-f`, `-c`, `--count`:
|
||||
Number of commands to run at a time
|
||||
|
||||
* `-n`, `--nonodeprefix`:
|
||||
Do not prefix output with node names
|
||||
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to run the command with, prompting if over
|
||||
the threshold
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Run ping against nodes n1 through n4:
|
||||
|
||||
@@ -2,8 +2,7 @@ nodesetboot(8) -- Check or set next boot device for noderange
|
||||
====================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodesetboot <noderange>`
|
||||
|
||||
`nodesetboot [options] <noderange> [default|cd|network|setup|hd|usb|floppy]`
|
||||
|
||||
## DESCRIPTION
|
||||
@@ -21,6 +20,8 @@ to use `nodesetboot <noderange> setup` and then initiate a reboot from within
|
||||
the operating system with ssh or similar rather than using the remote hardware
|
||||
control.
|
||||
|
||||
Running the command with no target queries the current setting.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-b`, `--bios`:
|
||||
@@ -33,6 +34,13 @@ control.
|
||||
|
||||
* `-u`, `--uefi`:
|
||||
This flag does nothing, it is for command compatibility with xCAT's rsetboot
|
||||
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to modify next boot device, prompting if
|
||||
over the threshold
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
* `default`:
|
||||
Request a normal default boot with no particular device override
|
||||
|
||||
@@ -3,7 +3,7 @@ nodeshell(8) -- Execute command on many nodes in a noderange through ssh
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeshell <noderange> [options] <command to execute on each node>`
|
||||
`nodeshell [options] <noderange> <command to execute on each node>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -13,10 +13,20 @@ nodeattribexpressions(5). `nodeshell` provides stdout as stdout and stderr
|
||||
as stderr, unlike psh which combines all stdout and stderr into stdout.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-c COUNT`, `-f COUNT`, `--count=COUNT`
|
||||
Specify the maximum number of instances to run concurrently
|
||||
|
||||
* `-c`:
|
||||
Specify the maximum number of instances to run concurrently.
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`
|
||||
Specify a maximum number of nodes to run remote ssh command to, prompting
|
||||
if over the threshold
|
||||
|
||||
* `-n`, `--nonodeprefix`
|
||||
Do not prefix output with node names
|
||||
|
||||
* `-p PORT`, `--port=PORT`
|
||||
Specify a custom port for ssh
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Running `echo hi` on for nodes:
|
||||
|
||||
@@ -3,12 +3,15 @@ nodestorage(8) -- Examine/Modify storage configuration of a node
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodestorage <noderange> [show|create|delete] [options]`
|
||||
`nodestorage <noderange> [show|create|delete|diskset] [hotspare|jbod|unconfigured] [options]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodestorage` provides access to the remote storage configuration of
|
||||
the noderange.
|
||||
the noderange. The `show` subcommand will show current storage configuration,
|
||||
`create` can be used to create new arrays or volumes, `delete` can be used to
|
||||
remove volumes and arrays, and `diskset` can modify the usage of disks
|
||||
indicated by `-d` to either be `unconfigured`, `jbod`, or `hotspare`.
|
||||
|
||||
## OPTIONS
|
||||
* `-r` **RAIDLEVEL**, `--raidlevel`=**RAIDLEVEL**:
|
||||
@@ -29,6 +32,15 @@ the noderange.
|
||||
volumes, or selecting a volume for delete. Default
|
||||
behavior is to use implementation provided default
|
||||
|
||||
* `-z` **STRIPSIZES**, `--stripsizes`=**STRIPSIZES**:
|
||||
Comma separated list of stripsizes to use when creating volumes.
|
||||
This value is in kilobytes. The default behavior is to allow the
|
||||
storage controller to decide
|
||||
|
||||
* `-m` **MAXNODES**, `--maxnodes`=**MAXNODES**:
|
||||
Specify a maximum number of nodes to configure storage on, prompting
|
||||
if over the threshold
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Deleting the volume `somedata`:
|
||||
|
||||
@@ -19,6 +19,12 @@ running in collective mode.
|
||||
Note that due to vendor filename requirements, any filename may have vendor
|
||||
specific suffixes added to any file produced.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-m`, `--maxnodes`:
|
||||
Specify a maximum number of nodes to download diagnostic data from, prompting
|
||||
if over the threshold
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Download support data from a single node to a specific filename
|
||||
|
||||
@@ -5,7 +5,7 @@ osdeploy(8) --- Configure general OS deployment facilities of confluent
|
||||
|
||||
`osdeploy import <iso>`
|
||||
`osdeploy updateboot <profile>`
|
||||
`osdeploy initialize [-h] [-g] [-u] [-s] [-k] [-t] [-p] [-i] [-l]`
|
||||
`osdeploy initialize [-h] [-g] [-u] [-s] [-k] [-t] [-p] [-i] [-l] [-a]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
|
||||
@@ -100,6 +100,9 @@ class HTTPSClient(client.HTTPConnection, object):
|
||||
self.sock = ctx.wrap_socket(psock, server_hostname=host)
|
||||
|
||||
def grab_url(self, url, data=None, returnrsp=False):
|
||||
return self.grab_url_with_status(url, data, returnrsp)[1]
|
||||
|
||||
def grab_url_with_status(self, url, data=None, returnrsp=False):
|
||||
if data:
|
||||
method = 'POST'
|
||||
else:
|
||||
@@ -108,9 +111,9 @@ class HTTPSClient(client.HTTPConnection, object):
|
||||
rsp = self.getresponse()
|
||||
if rsp.status >= 200 and rsp.status < 300:
|
||||
if returnrsp:
|
||||
return rsp
|
||||
return rsp.status, rsp
|
||||
else:
|
||||
return rsp.read()
|
||||
return rsp.status, rsp.read()
|
||||
raise Exception(rsp.read())
|
||||
|
||||
if __name__ == '__main__':
|
||||
@@ -121,8 +124,26 @@ if __name__ == '__main__':
|
||||
if len(sys.argv) == 1:
|
||||
HTTPSClient()
|
||||
sys.exit(0)
|
||||
if sys.argv[-2] == '-o':
|
||||
with open(sys.argv[3], 'wb') as outf:
|
||||
try:
|
||||
outbin = sys.argv.index('-o')
|
||||
sys.argv.pop(outbin)
|
||||
outbin = sys.argv.pop(outbin)
|
||||
except ValueError:
|
||||
outbin = None
|
||||
try:
|
||||
waitfor = sys.argv.index('-w')
|
||||
sys.argv.pop(waitfor)
|
||||
waitfor = int(sys.argv.pop(waitfor))
|
||||
except ValueError:
|
||||
waitfor = None
|
||||
try:
|
||||
data = sys.argv.index('-d')
|
||||
sys.argv.pop(data)
|
||||
data = sys.argv.pop(data)
|
||||
except ValueError:
|
||||
data = None
|
||||
if outbin:
|
||||
with open(outbin, 'wb') as outf:
|
||||
reader = HTTPSClient(json=json).grab_url(
|
||||
sys.argv[1], data, returnrsp=True)
|
||||
chunk = reader.read(16384)
|
||||
@@ -130,6 +151,13 @@ if __name__ == '__main__':
|
||||
outf.write(chunk)
|
||||
chunk = reader.read(16384)
|
||||
sys.exit(0)
|
||||
if os.path.exists(sys.argv[-1]):
|
||||
if len(sys.argv) > 2 and os.path.exists(sys.argv[-1]):
|
||||
data = open(sys.argv[-1]).read()
|
||||
sys.stdout.write(HTTPSClient(json=json).grab_url(sys.argv[1], data).decode())
|
||||
if waitfor:
|
||||
client = HTTPSClient(json=json)
|
||||
status = 201
|
||||
while status != waitfor:
|
||||
status, rsp = client.grab_url_with_status(sys.argv[1], data)
|
||||
sys.stdout.write(rsp.decode())
|
||||
else:
|
||||
sys.stdout.write(HTTPSClient(json=json).grab_url(sys.argv[1], data).decode())
|
||||
|
||||
@@ -12,7 +12,7 @@ sed -i 's/install::/install:*:/' /sysroot/etc/shadow
|
||||
sed -i 's/root::/root:*:/' /sysroot/etc/shadow
|
||||
mkdir -p /sysroot/root/.ssh
|
||||
#chmod 700 /sysroot/root/.ssh
|
||||
cat /ssh/*.rootpubkey > /sysroot/root/.ssh/authorized_keys
|
||||
cat /ssh/*pubkey > /sysroot/root/.ssh/authorized_keys
|
||||
#chmod 600 /sysroot/root/.ssh/authorized_keys
|
||||
mkdir -p /sysroot/etc/ssh/
|
||||
for i in /ssh/*.ca; do
|
||||
|
||||
@@ -9,12 +9,11 @@ if lspci -d 15b3:: -n |grep 15b3 > /dev/null; then
|
||||
# two lines to use the .iso instead of the tgz packaging
|
||||
#fetch_remote infiniband/mofed.iso
|
||||
#mkdir MLNX_OFED
|
||||
#mount -o loop ofed.iso MLNX_OFED
|
||||
#mount -o loop infiniband/mofed.iso MLNX_OFED
|
||||
fetch_remote infiniband/mofed.tgz
|
||||
tar xf infiniband/mofed.tgz
|
||||
# The rest is common between tar and iso
|
||||
cd MLNX_OFED*
|
||||
mount -o loop ofed
|
||||
./mlnxofedinstall --force
|
||||
fi
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ run_remote_python add_local_repositories
|
||||
# run_remote_python will use the appropriate python interpreter path to run the specified script
|
||||
# A post.custom is provided to more conveniently hold customizations, see the post.custom file.
|
||||
|
||||
# This will induce server side processing of the syncfile contents if
|
||||
# present
|
||||
run_remote_python syncfileclient
|
||||
|
||||
# run_remote example.sh
|
||||
# run_remote_python example.py
|
||||
run_remote post.custom
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/python
|
||||
import importlib
|
||||
import tempfile
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from importlib.machinery import SourceFileLoader
|
||||
try:
|
||||
apiclient = SourceFileLoader('apiclient', '/opt/confluent/bin/apiclient').load_module()
|
||||
except FileNotFoundError:
|
||||
apiclient = SourceFileLoader('apiclient', '/etc/confluent/apiclient').load_module()
|
||||
|
||||
|
||||
def partitionhostsline(line):
|
||||
comment = ''
|
||||
try:
|
||||
cmdidx = line.index('#')
|
||||
comment = line[cmdidx:]
|
||||
line = line[:cmdidx].strip()
|
||||
except ValueError:
|
||||
pass
|
||||
if not line:
|
||||
return '', [], comment
|
||||
ipaddr, names = line.split(maxsplit=1)
|
||||
names = names.split()
|
||||
return ipaddr, names, comment
|
||||
|
||||
class HostMerger:
|
||||
def __init__(self):
|
||||
self.byip = {}
|
||||
self.byname = {}
|
||||
self.sourcelines = []
|
||||
self.targlines = []
|
||||
|
||||
def read_source(self, sourcefile):
|
||||
with open(sourcefile, 'r') as hfile:
|
||||
self.sourcelines = hfile.read().split('\n')
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
for x in range(len(self.sourcelines)):
|
||||
line = self.sourcelines[x]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip:
|
||||
self.byip[currip] = x
|
||||
for name in names:
|
||||
self.byname[name] = x
|
||||
|
||||
def read_target(self, targetfile):
|
||||
with open(targetfile, 'r') as hfile:
|
||||
lines = hfile.read().split('\n')
|
||||
if not lines[-1]:
|
||||
lines = lines[:-1]
|
||||
for y in range(len(lines)):
|
||||
line = lines[y]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip in self.byip:
|
||||
x = self.byip[currip]
|
||||
if self.sourcelines[x] is None:
|
||||
# have already consumed this enntry
|
||||
continue
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
continue
|
||||
for name in names:
|
||||
if name in self.byname:
|
||||
x = self.byname[name]
|
||||
if self.sourcelines[x] is None:
|
||||
break
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
break
|
||||
else:
|
||||
self.targlines.append(line)
|
||||
|
||||
def write_out(self, targetfile):
|
||||
while not self.targlines[-1]:
|
||||
self.targlines = self.targlines[:-1]
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
with open(targetfile, 'w') as hosts:
|
||||
for line in self.targlines:
|
||||
hosts.write(line + '\n')
|
||||
for line in self.sourcelines:
|
||||
if line is not None:
|
||||
hosts.write(line + '\n')
|
||||
|
||||
|
||||
class CredMerger:
|
||||
def __init__(self):
|
||||
try:
|
||||
with open('/etc/login.defs', 'r') as ldefs:
|
||||
defs = ldefs.read().split('\n')
|
||||
except FileNotFoundError:
|
||||
defs = []
|
||||
lkup = {}
|
||||
self.discardnames = {}
|
||||
self.shadowednames = {}
|
||||
for line in defs:
|
||||
try:
|
||||
line = line[:line.index('#')]
|
||||
except ValueError:
|
||||
pass
|
||||
keyval = line.split()
|
||||
if len(keyval) < 2:
|
||||
continue
|
||||
lkup[keyval[0]] = keyval[1]
|
||||
self.uidmin = int(lkup.get('UID_MIN', 1000))
|
||||
self.uidmax = int(lkup.get('UID_MAX', 60000))
|
||||
self.gidmin = int(lkup.get('GID_MIN', 1000))
|
||||
self.gidmax = int(lkup.get('GID_MAX', 60000))
|
||||
self.shadowlines = None
|
||||
|
||||
def read_passwd(self, source, targfile=False):
|
||||
self.read_generic(source, self.uidmin, self.uidmax, targfile)
|
||||
|
||||
def read_group(self, source, targfile=False):
|
||||
self.read_generic(source, self.gidmin, self.gidmax, targfile)
|
||||
|
||||
def read_generic(self, source, minid, maxid, targfile):
|
||||
if targfile:
|
||||
self.targdata = []
|
||||
else:
|
||||
self.sourcedata = []
|
||||
with open(source, 'r') as inputfile:
|
||||
for line in inputfile.read().split('\n'):
|
||||
try:
|
||||
name, _, uid, _ = line.split(':', 3)
|
||||
uid = int(uid)
|
||||
except ValueError:
|
||||
continue
|
||||
if targfile:
|
||||
if uid <= minid or uid >= maxid:
|
||||
self.targdata.append(line)
|
||||
else:
|
||||
self.discardnames[name] = 1
|
||||
else:
|
||||
if uid >= minid and uid <= maxid:
|
||||
self.sourcedata.append(line)
|
||||
|
||||
def read_shadow(self, source):
|
||||
self.shadowlines = []
|
||||
try:
|
||||
with open(source, 'r') as inshadow:
|
||||
for line in inshadow.read().split('\n'):
|
||||
try:
|
||||
name, _ = line.split(':' , 1)
|
||||
except ValueError:
|
||||
continue
|
||||
if name in self.discardnames:
|
||||
continue
|
||||
self.shadowednames[name] = 1
|
||||
self.shadowlines.append(line)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
def write_out(self, outfile):
|
||||
with open(outfile, 'w') as targ:
|
||||
for line in self.targdata:
|
||||
targ.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
targ.write(line + '\n')
|
||||
if outfile == '/etc/passwd':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/shadow')
|
||||
with open('/etc/shadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':', 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!:::::::\n')
|
||||
if outfile == '/etc/group':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/gshadow')
|
||||
with open('/etc/gshadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':' , 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!::\n')
|
||||
|
||||
def synchronize():
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
ac = apiclient.HTTPSClient()
|
||||
data = json.dumps({'merge': tmpdir})
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles', data)
|
||||
if status == 202:
|
||||
while status != 204:
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles')
|
||||
if not isinstance(rsp, str):
|
||||
rsp = rsp.decode('utf8')
|
||||
pendpasswd = os.path.join(tmpdir, 'etc/passwd')
|
||||
if os.path.exists(pendpasswd):
|
||||
cm = CredMerger()
|
||||
cm.read_passwd(pendpasswd, targfile=False)
|
||||
cm.read_passwd('/etc/passwd', targfile=True)
|
||||
cm.write_out('/etc/passwd')
|
||||
pendgroup = os.path.join(tmpdir, 'etc/group')
|
||||
if os.path.exists(pendgroup):
|
||||
cm = CredMerger()
|
||||
cm.read_group(pendgroup, targfile=False)
|
||||
cm.read_group('/etc/group', targfile=True)
|
||||
cm.write_out('/etc/group')
|
||||
pendhosts = os.path.join(tmpdir, 'etc/hosts')
|
||||
if os.path.exists(pendhosts):
|
||||
cm = HostMerger()
|
||||
cm.read_source(pendhosts)
|
||||
cm.read_target('/etc/hosts')
|
||||
cm.write_out('/etc/hosts')
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
synchronize()
|
||||
@@ -13,7 +13,7 @@ sed -i 's/install::/install:*:/' /sysroot/etc/shadow
|
||||
sed -i 's/root::/root:*:/' /sysroot/etc/shadow
|
||||
mkdir -p /sysroot/root/.ssh
|
||||
chmod 700 /sysroot/root/.ssh
|
||||
cat /ssh/*.rootpubkey > /sysroot/root/.ssh/authorized_keys
|
||||
cat /ssh/*pubkey > /sysroot/root/.ssh/authorized_keys
|
||||
chmod 600 /sysroot/root/.ssh/authorized_keys
|
||||
mkdir -p /sysroot/etc/ssh/
|
||||
for i in /ssh/*.ca; do
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
Ansible playbooks ending in .yml or .yaml that are placed into this directory will be executed at the
|
||||
appropriate phase of the install process.
|
||||
|
||||
The 'hosts' may be omitted, and if included will be ignored, replaced with the host that is specifically
|
||||
requesting the playbooks be executed.
|
||||
|
||||
Also, the playbooks will be executed on the deployment server. Hence it may be slower in aggregate than
|
||||
running content under scripts/ which ask much less of the deployment server
|
||||
|
||||
Here is an example of what a playbook would look like broadly:
|
||||
|
||||
- name: Example
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Example1
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.4 test1
|
||||
create: yes
|
||||
- name: Example2
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.5 test2
|
||||
create: yes
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
Ansible playbooks ending in .yml or .yaml that are placed into this directory will be executed at the
|
||||
appropriate phase of the install process.
|
||||
|
||||
The 'hosts' may be omitted, and if included will be ignored, replaced with the host that is specifically
|
||||
requesting the playbooks be executed.
|
||||
|
||||
Also, the playbooks will be executed on the deployment server. Hence it may be slower in aggregate than
|
||||
running content under scripts/ which ask much less of the deployment server
|
||||
|
||||
Here is an example of what a playbook would look like broadly:
|
||||
|
||||
- name: Example
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Example1
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.4 test1
|
||||
create: yes
|
||||
- name: Example2
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.5 test2
|
||||
create: yes
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/sh
|
||||
sed -i 's/centos/CentOS/; s/rhel/Red Hat Enterprise Linux/; s/oraclelinux/Oracle Linux/' $2/profile.yaml
|
||||
sed -i 's/centos/CentOS/; s/rhel/Red Hat Enterprise Linux/; s/oraclelinux/Oracle Linux/; s/alma/AlmaLinux/' $2/profile.yaml
|
||||
ln -s $1/images/pxeboot/vmlinuz $2/boot/kernel && \
|
||||
ln -s $1/images/pxeboot/initrd.img $2/boot/initramfs/distribution
|
||||
mkdir -p $2/boot/efi/boot && \
|
||||
|
||||
@@ -19,6 +19,9 @@ run_remote firstboot.custom
|
||||
run_remote_parts firstboot
|
||||
|
||||
|
||||
# Induce execution of remote configuration, e.g. ansible plays in ansible/firstboot.d/
|
||||
run_remote_config firstboot
|
||||
|
||||
curl -X POST -d 'status: complete' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" https://$mgr/confluent-api/self/updatestatus
|
||||
systemctl disable firstboot
|
||||
rm /etc/systemd/system/firstboot.service
|
||||
|
||||
@@ -73,3 +73,21 @@ run_remote_python() {
|
||||
cd - > /dev/null
|
||||
return $retcode
|
||||
}
|
||||
|
||||
run_remote_config() {
|
||||
echo
|
||||
set_confluent_vars
|
||||
apiclient=/opt/confluent/bin/apiclient
|
||||
if [ -f /etc/confluent/apiclient ]; then
|
||||
apiclient=/etc/confluent/apiclient
|
||||
fi
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Requesting to run remote configuration for "'$*'" from $mgr under profile $profile
|
||||
/usr/libexec/platform-python $apiclient /confluent-api/self/remoteconfig/"$*" -d {}
|
||||
/usr/libexec/platform-python $apiclient /confluent-api/self/remoteconfig/status -w 204
|
||||
echo
|
||||
echo 'Completed remote configuration'
|
||||
echo '---------------------------------------------------------------------------'
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,6 @@ chmod og-rwx /etc/confluent/*
|
||||
export mgr profile nodename
|
||||
. /etc/confluent/functions
|
||||
|
||||
curl -X POST -d 'status: staged' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" https://$mgr/confluent-api/self/updatestatus
|
||||
|
||||
|
||||
if [ -f /tmp/cryptboot ]; then
|
||||
run_remote tpm_luks.sh
|
||||
@@ -30,9 +28,17 @@ run_remote_python add_local_repositories
|
||||
# run_remote_python will use the appropriate python interpreter path to run the specified script
|
||||
# A post.custom is provided to more conveniently hold customizations, see the post.custom file.
|
||||
|
||||
# This will induce server side processing of the syncfile contents if
|
||||
# present
|
||||
run_remote_python syncfileclient
|
||||
|
||||
# run_remote example.sh
|
||||
# run_remote_python example.py
|
||||
run_remote post.custom
|
||||
|
||||
# Also, scripts may be placed into 'post.d', e.g. post.d/01-runfirst.sh, post.d/02-runsecond.sh
|
||||
run_remote_parts post
|
||||
|
||||
# Induce execution of remote configuration, e.g. ansible plays in ansible/post.d/
|
||||
run_remote_config post
|
||||
curl -X POST -d 'status: staged' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" https://$mgr/confluent-api/self/updatestatus
|
||||
|
||||
@@ -14,6 +14,10 @@ chmod -R og-rwx /mnt/sysimage/etc/confluent
|
||||
cp /tmp/functions /mnt/sysimage/etc/confluent/
|
||||
. /tmp/functions
|
||||
cp /tmp/cryptboot /mnt/sysimage/tmp/
|
||||
echo Port 2222 >> /etc/ssh/sshd_config.anaconda
|
||||
echo Match LocalPort 22 >> /etc/ssh/sshd_config.anaconda
|
||||
echo " ChrootDirectory /mnt/sysimage" >> /etc/ssh/sshd_config.anaconda
|
||||
kill -HUP $(cat /run/sshd.pid)
|
||||
|
||||
# Preserve the ssh setup work done for the installer
|
||||
# by copying into the target system and setting up
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/python
|
||||
import importlib
|
||||
import tempfile
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from importlib.machinery import SourceFileLoader
|
||||
try:
|
||||
apiclient = SourceFileLoader('apiclient', '/opt/confluent/bin/apiclient').load_module()
|
||||
except FileNotFoundError:
|
||||
apiclient = SourceFileLoader('apiclient', '/etc/confluent/apiclient').load_module()
|
||||
|
||||
|
||||
def partitionhostsline(line):
|
||||
comment = ''
|
||||
try:
|
||||
cmdidx = line.index('#')
|
||||
comment = line[cmdidx:]
|
||||
line = line[:cmdidx].strip()
|
||||
except ValueError:
|
||||
pass
|
||||
if not line:
|
||||
return '', [], comment
|
||||
ipaddr, names = line.split(maxsplit=1)
|
||||
names = names.split()
|
||||
return ipaddr, names, comment
|
||||
|
||||
class HostMerger:
|
||||
def __init__(self):
|
||||
self.byip = {}
|
||||
self.byname = {}
|
||||
self.sourcelines = []
|
||||
self.targlines = []
|
||||
|
||||
def read_source(self, sourcefile):
|
||||
with open(sourcefile, 'r') as hfile:
|
||||
self.sourcelines = hfile.read().split('\n')
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
for x in range(len(self.sourcelines)):
|
||||
line = self.sourcelines[x]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip:
|
||||
self.byip[currip] = x
|
||||
for name in names:
|
||||
self.byname[name] = x
|
||||
|
||||
def read_target(self, targetfile):
|
||||
with open(targetfile, 'r') as hfile:
|
||||
lines = hfile.read().split('\n')
|
||||
if not lines[-1]:
|
||||
lines = lines[:-1]
|
||||
for y in range(len(lines)):
|
||||
line = lines[y]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip in self.byip:
|
||||
x = self.byip[currip]
|
||||
if self.sourcelines[x] is None:
|
||||
# have already consumed this enntry
|
||||
continue
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
continue
|
||||
for name in names:
|
||||
if name in self.byname:
|
||||
x = self.byname[name]
|
||||
if self.sourcelines[x] is None:
|
||||
break
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
break
|
||||
else:
|
||||
self.targlines.append(line)
|
||||
|
||||
def write_out(self, targetfile):
|
||||
while not self.targlines[-1]:
|
||||
self.targlines = self.targlines[:-1]
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
with open(targetfile, 'w') as hosts:
|
||||
for line in self.targlines:
|
||||
hosts.write(line + '\n')
|
||||
for line in self.sourcelines:
|
||||
if line is not None:
|
||||
hosts.write(line + '\n')
|
||||
|
||||
|
||||
class CredMerger:
|
||||
def __init__(self):
|
||||
try:
|
||||
with open('/etc/login.defs', 'r') as ldefs:
|
||||
defs = ldefs.read().split('\n')
|
||||
except FileNotFoundError:
|
||||
defs = []
|
||||
lkup = {}
|
||||
self.discardnames = {}
|
||||
self.shadowednames = {}
|
||||
for line in defs:
|
||||
try:
|
||||
line = line[:line.index('#')]
|
||||
except ValueError:
|
||||
pass
|
||||
keyval = line.split()
|
||||
if len(keyval) < 2:
|
||||
continue
|
||||
lkup[keyval[0]] = keyval[1]
|
||||
self.uidmin = int(lkup.get('UID_MIN', 1000))
|
||||
self.uidmax = int(lkup.get('UID_MAX', 60000))
|
||||
self.gidmin = int(lkup.get('GID_MIN', 1000))
|
||||
self.gidmax = int(lkup.get('GID_MAX', 60000))
|
||||
self.shadowlines = None
|
||||
|
||||
def read_passwd(self, source, targfile=False):
|
||||
self.read_generic(source, self.uidmin, self.uidmax, targfile)
|
||||
|
||||
def read_group(self, source, targfile=False):
|
||||
self.read_generic(source, self.gidmin, self.gidmax, targfile)
|
||||
|
||||
def read_generic(self, source, minid, maxid, targfile):
|
||||
if targfile:
|
||||
self.targdata = []
|
||||
else:
|
||||
self.sourcedata = []
|
||||
with open(source, 'r') as inputfile:
|
||||
for line in inputfile.read().split('\n'):
|
||||
try:
|
||||
name, _, uid, _ = line.split(':', 3)
|
||||
uid = int(uid)
|
||||
except ValueError:
|
||||
continue
|
||||
if targfile:
|
||||
if uid <= minid or uid >= maxid:
|
||||
self.targdata.append(line)
|
||||
else:
|
||||
self.discardnames[name] = 1
|
||||
else:
|
||||
if uid >= minid and uid <= maxid:
|
||||
self.sourcedata.append(line)
|
||||
|
||||
def read_shadow(self, source):
|
||||
self.shadowlines = []
|
||||
try:
|
||||
with open(source, 'r') as inshadow:
|
||||
for line in inshadow.read().split('\n'):
|
||||
try:
|
||||
name, _ = line.split(':' , 1)
|
||||
except ValueError:
|
||||
continue
|
||||
if name in self.discardnames:
|
||||
continue
|
||||
self.shadowednames[name] = 1
|
||||
self.shadowlines.append(line)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
def write_out(self, outfile):
|
||||
with open(outfile, 'w') as targ:
|
||||
for line in self.targdata:
|
||||
targ.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
targ.write(line + '\n')
|
||||
if outfile == '/etc/passwd':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/shadow')
|
||||
with open('/etc/shadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':', 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!:::::::\n')
|
||||
if outfile == '/etc/group':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/gshadow')
|
||||
with open('/etc/gshadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':' , 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!::\n')
|
||||
|
||||
def synchronize():
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
ac = apiclient.HTTPSClient()
|
||||
data = json.dumps({'merge': tmpdir})
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles', data)
|
||||
if status == 202:
|
||||
while status != 204:
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles')
|
||||
if not isinstance(rsp, str):
|
||||
rsp = rsp.decode('utf8')
|
||||
pendpasswd = os.path.join(tmpdir, 'etc/passwd')
|
||||
if os.path.exists(pendpasswd):
|
||||
cm = CredMerger()
|
||||
cm.read_passwd(pendpasswd, targfile=False)
|
||||
cm.read_passwd('/etc/passwd', targfile=True)
|
||||
cm.write_out('/etc/passwd')
|
||||
pendgroup = os.path.join(tmpdir, 'etc/group')
|
||||
if os.path.exists(pendgroup):
|
||||
cm = CredMerger()
|
||||
cm.read_group(pendgroup, targfile=False)
|
||||
cm.read_group('/etc/group', targfile=True)
|
||||
cm.write_out('/etc/group')
|
||||
pendhosts = os.path.join(tmpdir, 'etc/hosts')
|
||||
if os.path.exists(pendhosts):
|
||||
cm = HostMerger()
|
||||
cm.read_source(pendhosts)
|
||||
cm.read_target('/etc/hosts')
|
||||
cm.write_out('/etc/hosts')
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
synchronize()
|
||||
@@ -7,8 +7,22 @@ echo $uuid > /sys/devices/virtual/dmi/id/product_uuid
|
||||
mkdir -p /etc/confluent
|
||||
localcli network firewall unload
|
||||
touch /etc/confluent/confluent.info
|
||||
begin=$(date +%s)
|
||||
while ! grep NODENAME /etc/confluent/confluent.info; do
|
||||
echo "Searching for deployment service on local network..."
|
||||
/opt/confluent/bin/copernicus > /etc/confluent/confluent.info
|
||||
esxcfg-nics -l
|
||||
if [ $(date +%s) -gt $((begin + 90)) ]; then
|
||||
if [ ! -f /var/run/vmware/show-esx-shell-login ]; then
|
||||
chvt 1
|
||||
sleep 1
|
||||
chvt 2
|
||||
/etc/init.d/ESXShell start
|
||||
fi
|
||||
echo "Unable to locate a deployment system on the local network, verify network connectivity"
|
||||
echo "A debug session has been made available on Alt-F1"
|
||||
sleep 30
|
||||
fi
|
||||
done
|
||||
node=$(grep NODENAME: /etc/confluent/confluent.info|head -n 1|awk '{print $2}')
|
||||
mgr=$(grep MANAGER: /etc/confluent/confluent.info|head -n 1|awk '{print $2}')
|
||||
|
||||
@@ -4,7 +4,7 @@ install --firstdisk --overwritevmfs
|
||||
%include /tmp/ksnet
|
||||
%include /tmp/rootpw
|
||||
reboot
|
||||
%post
|
||||
%post --interpreter=busybox
|
||||
localcli network firewall unload
|
||||
STATUP=$(mktemp)
|
||||
echo '{"status": "complete"}' > $STATUP
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#!/usr/bin/python
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
uplinkmatch = re.compile('^\s*Uplinks:\s*(.*)')
|
||||
nodename = None
|
||||
for inf in open('/etc/confluent/confluent.info', 'r').read().split('\n'):
|
||||
if inf.startswith('NODENAME: '):
|
||||
@@ -24,8 +27,15 @@ for line in deploycfg:
|
||||
else:
|
||||
nslist=False
|
||||
cfg['nameservers'] = ','.join(nameservers)
|
||||
|
||||
vswinfo = subprocess.check_output(['localcli', 'network', 'vswitch', 'standard', 'list']).decode('utf8')
|
||||
vmnic = None
|
||||
for info in vswinfo.split('\n'):
|
||||
upinfo = uplinkmatch.match(info)
|
||||
if upinfo:
|
||||
vmnic = upinfo.group(1)
|
||||
netline = 'network --hostname={0} --bootproto={1}'.format(nodename, cfg['ipv4_method'])
|
||||
if vmnic:
|
||||
netline += ' --device={0}'.format(vmnic)
|
||||
if cfg['ipv4_method'] == 'static':
|
||||
netline += ' --ip={0} --netmask={1}'.format(cfg['ipv4_address'], cfg['ipv4_netmask'])
|
||||
if cfg.get('ipv4_gateway', 'null') not in (None, '', 'null'):
|
||||
|
||||
@@ -32,11 +32,12 @@ echo "done"
|
||||
cat > /etc/ssh/sshd_config << EOF
|
||||
Port 22
|
||||
Port 3389
|
||||
Subsystem sftp /usr/libexec/openssh/sftp-server
|
||||
PermitRootLogin yes
|
||||
AuthorizedKeysFile .ssh/authorized_keys
|
||||
EOF
|
||||
mkdir ~/.ssh
|
||||
cat /ssh/*.rootpubkey > ~/.ssh/authorized_keys
|
||||
cat /ssh/*pubkey > ~/.ssh/authorized_keys 2>/dev/null
|
||||
cat /tls/*.pem > /etc/confluent/ca.pem
|
||||
mkdir -p /etc/pki/tls/certs
|
||||
cat /tls/*.pem > /etc/pki/tls/certs/ca-bundle.crt
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
Ansible playbooks ending in .yml or .yaml that are placed into this directory will be executed at the
|
||||
appropriate phase of the install process.
|
||||
|
||||
The 'hosts' may be omitted, and if included will be ignored, replaced with the host that is specifically
|
||||
requesting the playbooks be executed.
|
||||
|
||||
Also, the playbooks will be executed on the deployment server. Hence it may be slower in aggregate than
|
||||
running content under scripts/ which ask much less of the deployment server
|
||||
|
||||
Here is an example of what a playbook would look like broadly:
|
||||
|
||||
- name: Example
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Example1
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.4 test1
|
||||
create: yes
|
||||
- name: Example2
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.5 test2
|
||||
create: yes
|
||||
|
||||
@@ -63,3 +63,20 @@ run_remote_python() {
|
||||
cd - > /dev/null
|
||||
return $retcode
|
||||
}
|
||||
|
||||
run_remote_config() {
|
||||
echo
|
||||
set_confluent_vars
|
||||
apiclient=/opt/confluent/bin/apiclient
|
||||
if [ -f /etc/confluent/apiclient ]; then
|
||||
apiclient=/etc/confluent/apiclient
|
||||
fi
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Requesting to run remote configuration for "'$*'" from $mgr under profile $profile
|
||||
/usr/libexec/platform-python $apiclient /confluent-api/self/remoteconfig/"$*" -d {}
|
||||
/usr/libexec/platform-python $apiclient /confluent-api/self/remoteconfig/status -w 204
|
||||
echo
|
||||
echo 'Completed remote configuration'
|
||||
echo '---------------------------------------------------------------------------'
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
# run_remote and run_remote_python are available to download scripts and
|
||||
# execute them.
|
||||
|
||||
# This will induce server side processing of the syncfile contents if
|
||||
# present
|
||||
run_remote_python syncfileclient
|
||||
|
||||
# Induce execution of remote configuration, e.g. ansible plays in ansible/onboot.d/
|
||||
run_remote_config onboot
|
||||
|
||||
# This is an example to request the BMC be configured on the network
|
||||
# according to how confluent has things configured:
|
||||
# run_remote_python configbmc -c
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/python
|
||||
import importlib
|
||||
import tempfile
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from importlib.machinery import SourceFileLoader
|
||||
try:
|
||||
apiclient = SourceFileLoader('apiclient', '/opt/confluent/bin/apiclient').load_module()
|
||||
except FileNotFoundError:
|
||||
apiclient = SourceFileLoader('apiclient', '/etc/confluent/apiclient').load_module()
|
||||
|
||||
|
||||
def partitionhostsline(line):
|
||||
comment = ''
|
||||
try:
|
||||
cmdidx = line.index('#')
|
||||
comment = line[cmdidx:]
|
||||
line = line[:cmdidx].strip()
|
||||
except ValueError:
|
||||
pass
|
||||
if not line:
|
||||
return '', [], comment
|
||||
ipaddr, names = line.split(maxsplit=1)
|
||||
names = names.split()
|
||||
return ipaddr, names, comment
|
||||
|
||||
class HostMerger:
|
||||
def __init__(self):
|
||||
self.byip = {}
|
||||
self.byname = {}
|
||||
self.sourcelines = []
|
||||
self.targlines = []
|
||||
|
||||
def read_source(self, sourcefile):
|
||||
with open(sourcefile, 'r') as hfile:
|
||||
self.sourcelines = hfile.read().split('\n')
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
for x in range(len(self.sourcelines)):
|
||||
line = self.sourcelines[x]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip:
|
||||
self.byip[currip] = x
|
||||
for name in names:
|
||||
self.byname[name] = x
|
||||
|
||||
def read_target(self, targetfile):
|
||||
with open(targetfile, 'r') as hfile:
|
||||
lines = hfile.read().split('\n')
|
||||
if not lines[-1]:
|
||||
lines = lines[:-1]
|
||||
for y in range(len(lines)):
|
||||
line = lines[y]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip in self.byip:
|
||||
x = self.byip[currip]
|
||||
if self.sourcelines[x] is None:
|
||||
# have already consumed this enntry
|
||||
continue
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
continue
|
||||
for name in names:
|
||||
if name in self.byname:
|
||||
x = self.byname[name]
|
||||
if self.sourcelines[x] is None:
|
||||
break
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
break
|
||||
else:
|
||||
self.targlines.append(line)
|
||||
|
||||
def write_out(self, targetfile):
|
||||
while not self.targlines[-1]:
|
||||
self.targlines = self.targlines[:-1]
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
with open(targetfile, 'w') as hosts:
|
||||
for line in self.targlines:
|
||||
hosts.write(line + '\n')
|
||||
for line in self.sourcelines:
|
||||
if line is not None:
|
||||
hosts.write(line + '\n')
|
||||
|
||||
|
||||
class CredMerger:
|
||||
def __init__(self):
|
||||
try:
|
||||
with open('/etc/login.defs', 'r') as ldefs:
|
||||
defs = ldefs.read().split('\n')
|
||||
except FileNotFoundError:
|
||||
defs = []
|
||||
lkup = {}
|
||||
self.discardnames = {}
|
||||
self.shadowednames = {}
|
||||
for line in defs:
|
||||
try:
|
||||
line = line[:line.index('#')]
|
||||
except ValueError:
|
||||
pass
|
||||
keyval = line.split()
|
||||
if len(keyval) < 2:
|
||||
continue
|
||||
lkup[keyval[0]] = keyval[1]
|
||||
self.uidmin = int(lkup.get('UID_MIN', 1000))
|
||||
self.uidmax = int(lkup.get('UID_MAX', 60000))
|
||||
self.gidmin = int(lkup.get('GID_MIN', 1000))
|
||||
self.gidmax = int(lkup.get('GID_MAX', 60000))
|
||||
self.shadowlines = None
|
||||
|
||||
def read_passwd(self, source, targfile=False):
|
||||
self.read_generic(source, self.uidmin, self.uidmax, targfile)
|
||||
|
||||
def read_group(self, source, targfile=False):
|
||||
self.read_generic(source, self.gidmin, self.gidmax, targfile)
|
||||
|
||||
def read_generic(self, source, minid, maxid, targfile):
|
||||
if targfile:
|
||||
self.targdata = []
|
||||
else:
|
||||
self.sourcedata = []
|
||||
with open(source, 'r') as inputfile:
|
||||
for line in inputfile.read().split('\n'):
|
||||
try:
|
||||
name, _, uid, _ = line.split(':', 3)
|
||||
uid = int(uid)
|
||||
except ValueError:
|
||||
continue
|
||||
if targfile:
|
||||
if uid <= minid or uid >= maxid:
|
||||
self.targdata.append(line)
|
||||
else:
|
||||
self.discardnames[name] = 1
|
||||
else:
|
||||
if uid >= minid and uid <= maxid:
|
||||
self.sourcedata.append(line)
|
||||
|
||||
def read_shadow(self, source):
|
||||
self.shadowlines = []
|
||||
try:
|
||||
with open(source, 'r') as inshadow:
|
||||
for line in inshadow.read().split('\n'):
|
||||
try:
|
||||
name, _ = line.split(':' , 1)
|
||||
except ValueError:
|
||||
continue
|
||||
if name in self.discardnames:
|
||||
continue
|
||||
self.shadowednames[name] = 1
|
||||
self.shadowlines.append(line)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
def write_out(self, outfile):
|
||||
with open(outfile, 'w') as targ:
|
||||
for line in self.targdata:
|
||||
targ.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
targ.write(line + '\n')
|
||||
if outfile == '/etc/passwd':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/shadow')
|
||||
with open('/etc/shadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':', 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!:::::::\n')
|
||||
if outfile == '/etc/group':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/gshadow')
|
||||
with open('/etc/gshadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':' , 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!::\n')
|
||||
|
||||
def synchronize():
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
ac = apiclient.HTTPSClient()
|
||||
data = json.dumps({'merge': tmpdir})
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles', data)
|
||||
if status == 202:
|
||||
while status != 204:
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles')
|
||||
if not isinstance(rsp, str):
|
||||
rsp = rsp.decode('utf8')
|
||||
pendpasswd = os.path.join(tmpdir, 'etc/passwd')
|
||||
if os.path.exists(pendpasswd):
|
||||
cm = CredMerger()
|
||||
cm.read_passwd(pendpasswd, targfile=False)
|
||||
cm.read_passwd('/etc/passwd', targfile=True)
|
||||
cm.write_out('/etc/passwd')
|
||||
pendgroup = os.path.join(tmpdir, 'etc/group')
|
||||
if os.path.exists(pendgroup):
|
||||
cm = CredMerger()
|
||||
cm.read_group(pendgroup, targfile=False)
|
||||
cm.read_group('/etc/group', targfile=True)
|
||||
cm.write_out('/etc/group')
|
||||
pendhosts = os.path.join(tmpdir, 'etc/hosts')
|
||||
if os.path.exists(pendhosts):
|
||||
cm = HostMerger()
|
||||
cm.read_source(pendhosts)
|
||||
cm.read_target('/etc/hosts')
|
||||
cm.write_out('/etc/hosts')
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
synchronize()
|
||||
@@ -12,7 +12,7 @@ sed -i 's/install::/install:*:/' /sysroot/etc/shadow
|
||||
sed -i 's/root::/root:*:/' /sysroot/etc/shadow
|
||||
mkdir -p /sysroot/root/.ssh
|
||||
#chmod 700 /sysroot/root/.ssh
|
||||
cat /ssh/*.rootpubkey > /sysroot/root/.ssh/authorized_keys
|
||||
cat /ssh/*pubkey > /sysroot/root/.ssh/authorized_keys
|
||||
#chmod 600 /sysroot/root/.ssh/authorized_keys
|
||||
mkdir -p /sysroot/etc/ssh/
|
||||
for i in /ssh/*.ca; do
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
echo "Installing certificates"
|
||||
echo '<authorized_keys xmlns="http://www.suse.com/1.0/yast2ns" xmlns:config="http://www.suse.com/1.0/configns" config:type="list">' > /tmp/rootkeys.xml
|
||||
for pub in /ssh/*.rootpubkey; do
|
||||
for pub in /ssh/*pubkey; do
|
||||
echo '<listentry>'$(cat $pub)'</listentry>' >> /tmp/rootkeys.xml
|
||||
done
|
||||
echo '</authorized_keys>' >> /tmp/rootkeys.xml
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
Ansible playbooks ending in .yml or .yaml that are placed into this directory will be executed at the
|
||||
appropriate phase of the install process.
|
||||
|
||||
The 'hosts' may be omitted, and if included will be ignored, replaced with the host that is specifically
|
||||
requesting the playbooks be executed.
|
||||
|
||||
Also, the playbooks will be executed on the deployment server. Hence it may be slower in aggregate than
|
||||
running content under scripts/ which ask much less of the deployment server
|
||||
|
||||
Here is an example of what a playbook would look like broadly:
|
||||
|
||||
- name: Example
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Example1
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.4 test1
|
||||
create: yes
|
||||
- name: Example2
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.5 test2
|
||||
create: yes
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
Ansible playbooks ending in .yml or .yaml that are placed into this directory will be executed at the
|
||||
appropriate phase of the install process.
|
||||
|
||||
The 'hosts' may be omitted, and if included will be ignored, replaced with the host that is specifically
|
||||
requesting the playbooks be executed.
|
||||
|
||||
Also, the playbooks will be executed on the deployment server. Hence it may be slower in aggregate than
|
||||
running content under scripts/ which ask much less of the deployment server
|
||||
|
||||
Here is an example of what a playbook would look like broadly:
|
||||
|
||||
- name: Example
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Example1
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.4 test1
|
||||
create: yes
|
||||
- name: Example2
|
||||
lineinfile:
|
||||
path: /etc/hosts
|
||||
line: 1.2.3.5 test2
|
||||
create: yes
|
||||
|
||||
@@ -39,6 +39,7 @@ dynamic behavior and replace with static configuration.
|
||||
<pattern>base</pattern>
|
||||
</patterns>
|
||||
<packages config:type="list">
|
||||
<package>python3</package>
|
||||
<package>openssl</package>
|
||||
<package>chrony</package>
|
||||
<package>rsync</package>
|
||||
|
||||
@@ -10,4 +10,11 @@ apikey=$(cat /etc/confluent/confluent.apikey)
|
||||
. /etc/confluent/functions
|
||||
|
||||
run_remote firstboot.custom
|
||||
|
||||
# Firstboot scripts may be placed into firstboot.d, e.g. firstboot.d/01-firstaction.sh, firstboot.d/02-secondaction.sh
|
||||
run_remote_parts firstboot
|
||||
|
||||
# Induce execution of remote configuration, e.g. ansible plays in ansible/firstboot.d/
|
||||
run_remote_config firstboot
|
||||
|
||||
curl --capath /etc/confluent/tls -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" -f -X POST -d "status: complete" https://$mgr/confluent-api/self/updatestatus
|
||||
|
||||
@@ -33,3 +33,20 @@ run_remote_python() {
|
||||
cd - > /dev/null
|
||||
return $retcode
|
||||
}
|
||||
|
||||
run_remote_config() {
|
||||
echo
|
||||
set_confluent_vars
|
||||
apiclient=/opt/confluent/bin/apiclient
|
||||
if [ -f /etc/confluent/apiclient ]; then
|
||||
apiclient=/etc/confluent/apiclient
|
||||
fi
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Requesting to run remote configuration for "'$*'" from $mgr under profile $profile
|
||||
python3 $apiclient /confluent-api/self/remoteconfig/"$*" -d {}
|
||||
python3 $apiclient /confluent-api/self/remoteconfig/status -w 204
|
||||
echo
|
||||
echo 'Completed remote configuration'
|
||||
echo '---------------------------------------------------------------------------'
|
||||
return
|
||||
}
|
||||
|
||||
@@ -21,7 +21,17 @@ chmod og-rwx /etc/confluent/*
|
||||
export mgr profile nodename
|
||||
. /etc/confluent/functions
|
||||
|
||||
# This will induce server side processing of the syncfile contents if
|
||||
# present
|
||||
run_remote_python syncfileclient
|
||||
|
||||
run_remote post.custom
|
||||
|
||||
# Also, scripts may be placed into 'post.d', e.g. post.d/01-runfirst.sh, post.d/02-runsecond.sh
|
||||
run_remote_parts post
|
||||
|
||||
# Induce execution of remote configuration, e.g. ansible plays in ansible/post.d/
|
||||
run_remote_config post
|
||||
|
||||
curl -X POST -d 'status: staged' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" https://$mgr/confluent-api/self/updatestatus
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ if [ "$cryptboot" != "" ] && [ "$cryptboot" != "none" ] && [ "$cryptboot" != "n
|
||||
fi
|
||||
|
||||
mkdir ~/.ssh
|
||||
cat /ssh/*.rootpubkey > ~/.ssh/authorized_keys
|
||||
cat /ssh/*pubkey > ~/.ssh/authorized_keys 2>/dev/null
|
||||
|
||||
ssh-keygen -A
|
||||
for i in /etc/ssh/ssh_host*key.pub; do
|
||||
certname=${i/.pub/-cert.pub}
|
||||
|
||||
@@ -17,5 +17,14 @@ cp -a /tls /mnt/etc/confluent/
|
||||
cp -a /tls/* /mnt/var/lib/ca-certificates/openssl
|
||||
cp -a /tls/* /mnt/var/lib/ca-certificates/pem
|
||||
cp -a /tls/*.pem /mnt/etc/pki/trust/anchors
|
||||
cat /tls/*.pem > /mnt/etc/confluent/ca.pem
|
||||
cp /opt/confluent/bin/apiclient /mnt/etc/confluent
|
||||
|
||||
run_remote setupssh.sh
|
||||
|
||||
echo Port 22 >> /etc/ssh/sshd_config
|
||||
echo Port 2222 >> /etc/ssh/sshd_config
|
||||
echo Match LocalPort 22 >> /etc/ssh/sshd_config
|
||||
echo " ChrootDirectory /mnt" >> /etc/ssh/sshd_config
|
||||
kill -HUP $(cat /run/sshd.pid)
|
||||
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/python
|
||||
import importlib
|
||||
import tempfile
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from importlib.machinery import SourceFileLoader
|
||||
try:
|
||||
apiclient = SourceFileLoader('apiclient', '/opt/confluent/bin/apiclient').load_module()
|
||||
except FileNotFoundError:
|
||||
apiclient = SourceFileLoader('apiclient', '/etc/confluent/apiclient').load_module()
|
||||
|
||||
|
||||
def partitionhostsline(line):
|
||||
comment = ''
|
||||
try:
|
||||
cmdidx = line.index('#')
|
||||
comment = line[cmdidx:]
|
||||
line = line[:cmdidx].strip()
|
||||
except ValueError:
|
||||
pass
|
||||
if not line:
|
||||
return '', [], comment
|
||||
ipaddr, names = line.split(maxsplit=1)
|
||||
names = names.split()
|
||||
return ipaddr, names, comment
|
||||
|
||||
class HostMerger:
|
||||
def __init__(self):
|
||||
self.byip = {}
|
||||
self.byname = {}
|
||||
self.sourcelines = []
|
||||
self.targlines = []
|
||||
|
||||
def read_source(self, sourcefile):
|
||||
with open(sourcefile, 'r') as hfile:
|
||||
self.sourcelines = hfile.read().split('\n')
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
for x in range(len(self.sourcelines)):
|
||||
line = self.sourcelines[x]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip:
|
||||
self.byip[currip] = x
|
||||
for name in names:
|
||||
self.byname[name] = x
|
||||
|
||||
def read_target(self, targetfile):
|
||||
with open(targetfile, 'r') as hfile:
|
||||
lines = hfile.read().split('\n')
|
||||
if not lines[-1]:
|
||||
lines = lines[:-1]
|
||||
for y in range(len(lines)):
|
||||
line = lines[y]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip in self.byip:
|
||||
x = self.byip[currip]
|
||||
if self.sourcelines[x] is None:
|
||||
# have already consumed this enntry
|
||||
continue
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
continue
|
||||
for name in names:
|
||||
if name in self.byname:
|
||||
x = self.byname[name]
|
||||
if self.sourcelines[x] is None:
|
||||
break
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
break
|
||||
else:
|
||||
self.targlines.append(line)
|
||||
|
||||
def write_out(self, targetfile):
|
||||
while not self.targlines[-1]:
|
||||
self.targlines = self.targlines[:-1]
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
with open(targetfile, 'w') as hosts:
|
||||
for line in self.targlines:
|
||||
hosts.write(line + '\n')
|
||||
for line in self.sourcelines:
|
||||
if line is not None:
|
||||
hosts.write(line + '\n')
|
||||
|
||||
|
||||
class CredMerger:
|
||||
def __init__(self):
|
||||
try:
|
||||
with open('/etc/login.defs', 'r') as ldefs:
|
||||
defs = ldefs.read().split('\n')
|
||||
except FileNotFoundError:
|
||||
defs = []
|
||||
lkup = {}
|
||||
self.discardnames = {}
|
||||
self.shadowednames = {}
|
||||
for line in defs:
|
||||
try:
|
||||
line = line[:line.index('#')]
|
||||
except ValueError:
|
||||
pass
|
||||
keyval = line.split()
|
||||
if len(keyval) < 2:
|
||||
continue
|
||||
lkup[keyval[0]] = keyval[1]
|
||||
self.uidmin = int(lkup.get('UID_MIN', 1000))
|
||||
self.uidmax = int(lkup.get('UID_MAX', 60000))
|
||||
self.gidmin = int(lkup.get('GID_MIN', 1000))
|
||||
self.gidmax = int(lkup.get('GID_MAX', 60000))
|
||||
self.shadowlines = None
|
||||
|
||||
def read_passwd(self, source, targfile=False):
|
||||
self.read_generic(source, self.uidmin, self.uidmax, targfile)
|
||||
|
||||
def read_group(self, source, targfile=False):
|
||||
self.read_generic(source, self.gidmin, self.gidmax, targfile)
|
||||
|
||||
def read_generic(self, source, minid, maxid, targfile):
|
||||
if targfile:
|
||||
self.targdata = []
|
||||
else:
|
||||
self.sourcedata = []
|
||||
with open(source, 'r') as inputfile:
|
||||
for line in inputfile.read().split('\n'):
|
||||
try:
|
||||
name, _, uid, _ = line.split(':', 3)
|
||||
uid = int(uid)
|
||||
except ValueError:
|
||||
continue
|
||||
if targfile:
|
||||
if uid <= minid or uid >= maxid:
|
||||
self.targdata.append(line)
|
||||
else:
|
||||
self.discardnames[name] = 1
|
||||
else:
|
||||
if uid >= minid and uid <= maxid:
|
||||
self.sourcedata.append(line)
|
||||
|
||||
def read_shadow(self, source):
|
||||
self.shadowlines = []
|
||||
try:
|
||||
with open(source, 'r') as inshadow:
|
||||
for line in inshadow.read().split('\n'):
|
||||
try:
|
||||
name, _ = line.split(':' , 1)
|
||||
except ValueError:
|
||||
continue
|
||||
if name in self.discardnames:
|
||||
continue
|
||||
self.shadowednames[name] = 1
|
||||
self.shadowlines.append(line)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
def write_out(self, outfile):
|
||||
with open(outfile, 'w') as targ:
|
||||
for line in self.targdata:
|
||||
targ.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
targ.write(line + '\n')
|
||||
if outfile == '/etc/passwd':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/shadow')
|
||||
with open('/etc/shadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':', 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!:::::::\n')
|
||||
if outfile == '/etc/group':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/gshadow')
|
||||
with open('/etc/gshadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':' , 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!::\n')
|
||||
|
||||
def synchronize():
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
ac = apiclient.HTTPSClient()
|
||||
data = json.dumps({'merge': tmpdir})
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles', data)
|
||||
if status == 202:
|
||||
while status != 204:
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles')
|
||||
if not isinstance(rsp, str):
|
||||
rsp = rsp.decode('utf8')
|
||||
pendpasswd = os.path.join(tmpdir, 'etc/passwd')
|
||||
if os.path.exists(pendpasswd):
|
||||
cm = CredMerger()
|
||||
cm.read_passwd(pendpasswd, targfile=False)
|
||||
cm.read_passwd('/etc/passwd', targfile=True)
|
||||
cm.write_out('/etc/passwd')
|
||||
pendgroup = os.path.join(tmpdir, 'etc/group')
|
||||
if os.path.exists(pendgroup):
|
||||
cm = CredMerger()
|
||||
cm.read_group(pendgroup, targfile=False)
|
||||
cm.read_group('/etc/group', targfile=True)
|
||||
cm.write_out('/etc/group')
|
||||
pendhosts = os.path.join(tmpdir, 'etc/hosts')
|
||||
if os.path.exists(pendhosts):
|
||||
cm = HostMerger()
|
||||
cm.read_source(pendhosts)
|
||||
cm.read_target('/etc/hosts')
|
||||
cm.write_out('/etc/hosts')
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
synchronize()
|
||||
@@ -11,4 +11,8 @@ apikey=$(cat /etc/confluent/confluent.apikey)
|
||||
mgr=$(grep ^deploy_server: /etc/confluent/confluent.deploycfg |awk '{print $2}')
|
||||
hostnamectl set-hostname $(grep ^NODENAME: /etc/confluent/confluent.info | awk '{print $2}')
|
||||
touch /etc/cloud/cloud-init.disabled
|
||||
source /etc/confluent/functions
|
||||
|
||||
run_remote_parts firstboot
|
||||
run_remote_config firstboot
|
||||
curl --capath /etc/confluent/tls -f -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $apikey" -X POST -d "status: complete" https://$mgr/confluent-api/self/updatestatus
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
function set_confluent_vars() {
|
||||
if [ -z "$mgr" ]; then
|
||||
mgr=$(grep ^deploy_server: /etc/confluent/confluent.deploycfg | sed -e 's/[^ ]*: //')
|
||||
fi
|
||||
if [ -z "$profile" ]; then
|
||||
profile=$(grep ^profile: /etc/confluent/confluent.deploycfg | sed -e 's/[^ ]*: //')
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_remote() {
|
||||
if [ -f /etc/confluent/ca.pem ]; then
|
||||
curlargs=" --cacert /etc/confluent/ca.pem"
|
||||
fi
|
||||
set_confluent_vars
|
||||
mkdir -p $(dirname $1)
|
||||
curl -f -sS $curlargs https://$mgr/confluent-public/os/$profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo $1 failed to download; return 1; fi
|
||||
}
|
||||
|
||||
run_remote_parts() {
|
||||
scriptlist=$(python3 /etc/confluent/apiclient /confluent-api/self/scriptlist/$1|sed -e 's/^- //')
|
||||
for script in $scriptlist; do
|
||||
run_remote $1.d/$script
|
||||
done
|
||||
}
|
||||
|
||||
run_remote() {
|
||||
requestedcmd="'$*'"
|
||||
curlargs=""
|
||||
if [ -f /etc/confluent/ca.pem ]; then
|
||||
curlargs=" --cacert /etc/confluent/ca.pem"
|
||||
fi
|
||||
set_confluent_vars
|
||||
echo
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Running $requestedcmd from https://$mgr/confluent-public/os/$profile/scripts/
|
||||
tmpdir=$(mktemp -d)
|
||||
echo Executing in $tmpdir
|
||||
cd $tmpdir
|
||||
mkdir -p $(dirname $1)
|
||||
curl -f -sS $curlargs https://$mgr/confluent-public/os/$profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo $requestedcmd failed to download; return 1; fi
|
||||
chmod +x $1
|
||||
cmd=$1
|
||||
#if [ -x /usr/bin/chcon ]; then
|
||||
# chcon system_u:object_r:bin_t:s0 $cmd
|
||||
#fi
|
||||
shift
|
||||
./$cmd $*
|
||||
retcode=$?
|
||||
echo "$requestedcmd exited with code $retcode"
|
||||
cd - > /dev/null
|
||||
return $retcode
|
||||
}
|
||||
|
||||
run_remote_python() {
|
||||
echo
|
||||
set_confluent_vars
|
||||
if [ -f /etc/confluent/ca.pem ]; then
|
||||
curlargs=" --cacert /etc/confluent/ca.pem"
|
||||
fi
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Running python script "'$*'" from https://$mgr/confluent-public/os/$profile/scripts/
|
||||
tmpdir=$(mktemp -d)
|
||||
echo Executing in $tmpdir
|
||||
cd $tmpdir
|
||||
mkdir -p $(dirname $1)
|
||||
curl -f -sS $curlargs https://$mgr/confluent-public/os/$profile/scripts/$1 > $1
|
||||
if [ $? != 0 ]; then echo "'$*'" failed to download; return 1; fi
|
||||
python3 $*
|
||||
retcode=$?
|
||||
echo "'$*' exited with code $retcode"
|
||||
cd - > /dev/null
|
||||
return $retcode
|
||||
}
|
||||
|
||||
run_remote_config() {
|
||||
echo
|
||||
set_confluent_vars
|
||||
apiclient=/opt/confluent/bin/apiclient
|
||||
if [ -f /etc/confluent/apiclient ]; then
|
||||
apiclient=/etc/confluent/apiclient
|
||||
fi
|
||||
echo '---------------------------------------------------------------------------'
|
||||
echo Requesting to run remote configuration for "'$*'" from $mgr under profile $profile
|
||||
python3 $apiclient /confluent-api/self/remoteconfig/"$*" -d {}
|
||||
python3 $apiclient /confluent-api/self/remoteconfig/status -w 204
|
||||
echo
|
||||
echo 'Completed remote configuration'
|
||||
echo '---------------------------------------------------------------------------'
|
||||
return
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ echo ' EnableSSHKeysign yes' >> $sshconf
|
||||
echo ' HostbasedKeyTypes *ed25519*' >> $sshconf
|
||||
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/firstboot.sh > /target/etc/confluent/firstboot.sh
|
||||
curl -f https://$mgr/confluent-public/os/$profile/scripts/functions > /target/etc/confluent/functions
|
||||
source /target/etc/confluent/functions
|
||||
chmod +x /target/etc/confluent/firstboot.sh
|
||||
cp /tmp/allnodes /target/root/.shosts
|
||||
cp /tmp/allnodes /target/etc/ssh/shosts.equiv
|
||||
@@ -44,11 +46,30 @@ kargs=$(curl https://$mgr/confluent-public/os/$profile/profile.yaml | grep ^inst
|
||||
if [ ! -z "$kargs" ]; then
|
||||
sed -i 's/GRUB_CMDLINE_LINUX="\([^"]*\)"/GRUB_CMDLINE_LINUX="\1 '"${kargs}"'"/' /target/etc/default/grub
|
||||
fi
|
||||
if [ 1 = $updategrub ]; then
|
||||
mount -o bind /dev /target/dev
|
||||
mount -o bind /proc /target/proc
|
||||
mount -o bind /sys /target/sys
|
||||
chroot /target update-grub
|
||||
umount /target/sys /target/dev /target/proc
|
||||
fi
|
||||
mkdir -p /opt/confluent/bin
|
||||
mkdir -p /etc/confluent
|
||||
cp -a /target/etc/confluent/* /etc/confluent
|
||||
cp /custom-installation/confluent/bin/apiclient /opt/confluent/bin/
|
||||
cp /custom-installation/confluent/bin/apiclient /target/etc/confluent/
|
||||
|
||||
mount -o bind /dev /target/dev
|
||||
mount -o bind /proc /target/proc
|
||||
mount -o bind /sys /target/sys
|
||||
if [ 1 = $updategrub ]; then
|
||||
chroot /target update-grub
|
||||
fi
|
||||
echo "Port 22" >> /etc/ssh/sshd_config
|
||||
echo "Port 2222" >> /etc/ssh/sshd_config
|
||||
echo "Match LocalPort 22" >> /etc/ssh/sshd_config
|
||||
echo " ChrootDirectory /target" >> /etc/ssh/sshd_config
|
||||
kill -HUP $(cat /run/sshd.pid)
|
||||
cat /target/etc/confluent/tls/*.pem > /target/etc/confluent/ca.pem
|
||||
cat /target/etc/confluent/tls/*.pem > /etc/confluent/ca.pem
|
||||
chroot /target bash -c "source /etc/confluent/functions; run_remote_python syncfileclient"
|
||||
chroot /target bash -c "source /etc/confluent/functions; run_remote_parts post"
|
||||
source /target/etc/confluent/functions
|
||||
|
||||
run_remote_config post
|
||||
|
||||
umount /target/sys /target/dev /target/proc
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ if [ "$cryptboot" != "" ] && [ "$cryptboot" != "none" ] && [ "$cryptboot" != "n
|
||||
fi
|
||||
|
||||
|
||||
cat /custom-installation/ssh/*.rootpubkey > /root/.ssh/authorized_keys
|
||||
cat /custom-installation/ssh/*pubkey > /root/.ssh/authorized_keys
|
||||
nodename=$(grep ^NODENAME: /custom-installation/confluent/confluent.info|awk '{print $2}')
|
||||
apikey=$(cat /custom-installation/confluent/confluent.apikey)
|
||||
for pubkey in /etc/ssh/ssh_host*key.pub; do
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/python
|
||||
import importlib
|
||||
import tempfile
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from importlib.machinery import SourceFileLoader
|
||||
try:
|
||||
apiclient = SourceFileLoader('apiclient', '/opt/confluent/bin/apiclient').load_module()
|
||||
except FileNotFoundError:
|
||||
apiclient = SourceFileLoader('apiclient', '/etc/confluent/apiclient').load_module()
|
||||
|
||||
|
||||
def partitionhostsline(line):
|
||||
comment = ''
|
||||
try:
|
||||
cmdidx = line.index('#')
|
||||
comment = line[cmdidx:]
|
||||
line = line[:cmdidx].strip()
|
||||
except ValueError:
|
||||
pass
|
||||
if not line:
|
||||
return '', [], comment
|
||||
ipaddr, names = line.split(maxsplit=1)
|
||||
names = names.split()
|
||||
return ipaddr, names, comment
|
||||
|
||||
class HostMerger:
|
||||
def __init__(self):
|
||||
self.byip = {}
|
||||
self.byname = {}
|
||||
self.sourcelines = []
|
||||
self.targlines = []
|
||||
|
||||
def read_source(self, sourcefile):
|
||||
with open(sourcefile, 'r') as hfile:
|
||||
self.sourcelines = hfile.read().split('\n')
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
for x in range(len(self.sourcelines)):
|
||||
line = self.sourcelines[x]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip:
|
||||
self.byip[currip] = x
|
||||
for name in names:
|
||||
self.byname[name] = x
|
||||
|
||||
def read_target(self, targetfile):
|
||||
with open(targetfile, 'r') as hfile:
|
||||
lines = hfile.read().split('\n')
|
||||
if not lines[-1]:
|
||||
lines = lines[:-1]
|
||||
for y in range(len(lines)):
|
||||
line = lines[y]
|
||||
currip, names, comment = partitionhostsline(line)
|
||||
if currip in self.byip:
|
||||
x = self.byip[currip]
|
||||
if self.sourcelines[x] is None:
|
||||
# have already consumed this enntry
|
||||
continue
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
continue
|
||||
for name in names:
|
||||
if name in self.byname:
|
||||
x = self.byname[name]
|
||||
if self.sourcelines[x] is None:
|
||||
break
|
||||
self.targlines.append(self.sourcelines[x])
|
||||
self.sourcelines[x] = None
|
||||
break
|
||||
else:
|
||||
self.targlines.append(line)
|
||||
|
||||
def write_out(self, targetfile):
|
||||
while not self.targlines[-1]:
|
||||
self.targlines = self.targlines[:-1]
|
||||
while not self.sourcelines[-1]:
|
||||
self.sourcelines = self.sourcelines[:-1]
|
||||
with open(targetfile, 'w') as hosts:
|
||||
for line in self.targlines:
|
||||
hosts.write(line + '\n')
|
||||
for line in self.sourcelines:
|
||||
if line is not None:
|
||||
hosts.write(line + '\n')
|
||||
|
||||
|
||||
class CredMerger:
|
||||
def __init__(self):
|
||||
try:
|
||||
with open('/etc/login.defs', 'r') as ldefs:
|
||||
defs = ldefs.read().split('\n')
|
||||
except FileNotFoundError:
|
||||
defs = []
|
||||
lkup = {}
|
||||
self.discardnames = {}
|
||||
self.shadowednames = {}
|
||||
for line in defs:
|
||||
try:
|
||||
line = line[:line.index('#')]
|
||||
except ValueError:
|
||||
pass
|
||||
keyval = line.split()
|
||||
if len(keyval) < 2:
|
||||
continue
|
||||
lkup[keyval[0]] = keyval[1]
|
||||
self.uidmin = int(lkup.get('UID_MIN', 1000))
|
||||
self.uidmax = int(lkup.get('UID_MAX', 60000))
|
||||
self.gidmin = int(lkup.get('GID_MIN', 1000))
|
||||
self.gidmax = int(lkup.get('GID_MAX', 60000))
|
||||
self.shadowlines = None
|
||||
|
||||
def read_passwd(self, source, targfile=False):
|
||||
self.read_generic(source, self.uidmin, self.uidmax, targfile)
|
||||
|
||||
def read_group(self, source, targfile=False):
|
||||
self.read_generic(source, self.gidmin, self.gidmax, targfile)
|
||||
|
||||
def read_generic(self, source, minid, maxid, targfile):
|
||||
if targfile:
|
||||
self.targdata = []
|
||||
else:
|
||||
self.sourcedata = []
|
||||
with open(source, 'r') as inputfile:
|
||||
for line in inputfile.read().split('\n'):
|
||||
try:
|
||||
name, _, uid, _ = line.split(':', 3)
|
||||
uid = int(uid)
|
||||
except ValueError:
|
||||
continue
|
||||
if targfile:
|
||||
if uid <= minid or uid >= maxid:
|
||||
self.targdata.append(line)
|
||||
else:
|
||||
self.discardnames[name] = 1
|
||||
else:
|
||||
if uid >= minid and uid <= maxid:
|
||||
self.sourcedata.append(line)
|
||||
|
||||
def read_shadow(self, source):
|
||||
self.shadowlines = []
|
||||
try:
|
||||
with open(source, 'r') as inshadow:
|
||||
for line in inshadow.read().split('\n'):
|
||||
try:
|
||||
name, _ = line.split(':' , 1)
|
||||
except ValueError:
|
||||
continue
|
||||
if name in self.discardnames:
|
||||
continue
|
||||
self.shadowednames[name] = 1
|
||||
self.shadowlines.append(line)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
def write_out(self, outfile):
|
||||
with open(outfile, 'w') as targ:
|
||||
for line in self.targdata:
|
||||
targ.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
targ.write(line + '\n')
|
||||
if outfile == '/etc/passwd':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/shadow')
|
||||
with open('/etc/shadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':', 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!:::::::\n')
|
||||
if outfile == '/etc/group':
|
||||
if self.shadowlines is None:
|
||||
self.read_shadow('/etc/gshadow')
|
||||
with open('/etc/gshadow', 'w') as shadout:
|
||||
for line in self.shadowlines:
|
||||
shadout.write(line + '\n')
|
||||
for line in self.sourcedata:
|
||||
name, _ = line.split(':' , 1)
|
||||
if name in self.shadowednames:
|
||||
continue
|
||||
shadout.write(name + ':!::\n')
|
||||
|
||||
def synchronize():
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
ac = apiclient.HTTPSClient()
|
||||
data = json.dumps({'merge': tmpdir})
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles', data)
|
||||
if status == 202:
|
||||
while status != 204:
|
||||
status, rsp = ac.grab_url_with_status('/confluent-api/self/remotesyncfiles')
|
||||
if not isinstance(rsp, str):
|
||||
rsp = rsp.decode('utf8')
|
||||
pendpasswd = os.path.join(tmpdir, 'etc/passwd')
|
||||
if os.path.exists(pendpasswd):
|
||||
cm = CredMerger()
|
||||
cm.read_passwd(pendpasswd, targfile=False)
|
||||
cm.read_passwd('/etc/passwd', targfile=True)
|
||||
cm.write_out('/etc/passwd')
|
||||
pendgroup = os.path.join(tmpdir, 'etc/group')
|
||||
if os.path.exists(pendgroup):
|
||||
cm = CredMerger()
|
||||
cm.read_group(pendgroup, targfile=False)
|
||||
cm.read_group('/etc/group', targfile=True)
|
||||
cm.write_out('/etc/group')
|
||||
pendhosts = os.path.join(tmpdir, 'etc/hosts')
|
||||
if os.path.exists(pendhosts):
|
||||
cm = HostMerger()
|
||||
cm.read_source(pendhosts)
|
||||
cm.read_target('/etc/hosts')
|
||||
cm.write_out('/etc/hosts')
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
synchronize()
|
||||
@@ -120,6 +120,8 @@ int main(int argc, char* argv[]) {
|
||||
ws.ws_col = 100;
|
||||
ws.ws_row = 31;
|
||||
}
|
||||
if (ws.ws_col < 80) { ws.ws_col = 80; }
|
||||
if (ws.ws_row < 24) { ws.ws_col = 24; }
|
||||
ioctl(ttyf, TIOCSWINSZ, &ws);
|
||||
tcsetattr(ttyf, TCSANOW, &tty);
|
||||
ioctl(ttyf, TIOCCONS, 0);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/python2
|
||||
|
||||
__author__ = 'jjohnson2,bfinley'
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
@@ -36,6 +38,7 @@ def main(args):
|
||||
ap = argparse.ArgumentParser(description='Manage OS deployment resources')
|
||||
sp = ap.add_subparsers(dest='command')
|
||||
wiz = sp.add_parser('initialize', help='Do OS deployment preparation')
|
||||
wiz.add_argument('-a', help='Initialize SSH access by confluent to nodes for automation such as ansible playbook execution or syncfiles', action='store_true')
|
||||
wiz.add_argument('-g', help='Initialize a Genesis profile to boot systems into a rescue or staging environment', action='store_true')
|
||||
wiz.add_argument('-u', help='Pull in root user key for node deployment', action='store_true')
|
||||
wiz.add_argument('-s', help='Set up SSH CA for managing node to node ssh and known hosts', action='store_true')
|
||||
@@ -50,7 +53,11 @@ def main(args):
|
||||
'updateboot',
|
||||
help='Push profile.yaml of the named profile data into boot assets as appropriate')
|
||||
upb.add_argument('profile', help='Profile to update boot assets')
|
||||
osls = sp.add_parser('list', help='List OS images available for deployment')
|
||||
cmdset = ap.parse_args()
|
||||
|
||||
if cmdset.command == 'list':
|
||||
return oslist()
|
||||
if cmdset.command == 'import':
|
||||
return osimport(cmdset.imagefile)
|
||||
if cmdset.command == 'initialize':
|
||||
@@ -94,6 +101,8 @@ def initialize_genesis():
|
||||
'/var/lib/confluent/public/os/genesis-x86_64/boot/initramfs/addons.cpio')
|
||||
os.symlink('/opt/confluent/genesis/x86_64/boot/kernel',
|
||||
'/var/lib/confluent/public/os/genesis-x86_64/boot/kernel')
|
||||
shutil.copytree('/opt/confluent/lib/osdeploy/genesis/profiles/default/ansible/',
|
||||
'/var/lib/confluent/public/os/genesis-x86_64/ansible/')
|
||||
shutil.copytree('/opt/confluent/lib/osdeploy/genesis/profiles/default/scripts/',
|
||||
'/var/lib/confluent/public/os/genesis-x86_64/scripts/')
|
||||
shutil.copyfile('/opt/confluent/lib/osdeploy/genesis/profiles/default/profile.yaml',
|
||||
@@ -200,6 +209,8 @@ def initialize(cmdset):
|
||||
cmdset.l = input().strip().lower().startswith('y')
|
||||
sys.stdout.write('Update tftp directory with binaries to support PXE (-p) (y/n): ')
|
||||
cmdset.p = input().strip().lower().startswith('y')
|
||||
sys.stdout.write('Initialize confluent ssh user key so confluent can execute remote automation (e.g. Ansible plays) (-a) (y/n): ')
|
||||
cmdset.a = input().strip().lower().startswith('y')
|
||||
sys.stdout.write('Generate new TLS certificates for HTTP, replacing any existing certificate (-t)? (y/n): ')
|
||||
cmdset.t = input().strip().lower().startswith('y')
|
||||
if not cmdset.t:
|
||||
@@ -235,6 +246,9 @@ def initialize(cmdset):
|
||||
if cmdset.s:
|
||||
didsomething = True
|
||||
sshutil.initialize_ca()
|
||||
if cmdset.a:
|
||||
didsomething = True
|
||||
sshutil.initialize_root_key(True, True)
|
||||
if cmdset.p:
|
||||
install_tftp_content()
|
||||
if cmdset.l:
|
||||
@@ -308,9 +322,36 @@ def updateboot(profilename):
|
||||
print(repr(rsp))
|
||||
|
||||
|
||||
def oslist():
|
||||
c = client.Command()
|
||||
print("Distributions:")
|
||||
for rsp in c.read('/deployment/distributions'):
|
||||
if 'error' in rsp:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
else:
|
||||
print(" " + rsp['item']['href'].replace('/', ''))
|
||||
print("")
|
||||
|
||||
print("Profiles:")
|
||||
for rsp in c.read('/deployment/profiles'):
|
||||
if 'error' in rsp:
|
||||
sys.stderr.write(res['error'] + '\n')
|
||||
exitcode = 1
|
||||
else:
|
||||
print(" " + rsp['item']['href'].replace('/', ''))
|
||||
print("")
|
||||
|
||||
|
||||
def osimport(imagefile):
|
||||
c = client.Command()
|
||||
imagefile = os.path.abspath(imagefile)
|
||||
if c.unixdomain:
|
||||
ofile = open(imagefile, 'rb')
|
||||
try:
|
||||
c.add_file(imagefile, ofile.fileno(), 'rb')
|
||||
except Exception:
|
||||
pass
|
||||
importing = False
|
||||
shortname = None
|
||||
for rsp in c.create('/deployment/importing/', {'filename': imagefile}):
|
||||
|
||||
@@ -72,7 +72,7 @@ def create_certificate(keyout=None, certout=None):
|
||||
if not keyout:
|
||||
raise Exception('Unable to locate TLS certificate path automatically')
|
||||
shortname = socket.gethostname().split('.')[0]
|
||||
longname = socket.getfqdn()
|
||||
longname = shortname # socket.getfqdn()
|
||||
subprocess.check_call(
|
||||
['openssl', 'ecparam', '-name', 'secp384r1', '-genkey', '-out',
|
||||
keyout])
|
||||
@@ -81,7 +81,7 @@ def create_certificate(keyout=None, certout=None):
|
||||
# there exists non-compliant clients that fail with them as IP
|
||||
san.extend(['DNS:{0}'.format(x) for x in get_ip_addresses()])
|
||||
san.append('DNS:{0}'.format(shortname))
|
||||
san.append('DNS:{0}'.format(longname))
|
||||
#san.append('DNS:{0}'.format(longname))
|
||||
san = ','.join(san)
|
||||
sslcfg = get_openssl_conf_location()
|
||||
tmpconfig = tempfile.mktemp()
|
||||
|
||||
@@ -76,8 +76,8 @@ def connect_to_leader(cert=None, name=None, leader=None):
|
||||
with cfm._initlock:
|
||||
banner = tlvdata.recv(remote) # the banner
|
||||
vers = banner.split()[2]
|
||||
if vers != b'v2':
|
||||
raise Exception('This instance only supports protocol 2, synchronize versions between collective members')
|
||||
if vers not in (b'v2', b'v3'):
|
||||
raise Exception('This instance only supports protocol 2 or 3, synchronize versions between collective members')
|
||||
tlvdata.recv(remote) # authpassed... 0..
|
||||
if name is None:
|
||||
name = get_myname()
|
||||
|
||||
@@ -1138,6 +1138,7 @@ class ConfigManager(object):
|
||||
return _cfgstore['tenant'][self.tenant]
|
||||
|
||||
def __init__(self, tenant, decrypt=False, username=None):
|
||||
self.clientfiles = {}
|
||||
global _cfgstore
|
||||
with _initlock:
|
||||
if _cfgstore is None:
|
||||
@@ -1172,6 +1173,13 @@ class ConfigManager(object):
|
||||
self._bg_sync_to_file()
|
||||
self.wait_for_sync()
|
||||
|
||||
def add_client_file(self, clientfile):
|
||||
self.clientfiles[clientfile.filename] = clientfile.fileobject
|
||||
|
||||
def close_client_files(self):
|
||||
for f in self.clientfiles:
|
||||
self.clientfiles[f].close()
|
||||
|
||||
def get_collective_member(self, name):
|
||||
return get_collective_member(name)
|
||||
|
||||
|
||||
@@ -33,42 +33,67 @@ import confluent.tlvdata as tlvdata
|
||||
import confluent.util as util
|
||||
import eventlet
|
||||
import eventlet.event
|
||||
import eventlet.green.os as os
|
||||
import eventlet.green.select as select
|
||||
import eventlet.green.socket as socket
|
||||
import eventlet.green.subprocess as subprocess
|
||||
import eventlet.green.ssl as ssl
|
||||
import pyte
|
||||
import eventlet.semaphore as semaphore
|
||||
import fcntl
|
||||
import random
|
||||
import struct
|
||||
import time
|
||||
import traceback
|
||||
|
||||
_handled_consoles = {}
|
||||
|
||||
_tracelog = None
|
||||
_bufferdaemon = None
|
||||
_bufferlock = None
|
||||
|
||||
try:
|
||||
range = xrange
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
pytecolors2ansi = {
|
||||
'black': 0,
|
||||
'red': 1,
|
||||
'green': 2,
|
||||
'brown': 3,
|
||||
'blue': 4,
|
||||
'magenta': 5,
|
||||
'cyan': 6,
|
||||
'white': 7,
|
||||
'default': 9,
|
||||
}
|
||||
# might be able to use IBMPC map from pyte charsets,
|
||||
# in that case, would have to mask out certain things (like ESC)
|
||||
# in the same way that Screen's draw method would do
|
||||
# for now at least get some of the arrows in there (note ESC is one
|
||||
# of those arrows... so skip it...
|
||||
ansichars = dict(zip((0x18, 0x19), u'\u2191\u2193'))
|
||||
|
||||
def chunk_output(output, n):
|
||||
for i in range(0, len(output), n):
|
||||
yield output[i:i + n]
|
||||
|
||||
def get_buffer_output(nodename):
|
||||
out = _bufferdaemon.stdin
|
||||
instream = _bufferdaemon.stdout
|
||||
if not isinstance(nodename, bytes):
|
||||
nodename = nodename.encode('utf8')
|
||||
outdata = b''
|
||||
with _bufferlock:
|
||||
out.write(struct.pack('I', len(nodename)))
|
||||
out.write(nodename)
|
||||
out.flush()
|
||||
select.select((instream,), (), (), 30)
|
||||
while not outdata or outdata[-1]:
|
||||
chunk = instream.read(128)
|
||||
if chunk:
|
||||
outdata += chunk
|
||||
else:
|
||||
select.select((instream,), (), (), 0)
|
||||
return outdata[:-1]
|
||||
|
||||
|
||||
def _utf8_normalize(data, shiftin, decoder):
|
||||
def send_output(nodename, output):
|
||||
if not isinstance(nodename, bytes):
|
||||
nodename = nodename.encode('utf8')
|
||||
with _bufferlock:
|
||||
_bufferdaemon.stdin.write(struct.pack('I', len(nodename) | (1 << 29)))
|
||||
_bufferdaemon.stdin.write(nodename)
|
||||
_bufferdaemon.stdin.flush()
|
||||
for chunk in chunk_output(output, 8192):
|
||||
_bufferdaemon.stdin.write(struct.pack('I', len(chunk) | (2 << 29)))
|
||||
_bufferdaemon.stdin.write(chunk)
|
||||
_bufferdaemon.stdin.flush()
|
||||
|
||||
def _utf8_normalize(data, decoder):
|
||||
# first we give the stateful decoder a crack at the byte stream,
|
||||
# we may come up empty in the event of a partial multibyte
|
||||
try:
|
||||
@@ -88,60 +113,9 @@ def _utf8_normalize(data, shiftin, decoder):
|
||||
# Finally, the low part of ascii is valid utf-8, but we are going to be
|
||||
# more interested in the cp437 versions (since this is console *output*
|
||||
# not input
|
||||
if shiftin is None:
|
||||
data = data.translate(ansichars)
|
||||
return data.encode('utf-8')
|
||||
|
||||
|
||||
def pytechars2line(chars, maxlen=None):
|
||||
line = b'\x1b[m' # start at default params
|
||||
lb = False # last bold
|
||||
li = False # last italic
|
||||
lu = False # last underline
|
||||
ls = False # last strikethrough
|
||||
lr = False # last reverse
|
||||
lfg = 'default' # last fg color
|
||||
lbg = 'default' # last bg color
|
||||
hasdata = False
|
||||
len = 1
|
||||
for charidx in range(maxlen):
|
||||
char = chars[charidx]
|
||||
csi = bytearray([])
|
||||
if char.fg != lfg:
|
||||
csi.append(30 + pytecolors2ansi.get(char.fg, 9))
|
||||
lfg = char.fg
|
||||
if char.bg != lbg:
|
||||
csi.append(40 + pytecolors2ansi.get(char.bg, 9))
|
||||
lbg = char.bg
|
||||
if char.bold != lb:
|
||||
lb = char.bold
|
||||
csi.append(1 if lb else 22)
|
||||
if char.italics != li:
|
||||
li = char.italics
|
||||
csi.append(3 if li else 23)
|
||||
if char.underscore != lu:
|
||||
lu = char.underscore
|
||||
csi.append(4 if lu else 24)
|
||||
if char.strikethrough != ls:
|
||||
ls = char.strikethrough
|
||||
csi.append(9 if ls else 29)
|
||||
if char.reverse != lr:
|
||||
lr = char.reverse
|
||||
csi.append(7 if lr else 27)
|
||||
if csi:
|
||||
line += b'\x1b[' + b';'.join(['{0}'.format(x).encode('utf-8') for x in csi]) + b'm'
|
||||
if not hasdata and char.data.rstrip():
|
||||
hasdata = True
|
||||
chardata = char.data
|
||||
if not isinstance(chardata, bytes):
|
||||
chardata = chardata.encode('utf-8')
|
||||
line += chardata
|
||||
if maxlen and len >= maxlen:
|
||||
break
|
||||
len += 1
|
||||
return line, hasdata
|
||||
|
||||
|
||||
class ConsoleHandler(object):
|
||||
_plugin_path = '/nodes/{0}/_console/session'
|
||||
_logtobuffer = True
|
||||
@@ -161,15 +135,15 @@ class ConsoleHandler(object):
|
||||
self.node = node
|
||||
self.connectstate = 'unconnected'
|
||||
self._isalive = True
|
||||
self.buffer = pyte.Screen(100, 31)
|
||||
self.termstream = pyte.ByteStream()
|
||||
self.termstream.attach(self.buffer)
|
||||
#self.buffer = pyte.Screen(100, 31)
|
||||
#self.termstream = pyte.ByteStream()
|
||||
#self.termstream.attach(self.buffer)
|
||||
self.livesessions = set([])
|
||||
self.utf8decoder = codecs.getincrementaldecoder('utf-8')()
|
||||
if self._logtobuffer:
|
||||
self.logger = log.Logger(node, console=True,
|
||||
tenant=configmanager.tenant)
|
||||
(text, termstate, timestamp) = (b'', 0, False)
|
||||
timestamp = False
|
||||
# when reading from log file, we will use wall clock
|
||||
# it should usually match walltime.
|
||||
self.lasttime = 0
|
||||
@@ -182,13 +156,7 @@ class ConsoleHandler(object):
|
||||
# guess
|
||||
self.lasttime = util.monotonic_time()
|
||||
self.clearbuffer()
|
||||
self.appmodedetected = False
|
||||
self.shiftin = None
|
||||
self.reconnect = None
|
||||
if termstate & 1:
|
||||
self.appmodedetected = True
|
||||
if termstate & 2:
|
||||
self.shiftin = b'0'
|
||||
self.users = {}
|
||||
self._attribwatcher = None
|
||||
self._console = None
|
||||
@@ -216,10 +184,7 @@ class ConsoleHandler(object):
|
||||
if not isinstance(data, bytes):
|
||||
data = data.encode('utf-8')
|
||||
try:
|
||||
self.termstream.feed(data)
|
||||
except StopIteration: # corrupt parser state, start over
|
||||
self.termstream = pyte.ByteStream()
|
||||
self.termstream.attach(self.buffer)
|
||||
send_output(self.node, data)
|
||||
except Exception:
|
||||
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
|
||||
event=log.Events.stacktrace)
|
||||
@@ -536,19 +501,7 @@ class ConsoleHandler(object):
|
||||
return
|
||||
if not isinstance(data, bytes):
|
||||
data = data.encode('utf-8')
|
||||
if b'\x1b[?1l' in data: # request for ansi mode cursor keys
|
||||
self.appmodedetected = False
|
||||
if b'\x1b[?1h' in data: # remember the session wants the client to use
|
||||
# 'application mode' Thus far only observed on esxi
|
||||
self.appmodedetected = True
|
||||
if b'\x1b)0' in data:
|
||||
# console indicates it wants access to special drawing characters
|
||||
self.shiftin = b'0'
|
||||
eventdata = 0
|
||||
if self.appmodedetected:
|
||||
eventdata |= 1
|
||||
if self.shiftin is not None:
|
||||
eventdata |= 2
|
||||
# TODO: analyze buffer for registered events, examples:
|
||||
# panics
|
||||
# certificate signing request
|
||||
@@ -557,7 +510,7 @@ class ConsoleHandler(object):
|
||||
self.clearerror = False
|
||||
self.feedbuffer(b'\x1bc\x1b[2J\x1b[1;1H')
|
||||
self._send_rcpts(b'\x1bc\x1b[2J\x1b[1;1H')
|
||||
self._send_rcpts(_utf8_normalize(data, self.shiftin, self.utf8decoder))
|
||||
self._send_rcpts(_utf8_normalize(data, self.utf8decoder))
|
||||
self.log(data, eventdata=eventdata)
|
||||
self.lasttime = util.monotonic_time()
|
||||
self.feedbuffer(data)
|
||||
@@ -583,36 +536,7 @@ class ConsoleHandler(object):
|
||||
'connectstate': self.connectstate,
|
||||
'clientcount': len(self.livesessions),
|
||||
}
|
||||
retdata = b'\x1b[H\x1b[J' # clear screen
|
||||
pendingbl = b'' # pending blank lines
|
||||
maxlen = 0
|
||||
for line in self.buffer.display:
|
||||
line = line.rstrip()
|
||||
if len(line) > maxlen:
|
||||
maxlen = len(line)
|
||||
for line in range(self.buffer.lines):
|
||||
nline, notblank = pytechars2line(self.buffer.buffer[line], maxlen)
|
||||
if notblank:
|
||||
if pendingbl:
|
||||
retdata += pendingbl
|
||||
pendingbl = b''
|
||||
retdata += nline + b'\r\n'
|
||||
else:
|
||||
pendingbl += nline + b'\r\n'
|
||||
if len(retdata) > 6:
|
||||
retdata = retdata[:-2] # remove the last \r\n
|
||||
cursordata = '\x1b[{0};{1}H'.format(self.buffer.cursor.y + 1,
|
||||
self.buffer.cursor.x + 1)
|
||||
if not isinstance(cursordata, bytes):
|
||||
cursordata = cursordata.encode('utf-8')
|
||||
retdata += cursordata
|
||||
if self.shiftin is not None: # detected that terminal requested a
|
||||
# shiftin character set, relay that to the terminal that cannected
|
||||
retdata += b'\x1b)' + self.shiftin
|
||||
#if self.appmodedetected:
|
||||
# retdata += b'\x1b[?1h'
|
||||
#else:
|
||||
# retdata += b'\x1b[?1l'
|
||||
retdata = get_buffer_output(self.node)
|
||||
return retdata, connstate
|
||||
|
||||
def write(self, data):
|
||||
@@ -661,7 +585,16 @@ def _start_tenant_sessions(cfm):
|
||||
|
||||
def start_console_sessions():
|
||||
global _tracelog
|
||||
global _bufferdaemon
|
||||
global _bufferlock
|
||||
_bufferlock = semaphore.Semaphore()
|
||||
_tracelog = log.Logger('trace')
|
||||
_bufferdaemon = subprocess.Popen(
|
||||
['/opt/confluent/bin/vtbufferd'], stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE)
|
||||
fl = fcntl.fcntl(_bufferdaemon.stdout.fileno(), fcntl.F_GETFL)
|
||||
fcntl.fcntl(_bufferdaemon.stdout.fileno(),
|
||||
fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
||||
configmodule.hook_new_configmanagers(_start_tenant_sessions)
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,8 @@ def handle_deployment(configmanager, inputdata, pathcomponents,
|
||||
yield imp
|
||||
return
|
||||
elif operation == 'create':
|
||||
importer = osimage.MediaImporter(inputdata['filename'])
|
||||
importer = osimage.MediaImporter(inputdata['filename'],
|
||||
configmanager)
|
||||
yield msg.KeyValueData({'target': importer.targpath,
|
||||
'name': importer.importkey})
|
||||
return
|
||||
|
||||
@@ -494,6 +494,7 @@ def check_reply(node, info, packet, sock, cfg, reqview):
|
||||
log.log({'error': nicerr})
|
||||
if niccfg.get('ipv4_broken', False):
|
||||
# Received a request over a nic with no ipv4 configured, ignore it
|
||||
log.log({'error': 'Skipping boot reply to {0} due to no viable IPv4 configuration on deployment system'.format(node)})
|
||||
return
|
||||
clipn = None
|
||||
if niccfg['ipv4_address'] and niccfg['ipv4_method'] != 'firmwaredhcp':
|
||||
@@ -557,6 +558,16 @@ def check_reply(node, info, packet, sock, cfg, reqview):
|
||||
staticassigns[fulladdr] = (clipn, repview[:replen + 28].tobytes())
|
||||
elif fulladdr in staticassigns:
|
||||
del staticassigns[fulladdr]
|
||||
if httpboot:
|
||||
boottype = 'HTTP'
|
||||
else:
|
||||
boottype = 'PXE'
|
||||
if clipn:
|
||||
ipinfo = 'with static address {0}'.format(niccfg['ipv4_address'])
|
||||
else:
|
||||
ipinfo = 'without address'
|
||||
log.log({
|
||||
'info': 'Offering {0} boot {1} to {2}'.format(boottype, ipinfo, node)})
|
||||
send_raw_packet(repview, replen + 28, reqview, info)
|
||||
|
||||
def send_raw_packet(repview, replen, reqview, info):
|
||||
|
||||
@@ -201,7 +201,14 @@ def _find_srvtype(net, net4, srvtype, addresses, xid):
|
||||
:param addresses: Pass through of addresses argument from find_targets
|
||||
:return:
|
||||
"""
|
||||
if addresses is None:
|
||||
if addresses is not None:
|
||||
for addr in addresses:
|
||||
for saddr in socket.getaddrinfo(addr, 427):
|
||||
if saddr[0] == socket.AF_INET:
|
||||
net4.sendto(data, saddr[4])
|
||||
elif saddr[0] == socket.AF_INET6:
|
||||
net.sendto(data, saddr[4])
|
||||
else:
|
||||
data = _generate_request_payload(srvtype, True, xid)
|
||||
net4.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
v6addrs = []
|
||||
|
||||
@@ -355,10 +355,6 @@ def _parse_ssdp(peer, rsp, peerdata):
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
for rsp in scan(['urn:dmtf-org:service:redfish-rest:1']):
|
||||
for rsp in scan(['urn:dmtf-org:service:redfish-rest:1'], '10.240.52.189'):
|
||||
print(repr(rsp))
|
||||
def fun(a):
|
||||
print(repr(a))
|
||||
def byefun(a):
|
||||
print('bye' + repr(a))
|
||||
snoop(fun, byefun)
|
||||
|
||||
|
||||
@@ -32,11 +32,12 @@ uploadsbytarget = {}
|
||||
downloadsbytarget = {}
|
||||
updatepool = eventlet.greenpool.GreenPool(256)
|
||||
_tracelog = None
|
||||
filecontentbyname = {}
|
||||
|
||||
|
||||
def execupdate(handler, filename, updateobj, type, owner, node):
|
||||
def execupdate(handler, filename, updateobj, type, owner, node, datfile):
|
||||
global _tracelog
|
||||
if type != 'ffdc':
|
||||
if type != 'ffdc' and not datfile:
|
||||
errstr = False
|
||||
if not os.path.exists(filename):
|
||||
errstr = '{0} does not appear to exist on {1}, or is in a directory with permissions forbidding confluent user/group access'.format(
|
||||
@@ -64,9 +65,10 @@ def execupdate(handler, filename, updateobj, type, owner, node):
|
||||
try:
|
||||
if type == 'firmware':
|
||||
completion = handler(filename, progress=updateobj.handle_progress,
|
||||
bank=updateobj.bank)
|
||||
data=datfile, bank=updateobj.bank)
|
||||
else:
|
||||
completion = handler(filename, progress=updateobj.handle_progress)
|
||||
completion = handler(filename, progress=updateobj.handle_progress,
|
||||
data=datfile)
|
||||
if type == 'ffdc' and completion:
|
||||
filename = completion
|
||||
completion = None
|
||||
@@ -90,14 +92,20 @@ def execupdate(handler, filename, updateobj, type, owner, node):
|
||||
|
||||
class Updater(object):
|
||||
def __init__(self, node, handler, filename, tenant=None, name=None,
|
||||
bank=None, type='firmware', owner=None):
|
||||
bank=None, type='firmware', owner=None, configmanager=None):
|
||||
self.bank = bank
|
||||
self.node = node
|
||||
self.phase = 'initializing'
|
||||
self.detail = ''
|
||||
self.percent = 0.0
|
||||
if configmanager and filename in configmanager.clientfiles:
|
||||
cf = configmanager.clientfiles[filename]
|
||||
datfile = os.fdopen(os.dup(cf.fileno()), cf.mode)
|
||||
else:
|
||||
datfile = None
|
||||
self.datfile = datfile
|
||||
self.updateproc = updatepool.spawn(execupdate, handler, filename,
|
||||
self, type, owner, node)
|
||||
self, type, owner, node, datfile)
|
||||
if type == 'firmware':
|
||||
myparty = updatesbytarget
|
||||
elif type == 'mediaupload':
|
||||
@@ -120,6 +128,8 @@ class Updater(object):
|
||||
|
||||
def cancel(self):
|
||||
self.updateproc.kill()
|
||||
if self.datfile:
|
||||
self.datfile.close()
|
||||
|
||||
@property
|
||||
def progress(self):
|
||||
|
||||
@@ -1054,7 +1054,7 @@ class InputNetworkConfiguration(ConfluentInputMessage):
|
||||
if 'ipv4_gateway' not in inputdata:
|
||||
inputdata['ipv4_gateway'] = None
|
||||
|
||||
if 'ipv4_configuration' in inputdata:
|
||||
if 'ipv4_configuration' in inputdata and inputdata['ipv4_configuration']:
|
||||
if inputdata['ipv4_configuration'].lower() not in ['dhcp','static']:
|
||||
raise exc.InvalidArgumentException(
|
||||
'Unrecognized ipv4_configuration')
|
||||
|
||||
@@ -216,17 +216,46 @@ def extract_entries(entries, flags=0, callback=None, totalsize=None, extractlist
|
||||
os.chmod(str(entry), 0o644)
|
||||
if callback:
|
||||
callback({'progress': float(sizedone) / float(totalsize)})
|
||||
return float(sizedone) / float(totalsize)
|
||||
|
||||
|
||||
def extract_file(filepath, flags=0, callback=lambda x: None, imginfo=(), extractlist=None):
|
||||
def extract_file(archfile, flags=0, callback=lambda x: None, imginfo=(), extractlist=None):
|
||||
"""Extracts an archive from a file into the current directory."""
|
||||
totalsize = 0
|
||||
for img in imginfo:
|
||||
if not imginfo[img]:
|
||||
continue
|
||||
totalsize += imginfo[img]
|
||||
with libarchive.file_reader(filepath) as archive:
|
||||
extract_entries(archive, flags, callback, totalsize, extractlist)
|
||||
dfd = os.dup(archfile.fileno())
|
||||
os.lseek(dfd, 0, 0)
|
||||
pctdone = 0
|
||||
try:
|
||||
with libarchive.fd_reader(dfd) as archive:
|
||||
pctdone = extract_entries(archive, flags, callback, totalsize,
|
||||
extractlist)
|
||||
finally:
|
||||
os.close(dfd)
|
||||
return pctdone
|
||||
|
||||
|
||||
def check_alma(isoinfo):
|
||||
ver = None
|
||||
arch = None
|
||||
cat = None
|
||||
for entry in isoinfo[0]:
|
||||
if 'almalinux-release-8' in entry:
|
||||
ver = entry.split('-')[2]
|
||||
arch = entry.split('.')[-2]
|
||||
cat = 'el8'
|
||||
break
|
||||
else:
|
||||
return None
|
||||
if arch == 'noarch' and '.discinfo' in isoinfo[1]:
|
||||
prodinfo = isoinfo[1]['.discinfo']
|
||||
arch = prodinfo.split(b'\n')[2]
|
||||
if not isinstance(arch, str):
|
||||
arch = arch.decode('utf-8')
|
||||
return {'name': 'alma-{0}-{1}'.format(ver, arch), 'method': EXTRACT, 'category': cat}
|
||||
|
||||
|
||||
def check_centos(isoinfo):
|
||||
@@ -420,51 +449,60 @@ def check_rhel(isoinfo):
|
||||
return {'name': 'rhel-{0}-{1}'.format(ver, arch), 'method': EXTRACT, 'category': 'el{0}'.format(major)}
|
||||
|
||||
|
||||
def scan_iso(filename):
|
||||
def scan_iso(archive):
|
||||
filesizes = {}
|
||||
filecontents = {}
|
||||
with libarchive.file_reader(filename) as reader:
|
||||
for ent in reader:
|
||||
if str(ent).endswith('TRANS.TBL'):
|
||||
continue
|
||||
eventlet.sleep(0)
|
||||
filesizes[str(ent)] = ent.size
|
||||
if str(ent) in READFILES:
|
||||
filecontents[str(ent)] = b''
|
||||
for block in ent.get_blocks():
|
||||
filecontents[str(ent)] += bytes(block)
|
||||
dfd = os.dup(archive.fileno())
|
||||
os.lseek(dfd, 0, 0)
|
||||
try:
|
||||
with libarchive.fd_reader(dfd) as reader:
|
||||
for ent in reader:
|
||||
if str(ent).endswith('TRANS.TBL'):
|
||||
continue
|
||||
eventlet.sleep(0)
|
||||
filesizes[str(ent)] = ent.size
|
||||
if str(ent) in READFILES:
|
||||
filecontents[str(ent)] = b''
|
||||
for block in ent.get_blocks():
|
||||
filecontents[str(ent)] += bytes(block)
|
||||
finally:
|
||||
os.close(dfd)
|
||||
return filesizes, filecontents
|
||||
|
||||
|
||||
def fingerprint(filename):
|
||||
with open(filename, 'rb') as archive:
|
||||
header = archive.read(32768)
|
||||
archive.seek(32769)
|
||||
if archive.read(6) == b'CD001\x01':
|
||||
# ISO image
|
||||
isoinfo = scan_iso(filename)
|
||||
name = None
|
||||
for fun in globals():
|
||||
if fun.startswith('check_'):
|
||||
name = globals()[fun](isoinfo)
|
||||
if name:
|
||||
return name, isoinfo[0]
|
||||
return None
|
||||
else:
|
||||
sum = hashlib.sha256(header)
|
||||
if sum.digest() in HEADERSUMS:
|
||||
archive.seek(32768)
|
||||
def fingerprint(archive):
|
||||
archive.seek(0)
|
||||
header = archive.read(32768)
|
||||
archive.seek(32769)
|
||||
if archive.read(6) == b'CD001\x01':
|
||||
# ISO image
|
||||
isoinfo = scan_iso(archive)
|
||||
name = None
|
||||
for fun in globals():
|
||||
if fun.startswith('check_'):
|
||||
name = globals()[fun](isoinfo)
|
||||
if name:
|
||||
return name, isoinfo[0]
|
||||
return None
|
||||
else:
|
||||
sum = hashlib.sha256(header)
|
||||
if sum.digest() in HEADERSUMS:
|
||||
archive.seek(32768)
|
||||
chunk = archive.read(32768)
|
||||
while chunk:
|
||||
sum.update(chunk)
|
||||
chunk = archive.read(32768)
|
||||
while chunk:
|
||||
sum.update(chunk)
|
||||
chunk = archive.read(32768)
|
||||
imginfo = HASHPRINTS.get(sum.hexdigest(), None)
|
||||
if imginfo:
|
||||
return imginfo, None
|
||||
imginfo = HASHPRINTS.get(sum.hexdigest(), None)
|
||||
if imginfo:
|
||||
return imginfo, None
|
||||
|
||||
|
||||
def import_image(filename, callback, backend=False):
|
||||
identity = fingerprint(filename)
|
||||
def import_image(filename, callback, backend=False, mfd=None):
|
||||
if mfd:
|
||||
archive = os.fdopen(int(mfd), 'rb')
|
||||
else:
|
||||
archive = open(filename, 'rb')
|
||||
identity = fingerprint(archive)
|
||||
if not identity:
|
||||
return -1
|
||||
identity, imginfo = identity
|
||||
@@ -478,20 +516,37 @@ def import_image(filename, callback, backend=False):
|
||||
os.chdir(targpath)
|
||||
if not backend:
|
||||
print('Importing OS to ' + targpath + ':')
|
||||
printit({'progress': 0.0})
|
||||
callback({'progress': 0.0})
|
||||
pct = 0.0
|
||||
if EXTRACT & identity['method']:
|
||||
extract_file(filename, callback=callback, imginfo=imginfo, extractlist=identity.get('extractlist', None))
|
||||
pct = extract_file(archive, callback=callback, imginfo=imginfo,
|
||||
extractlist=identity.get('extractlist', None))
|
||||
if COPY & identity['method']:
|
||||
basename = identity.get('copyto', os.path.basename(filename))
|
||||
targpath = os.path.join(targpath, basename)
|
||||
shutil.copyfile(filename, targpath)
|
||||
targiso = os.path.join(targpath, basename)
|
||||
archive.seek(0, 2)
|
||||
totalsz = archive.tell()
|
||||
currsz = 0
|
||||
modpct = 1.0 - pct
|
||||
archive.seek(0, 0)
|
||||
printat = 0
|
||||
with open(targiso, 'wb') as targ:
|
||||
buf = archive.read(32768)
|
||||
while buf:
|
||||
currsz += len(buf)
|
||||
pgress = pct + ((float(currsz) / float(totalsz)) * modpct)
|
||||
if time.time() > printat:
|
||||
callback({'progress': pgress})
|
||||
printat = time.time() + 0.5
|
||||
targ.write(buf)
|
||||
buf = archive.read(32768)
|
||||
with open(targpath + '/distinfo.yaml', 'w') as distinfo:
|
||||
distinfo.write(yaml.dump(identity, default_flow_style=False))
|
||||
if 'subname' in identity:
|
||||
del identity['subname']
|
||||
with open(distpath + '/distinfo.yaml', 'w') as distinfo:
|
||||
distinfo.write(yaml.dump(identity, default_flow_style=False))
|
||||
printit({'progress': 1.0})
|
||||
callback({'progress': 1.0})
|
||||
sys.stdout.write('\n')
|
||||
|
||||
def printit(info):
|
||||
@@ -561,10 +616,15 @@ def generate_stock_profiles(defprofile, distpath, targpath, osname,
|
||||
|
||||
class MediaImporter(object):
|
||||
|
||||
def __init__(self, media):
|
||||
def __init__(self, media, cfm=None):
|
||||
self.worker = None
|
||||
self.profiles = []
|
||||
identity = fingerprint(media)
|
||||
medfile = None
|
||||
if cfm and media in cfm.clientfiles:
|
||||
medfile = cfm.clientfiles[media]
|
||||
else:
|
||||
medfile = open(media, 'rb')
|
||||
identity = fingerprint(medfile)
|
||||
if not identity:
|
||||
raise exc.InvalidArgumentException('Unsupported Media')
|
||||
self.percent = 0.0
|
||||
@@ -591,6 +651,7 @@ class MediaImporter(object):
|
||||
if os.path.exists(self.targpath):
|
||||
raise Exception('{0} already exists'.format(self.targpath))
|
||||
self.filename = os.path.abspath(media)
|
||||
self.medfile = medfile
|
||||
self.importer = eventlet.spawn(self.importmedia)
|
||||
|
||||
def stop(self):
|
||||
@@ -603,10 +664,11 @@ class MediaImporter(object):
|
||||
|
||||
def importmedia(self):
|
||||
os.environ['PYTHONPATH'] = ':'.join(sys.path)
|
||||
os.environ['CONFLUENT_MEDIAFD'] = '{0}'.format(self.medfile.fileno())
|
||||
with open(os.devnull, 'w') as devnull:
|
||||
self.worker = subprocess.Popen(
|
||||
[sys.executable, __file__, self.filename, '-b'],
|
||||
stdin=devnull, stdout=subprocess.PIPE)
|
||||
stdin=devnull, stdout=subprocess.PIPE, close_fds=False)
|
||||
wkr = self.worker
|
||||
currline = b''
|
||||
while wkr.poll() is None:
|
||||
@@ -651,6 +713,7 @@ def get_importing_status(importkey):
|
||||
if __name__ == '__main__':
|
||||
os.umask(0o022)
|
||||
if len(sys.argv) > 2:
|
||||
sys.exit(import_image(sys.argv[1], callback=printit, backend=True))
|
||||
mfd = os.environ.get('CONFLUENT_MEDIAFD', None)
|
||||
sys.exit(import_image(sys.argv[1], callback=printit, backend=True, mfd=mfd))
|
||||
else:
|
||||
sys.exit(import_image(sys.argv[1], callback=printit))
|
||||
|
||||
@@ -87,7 +87,7 @@ def retrieve_nodegroup(nodegroup, element, configmanager, inputdata):
|
||||
desc = ''
|
||||
if 'value' in currattr or 'expression' in currattr:
|
||||
yield msg.Attributes(kv={attribute: currattr}, desc=desc)
|
||||
elif 'cryptvalue' in currattr:
|
||||
elif 'cryptvalue' in currattr or 'hashvalue' in currattr:
|
||||
yield msg.CryptedAttributes(
|
||||
kv={attribute: currattr},
|
||||
desc=desc)
|
||||
|
||||
@@ -24,11 +24,8 @@ def update(nodes, element, configmanager, inputdata):
|
||||
em = emebs[node]['enclosure.manager']['value']
|
||||
eb = emebs[node]['enclosure.bay']['value']
|
||||
except KeyError:
|
||||
yield msg.ConfluentNodeError(
|
||||
node,
|
||||
'Reseat is only supported on servers in an enclosure, and '
|
||||
'with enclosure.manager and enclosure.bay defined')
|
||||
continue
|
||||
em = node
|
||||
eb = -1
|
||||
try:
|
||||
for rsp in core.handle_path(
|
||||
'/nodes/{0}/_enclosure/reseat_bay'.format(em),
|
||||
|
||||
@@ -462,6 +462,7 @@ persistent_ipmicmds = {}
|
||||
class IpmiHandler(object):
|
||||
def __init__(self, operation, node, element, cfd, inputdata, cfg, output,
|
||||
realop):
|
||||
self.cfm = cfg
|
||||
self.invmap = {}
|
||||
self.output = output
|
||||
self.sensorcategory = None
|
||||
@@ -589,7 +590,7 @@ class IpmiHandler(object):
|
||||
def handle_update(self):
|
||||
u = firmwaremanager.Updater(self.node, self.ipmicmd.update_firmware,
|
||||
self.inputdata.nodefile(self.node), self.tenant,
|
||||
bank=self.inputdata.bank)
|
||||
bank=self.inputdata.bank, configmanager=self.cfm)
|
||||
self.output.put(
|
||||
msg.CreatedResource(
|
||||
'nodes/{0}/inventory/firmware/updates/active/{1}'.format(
|
||||
@@ -598,11 +599,11 @@ class IpmiHandler(object):
|
||||
def handle_media_upload(self):
|
||||
u = firmwaremanager.Updater(self.node, self.ipmicmd.upload_media,
|
||||
self.inputdata.nodefile(self.node), self.tenant,
|
||||
type='mediaupload')
|
||||
type='mediaupload', configmanager=self.cfm)
|
||||
self.output.put(msg.CreatedResource(
|
||||
'nodes/{0}/media/uploads/{1}'.format(self.node, u.name)))
|
||||
|
||||
def get_diags(self, savefile, progress):
|
||||
def get_diags(self, savefile, progress, data=None):
|
||||
return self.ipmicmd.get_diagnostic_data(
|
||||
savefile, progress=progress, autosuffix=True)
|
||||
|
||||
@@ -1524,14 +1525,21 @@ class IpmiHandler(object):
|
||||
self.element = self.element[:-1]
|
||||
if self.op in ('create', 'update'):
|
||||
filename = self.inputdata.nodefile(self.node)
|
||||
if not os.access(filename, os.R_OK):
|
||||
datfile = None
|
||||
if filename in self.cfm.clientfiles:
|
||||
cf = self.cfm.clientfiles[filename]
|
||||
datfile = os.fdopen(os.dup(cf.fileno()), cf.mode)
|
||||
if datfile is None and not os.access(filename, os.R_OK):
|
||||
errstr = ('{0} is not readable by confluent on {1} '
|
||||
'(ensure confluent user or group can access file '
|
||||
'and parent directories)').format(
|
||||
filename, socket.gethostname())
|
||||
self.output.put(msg.ConfluentNodeError(self.node, errstr))
|
||||
return
|
||||
self.ipmicmd.apply_license(filename)
|
||||
try:
|
||||
self.ipmicmd.apply_license(filename, data=datfile)
|
||||
finally:
|
||||
datfile.close()
|
||||
if len(self.element) == 3:
|
||||
self.output.put(msg.ChildCollection('all'))
|
||||
i = 1
|
||||
|
||||
@@ -348,6 +348,7 @@ persistent_ipmicmds = {}
|
||||
class IpmiHandler(object):
|
||||
def __init__(self, operation, node, element, cfd, inputdata, cfg, output,
|
||||
realop):
|
||||
self.cfm = cfg
|
||||
self.sensormap = {}
|
||||
self.invmap = {}
|
||||
self.output = output
|
||||
@@ -453,7 +454,8 @@ class IpmiHandler(object):
|
||||
def handle_update(self):
|
||||
u = firmwaremanager.Updater(self.node, self.ipmicmd.update_firmware,
|
||||
self.inputdata.nodefile(self.node), self.tenant,
|
||||
bank=self.inputdata.bank)
|
||||
bank=self.inputdata.bank,
|
||||
configmanager=self.cfm)
|
||||
self.output.put(
|
||||
msg.CreatedResource(
|
||||
'nodes/{0}/inventory/firmware/updates/active/{1}'.format(
|
||||
@@ -462,11 +464,11 @@ class IpmiHandler(object):
|
||||
def handle_media_upload(self):
|
||||
u = firmwaremanager.Updater(self.node, self.ipmicmd.upload_media,
|
||||
self.inputdata.nodefile(self.node), self.tenant,
|
||||
type='mediaupload')
|
||||
type='mediaupload', configmanager=self.cfm)
|
||||
self.output.put(msg.CreatedResource(
|
||||
'nodes/{0}/media/uploads/{1}'.format(self.node, u.name)))
|
||||
|
||||
def get_diags(self, savefile, progress):
|
||||
def get_diags(self, savefile, progress, data=None):
|
||||
return self.ipmicmd.get_diagnostic_data(
|
||||
savefile, progress=progress, autosuffix=True)
|
||||
|
||||
@@ -1368,14 +1370,21 @@ class IpmiHandler(object):
|
||||
self.element = self.element[:-1]
|
||||
if self.op in ('create', 'update'):
|
||||
filename = self.inputdata.nodefile(self.node)
|
||||
if not os.access(filename, os.R_OK):
|
||||
datfile = None
|
||||
if filename in self.cfm.clientfiles:
|
||||
cf = self.cfm.clientfiles[filename]
|
||||
datfile = os.fdopen(os.dup(cf.fileno()), cf.mode)
|
||||
if datfile is None and not os.access(filename, os.R_OK):
|
||||
errstr = ('{0} is not readable by confluent on {1} '
|
||||
'(ensure confluent user or group can access file '
|
||||
'and parent directories)').format(
|
||||
filename, socket.gethostname())
|
||||
self.output.put(msg.ConfluentNodeError(self.node, errstr))
|
||||
return
|
||||
self.ipmicmd.apply_license(self.inputdata.nodefile(self.node))
|
||||
try:
|
||||
self.ipmicmd.apply_license(filename, data=datfile)
|
||||
finally:
|
||||
datfile.close()
|
||||
if len(self.element) == 3:
|
||||
self.output.put(msg.ChildCollection('all'))
|
||||
i = 1
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2021 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.
|
||||
|
||||
try:
|
||||
import confluent.sshutil as sshutil
|
||||
except ImportError:
|
||||
pass
|
||||
import eventlet
|
||||
import eventlet.green.subprocess as subprocess
|
||||
import json
|
||||
import msgpack
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
running_status = {}
|
||||
class PlayRunner(object):
|
||||
def __init__(self, playfiles, nodes):
|
||||
self.playfiles = playfiles
|
||||
self.nodes = nodes
|
||||
self.worker = None
|
||||
self.results = []
|
||||
self.complete = False
|
||||
|
||||
def _start_playbooks(self):
|
||||
self.worker = eventlet.spawn(self._really_run_playbooks)
|
||||
|
||||
def get_available_results(self):
|
||||
avail = self.results
|
||||
self.results = []
|
||||
return avail
|
||||
|
||||
def dump_text(self):
|
||||
retinfo = self.dump_dict()
|
||||
textout = ''
|
||||
for result in retinfo['results']:
|
||||
textout += 'TASK [{}] *******************************\n'.format(
|
||||
result['task_name'])
|
||||
for warning in result['warnings']:
|
||||
textout += '[WARNING]: ' + warning + '\n'
|
||||
if 'errorinfo' in result:
|
||||
textout += '{} => {}\n'.format(result['state'],
|
||||
result['errorinfo'])
|
||||
else:
|
||||
if result['changed']:
|
||||
textout += 'changed\n'
|
||||
else:
|
||||
textout += result['state'] + '\n'
|
||||
textout += '\n'
|
||||
return textout
|
||||
|
||||
def dump_json(self):
|
||||
return json.dumps(self.dump_dict())
|
||||
|
||||
def dump_dict(self):
|
||||
return {
|
||||
'complete': self.complete,
|
||||
'results': self.get_available_results()
|
||||
}
|
||||
|
||||
def _really_run_playbooks(self):
|
||||
with open(os.devnull, 'w+') as devnull:
|
||||
targnodes = ','.join(self.nodes)
|
||||
for playfilename in self.playfiles:
|
||||
worker = subprocess.Popen(
|
||||
[sys.executable, __file__, targnodes, playfilename],
|
||||
stdin=devnull, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stdout, self.stderr = worker.communicate()
|
||||
current = memoryview(stdout)
|
||||
while len(current):
|
||||
sz = struct.unpack('=q', current[:8])[0]
|
||||
result = msgpack.unpackb(current[8:8+sz], raw=False)
|
||||
self.results.append(result)
|
||||
current = current[8+sz:]
|
||||
self.complete = True
|
||||
|
||||
|
||||
def run_playbooks(playfiles, nodes):
|
||||
sshutil.prep_ssh_key('/etc/confluent/ssh/automation')
|
||||
runner = PlayRunner(playfiles, nodes)
|
||||
for node in nodes:
|
||||
running_status[node] = runner
|
||||
runner._start_playbooks()
|
||||
|
||||
|
||||
def print_result(result, state, collector=None):
|
||||
output = {
|
||||
'task_name': result.task_name,
|
||||
'changed': result._result['changed'],
|
||||
}
|
||||
output['state'] = state
|
||||
output['warnings'] = result._result.get('warnings', [])
|
||||
try:
|
||||
del result._result['warnings']
|
||||
except KeyError:
|
||||
pass
|
||||
if collector:
|
||||
output['errorinfo'] = collector._dump_results(result._result)
|
||||
msg = msgpack.packb(output, use_bin_type=True)
|
||||
msglen = len(msg)
|
||||
sys.stdout.buffer.write(struct.pack('=q', msglen))
|
||||
sys.stdout.buffer.write(msg)
|
||||
|
||||
if __name__ == '__main__':
|
||||
from ansible.inventory.manager import InventoryManager
|
||||
from ansible.parsing.dataloader import DataLoader
|
||||
from ansible.executor.task_queue_manager import TaskQueueManager
|
||||
from ansible.vars.manager import VariableManager
|
||||
from ansible.playbook.play import Play
|
||||
from ansible import context
|
||||
from ansible.module_utils.common.collections import ImmutableDict
|
||||
from ansible.plugins.callback import CallbackBase
|
||||
import yaml
|
||||
|
||||
class ResultsCollector(CallbackBase):
|
||||
|
||||
def v2_runner_on_unreachable(self, result):
|
||||
print_result(result, 'UNREACHABLE', self)
|
||||
|
||||
def v2_runner_on_ok(self, result, *args, **kwargs):
|
||||
print_result(result, 'ok')
|
||||
|
||||
def v2_runner_on_failed(self, result, *args, **kwargs):
|
||||
print_result(result, 'FAILED', self)
|
||||
|
||||
context.CLIARGS = ImmutableDict(
|
||||
connection='smart', module_path=['/usr/share/ansible'], forks=10,
|
||||
become=None, become_method=None, become_user=None, check=False,
|
||||
diff=False, verbosity=0, remote_user='root')
|
||||
|
||||
|
||||
invlist = sys.argv[1] + ','
|
||||
loader = DataLoader()
|
||||
invman = InventoryManager(loader=loader, sources=invlist)
|
||||
varman = VariableManager(loader=loader, inventory=invman)
|
||||
|
||||
plays = yaml.safe_load(open(sys.argv[2]))
|
||||
if isinstance(plays, dict):
|
||||
plays = [plays]
|
||||
taskman = TaskQueueManager(inventory=invman, loader=loader, passwords={},
|
||||
variable_manager=varman, stdout_callback=ResultsCollector())
|
||||
for currplay in plays:
|
||||
currplay['hosts'] = sys.argv[1]
|
||||
play = Play().load(currplay, loader=loader)
|
||||
try:
|
||||
taskman.run(play)
|
||||
finally:
|
||||
taskman.cleanup()
|
||||
if loader:
|
||||
loader.cleanup_all_tmp_files()
|
||||
@@ -1,3 +1,5 @@
|
||||
import confluent.runansible as runansible
|
||||
import confluent.syncfiles as syncfiles
|
||||
import confluent.config.configmanager as configmanager
|
||||
import confluent.collective.manager as collective
|
||||
import confluent.netutil as netutil
|
||||
@@ -78,7 +80,8 @@ def handle_request(env, start_response):
|
||||
start_response('406 Not supported', [])
|
||||
yield 'Unsupported content type in ACCEPT: ' + retype
|
||||
return
|
||||
if env['REQUEST_METHOD'] not in ('HEAD', 'GET') and 'CONTENT_LENGTH' in env and int(env['CONTENT_LENGTH']) > 0:
|
||||
operation = env['REQUEST_METHOD']
|
||||
if operation not in ('HEAD', 'GET') and 'CONTENT_LENGTH' in env and int(env['CONTENT_LENGTH']) > 0:
|
||||
reqbody = env['wsgi.input'].read(int(env['CONTENT_LENGTH']))
|
||||
if env['PATH_INFO'] == '/self/bmcconfig':
|
||||
hmattr = cfg.get_node_attributes(nodename, 'hardwaremanagement.*')
|
||||
@@ -193,7 +196,7 @@ def handle_request(env, start_response):
|
||||
ncfg['dnsdomain'] = dnsdomain
|
||||
start_response('200 OK', (('Content-Type', retype),))
|
||||
yield dumper(ncfg)
|
||||
elif env['PATH_INFO'] == '/self/sshcert':
|
||||
elif env['PATH_INFO'] == '/self/sshcert' and reqbody:
|
||||
if not sshutil.ca_exists():
|
||||
start_response('500 Unconfigured', ())
|
||||
yield 'CA is not configured on this system (run ...)'
|
||||
@@ -211,14 +214,11 @@ def handle_request(env, start_response):
|
||||
else:
|
||||
start_response('200 OK', (('Content-Type', retype),))
|
||||
yield dumper(sorted(nodes))
|
||||
elif env['PATH_INFO'] == '/self/remoteconfigbmc':
|
||||
if reqbody:
|
||||
try:
|
||||
reqbody = yaml.safe_load(reqbody)
|
||||
except Exception:
|
||||
reqbody = None
|
||||
if not reqbody:
|
||||
start_response('400 bad request', ())
|
||||
elif env['PATH_INFO'] == '/self/remoteconfigbmc' and reqbody:
|
||||
try:
|
||||
reqbody = yaml.safe_load(reqbody)
|
||||
except Exception:
|
||||
reqbody = None
|
||||
cfgmod = reqbody.get('configmod', 'unspecified')
|
||||
if cfgmod == 'xcc':
|
||||
xcc.remote_nodecfg(nodename, cfg)
|
||||
@@ -229,7 +229,7 @@ def handle_request(env, start_response):
|
||||
yield 'Unsupported configmod "{}"'.format(cfgmod)
|
||||
start_response('200 Ok', ())
|
||||
yield 'complete'
|
||||
elif env['PATH_INFO'] == '/self/updatestatus':
|
||||
elif env['PATH_INFO'] == '/self/updatestatus' and reqbody:
|
||||
update = yaml.safe_load(reqbody)
|
||||
if update['status'] == 'staged':
|
||||
targattr = 'deployment.stagedprofile'
|
||||
@@ -258,33 +258,58 @@ def handle_request(env, start_response):
|
||||
else:
|
||||
start_response('500 Error', (('Content-Type', 'text/plain'),))
|
||||
yield 'No pending profile detected, unable to accept status update'
|
||||
elif env['PATH_INFO'] == '/self/saveapikey':
|
||||
elif env['PATH_INFO'] == '/self/saveapikey' and reqbody:
|
||||
cfg.set_node_attributes({
|
||||
nodename: {'deployment.sealedapikey': {'value': reqbody}}})
|
||||
start_response('200 OK', ())
|
||||
yield ''
|
||||
elif env['PATH_INFO'].startswith('/self/scriptlist/'):
|
||||
scriptcat = env['PATH_INFO'].replace('/self/scriptlist/', '')
|
||||
if '..' in scriptcat:
|
||||
start_response('400 Bad Requst', ())
|
||||
elif env['PATH_INFO'].startswith('/self/remoteconfig/') and 'POST' == operation:
|
||||
scriptcat = env['PATH_INFO'].replace('/self/remoteconfig/', '')
|
||||
slist, profile = get_scriptlist(
|
||||
scriptcat, cfg, nodename,
|
||||
'/var/lib/confluent/public/os/{0}/ansible/{1}.d/')
|
||||
playlist = []
|
||||
dirname = '/var/lib/confluent/public/os/{0}/ansible/{1}.d/'.format(
|
||||
profile, scriptcat)
|
||||
for filename in slist:
|
||||
if filename.endswith('.yaml') or filename.endswith('.yml'):
|
||||
playlist.append(os.path.join(dirname, filename))
|
||||
if playlist:
|
||||
runansible.run_playbooks(playlist, [nodename])
|
||||
start_response('202 Queued', ())
|
||||
yield ''
|
||||
else:
|
||||
start_response('200 OK', ())
|
||||
yield ''
|
||||
return
|
||||
deployinfo = cfg.get_node_attributes(
|
||||
nodename, ('deployment.*',))
|
||||
deployinfo = deployinfo.get(nodename, {})
|
||||
profile = deployinfo.get(
|
||||
'deployment.pendingprofile', {}).get('value', '')
|
||||
if not profile:
|
||||
profile = deployinfo.get(
|
||||
'deployment.stagedprofile', {}).get('value', '')
|
||||
if not profile:
|
||||
profile = deployinfo.get(
|
||||
'deployment.profile', {}).get('value', '')
|
||||
slist = None
|
||||
try:
|
||||
slist = os.listdir('/var/lib/confluent/public/os/{0}/scripts/{1}.d/'.format(profile, scriptcat))
|
||||
except OSError:
|
||||
pass
|
||||
elif env['PATH_INFO'].startswith('/self/remotesyncfiles'):
|
||||
if 'POST' == operation:
|
||||
result = syncfiles.start_syncfiles(
|
||||
nodename, cfg, json.loads(reqbody))
|
||||
start_response(result, ())
|
||||
yield ''
|
||||
return
|
||||
if 'GET' == operation:
|
||||
status, output = syncfiles.get_syncresult(nodename)
|
||||
start_response(status, ())
|
||||
yield output
|
||||
return
|
||||
elif env['PATH_INFO'].startswith('/self/remoteconfig/status'):
|
||||
rst = runansible.running_status.get(nodename, None)
|
||||
if not rst:
|
||||
start_response('204 Not Running', ())
|
||||
yield ''
|
||||
return
|
||||
start_response('200 OK', ())
|
||||
if rst.complete:
|
||||
del runansible.running_status[nodename]
|
||||
yield rst.dump_text()
|
||||
return
|
||||
elif env['PATH_INFO'].startswith('/self/scriptlist/'):
|
||||
scriptcat = env['PATH_INFO'].replace('/self/scriptlist/', '')
|
||||
slist, _ = get_scriptlist(
|
||||
scriptcat, cfg, nodename,
|
||||
'/var/lib/confluent/public/os/{0}/scripts/{1}.d/')
|
||||
if slist:
|
||||
start_response('200 OK', (('Content-Type', 'application/yaml'),))
|
||||
yield yaml.safe_dump(util.natural_sort(slist), default_flow_style=False)
|
||||
@@ -295,6 +320,27 @@ def handle_request(env, start_response):
|
||||
start_response('404 Not Found', ())
|
||||
yield 'Not found'
|
||||
|
||||
def get_scriptlist(scriptcat, cfg, nodename, pathtemplate):
|
||||
if '..' in scriptcat:
|
||||
return None, None
|
||||
deployinfo = cfg.get_node_attributes(
|
||||
nodename, ('deployment.*',))
|
||||
deployinfo = deployinfo.get(nodename, {})
|
||||
profile = deployinfo.get(
|
||||
'deployment.pendingprofile', {}).get('value', '')
|
||||
if not profile:
|
||||
profile = deployinfo.get(
|
||||
'deployment.stagedprofile', {}).get('value', '')
|
||||
if not profile:
|
||||
profile = deployinfo.get(
|
||||
'deployment.profile', {}).get('value', '')
|
||||
slist = []
|
||||
try:
|
||||
slist = os.listdir(pathtemplate.format(profile, scriptcat))
|
||||
except OSError:
|
||||
pass
|
||||
return slist, profile
|
||||
|
||||
|
||||
def get_cluster_list(nodename=None, cfg=None):
|
||||
if cfg is None:
|
||||
|
||||
@@ -77,6 +77,8 @@ except ImportError:
|
||||
|
||||
plainsocket = None
|
||||
|
||||
libc = ctypes.CDLL(ctypes.util.find_library('c'))
|
||||
|
||||
def _should_authlog(path, operation):
|
||||
if (operation == 'retrieve' and
|
||||
('/sensors/' in path or '/health/' in path or
|
||||
@@ -126,7 +128,8 @@ def sessionhdl(connection, authname, skipauth=False, cert=None):
|
||||
cfm = authdata[1]
|
||||
authenticated = True
|
||||
# version 0 == original, version 1 == pickle3 allowed, 2 = pickle forbidden, msgpack allowed
|
||||
send_data(connection, "Confluent -- v2 --")
|
||||
# v3 - filehandle allowed
|
||||
send_data(connection, "Confluent -- v3 --")
|
||||
while not authenticated: # prompt for name and passphrase
|
||||
send_data(connection, {'authpassed': 0})
|
||||
response = tlvdata.recv(connection)
|
||||
@@ -156,7 +159,7 @@ def sessionhdl(connection, authname, skipauth=False, cert=None):
|
||||
cfm = authdata[1]
|
||||
send_data(connection, {'authpassed': 1})
|
||||
request = tlvdata.recv(connection)
|
||||
if request and 'collective' in request:
|
||||
if request and isinstance(request, dict) and 'collective' in request:
|
||||
if skipauth:
|
||||
if not libssl:
|
||||
tlvdata.send(
|
||||
@@ -197,7 +200,7 @@ def sessionhdl(connection, authname, skipauth=False, cert=None):
|
||||
'error': 'Unexpected error - ' + str(e)})
|
||||
send_data(connection, {'_requestdone': 1})
|
||||
request = tlvdata.recv(connection)
|
||||
|
||||
cfm.close_client_files()
|
||||
|
||||
def send_response(responses, connection):
|
||||
if responses is None:
|
||||
@@ -208,6 +211,9 @@ def send_response(responses, connection):
|
||||
|
||||
|
||||
def process_request(connection, request, cfm, authdata, authname, skipauth):
|
||||
if isinstance(request, tlvdata.ClientFile):
|
||||
cfm.add_client_file(request)
|
||||
return
|
||||
if not isinstance(request, dict):
|
||||
raise exc.InvalidArgumentException
|
||||
operation = request['operation']
|
||||
@@ -475,7 +481,6 @@ class SockApi(object):
|
||||
self.unixdomainserver = eventlet.spawn(_unixdomainhandler)
|
||||
|
||||
def watch_for_cert(self):
|
||||
libc = ctypes.CDLL(ctypes.util.find_library('c'))
|
||||
watcher = libc.inotify_init1(os.O_NONBLOCK)
|
||||
if libc.inotify_add_watch(watcher, b'/etc/confluent/', 0x100) > -1:
|
||||
while True:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
import base64
|
||||
import confluent.config.configmanager as cfm
|
||||
import confluent.collective.manager as collective
|
||||
import eventlet.green.subprocess as subprocess
|
||||
import glob
|
||||
@@ -7,6 +9,9 @@ import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
agent_pid = None
|
||||
ready_keys = {}
|
||||
|
||||
def normalize_uid():
|
||||
curruid = os.geteuid()
|
||||
neededuid = os.stat('/etc/confluent').st_uid
|
||||
@@ -17,6 +22,34 @@ def normalize_uid():
|
||||
return curruid
|
||||
|
||||
|
||||
def assure_agent():
|
||||
global agent_pid
|
||||
if agent_pid is None:
|
||||
sai = subprocess.check_output(['ssh-agent'])
|
||||
for line in sai.split(b'\n'):
|
||||
if b';' not in line:
|
||||
continue
|
||||
line, _ = line.split(b';', 1)
|
||||
if b'=' not in line:
|
||||
continue
|
||||
k, v = line.split(b'=', 1)
|
||||
if not isinstance(k, str):
|
||||
k = k.decode('utf8')
|
||||
v = v.decode('utf8')
|
||||
if k == 'SSH_AGENT_PID':
|
||||
agent_pid = v
|
||||
os.environ[k] = v
|
||||
|
||||
def get_passphrase():
|
||||
# convert the master key to base64
|
||||
# for use in ssh passphrase context
|
||||
if cfm._masterkey is None:
|
||||
cfm.init_masterkey()
|
||||
phrase = base64.b64encode(cfm._masterkey)
|
||||
if not isinstance(phrase, str):
|
||||
phrase = phrase.decode('utf8')
|
||||
return phrase
|
||||
|
||||
def initialize_ca():
|
||||
ouid = normalize_uid()
|
||||
try:
|
||||
@@ -27,10 +60,11 @@ def initialize_ca():
|
||||
finally:
|
||||
os.seteuid(ouid)
|
||||
myname = collective.get_myname()
|
||||
caname = '{0} SSH CA'.format(myname)
|
||||
comment = '{0} SSH CA'.format(myname)
|
||||
subprocess.check_call(
|
||||
['ssh-keygen', '-C', caname, '-t', 'ed25519', '-f',
|
||||
'/etc/confluent/ssh/ca', '-N', ''], preexec_fn=normalize_uid)
|
||||
['ssh-keygen', '-C', comment, '-t', 'ed25519', '-f',
|
||||
'/etc/confluent/ssh/ca', '-N', get_passphrase()],
|
||||
preexec_fn=normalize_uid)
|
||||
try:
|
||||
os.makedirs('/var/lib/confluent/public/site/ssh/', mode=0o755)
|
||||
except OSError as e:
|
||||
@@ -41,9 +75,31 @@ def initialize_ca():
|
||||
# newent = '@cert-authority * ' + capub.read()
|
||||
|
||||
|
||||
def prep_ssh_key(keyname):
|
||||
assure_agent()
|
||||
if keyname in ready_keys:
|
||||
return
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
askpass = os.path.join(tmpdir, 'askpass.sh')
|
||||
with open(askpass, 'w') as ap:
|
||||
ap.write('#!/bin/sh\necho $CONFLUENT_SSH_PASSPHRASE\n')
|
||||
os.chmod(askpass, 0o700)
|
||||
os.environ['CONFLUENT_SSH_PASSPHRASE'] = get_passphrase()
|
||||
os.environ['DISPLAY'] = 'NONE'
|
||||
os.environ['SSH_ASKPASS'] = askpass
|
||||
with open(os.devnull, 'wb') as devnull:
|
||||
subprocess.check_call(['ssh-add', keyname], stdin=devnull)
|
||||
del os.environ['CONFLUENT_SSH_PASSPHRASE']
|
||||
ready_keys[keyname] = 1
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
def sign_host_key(pubkey, nodename, principals=()):
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
prep_ssh_key('/etc/confluent/ssh/ca')
|
||||
ready_keys['ca.pub'] = 1
|
||||
pkeyname = os.path.join(tmpdir, 'hostkey.pub')
|
||||
with open(pkeyname, 'wb') as pubfile:
|
||||
pubfile.write(pubkey)
|
||||
@@ -51,7 +107,7 @@ def sign_host_key(pubkey, nodename, principals=()):
|
||||
principals.add(nodename)
|
||||
principals = ','.join(sorted(principals))
|
||||
subprocess.check_call(
|
||||
['ssh-keygen', '-s', '/etc/confluent/ssh/ca', '-I', nodename,
|
||||
['ssh-keygen', '-Us', '/etc/confluent/ssh/ca.pub', '-I', nodename,
|
||||
'-n', principals, '-h', pkeyname])
|
||||
certname = pkeyname.replace('.pub', '-cert.pub')
|
||||
with open(certname) as cert:
|
||||
@@ -59,15 +115,22 @@ def sign_host_key(pubkey, nodename, principals=()):
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
def initialize_root_key(generate):
|
||||
def initialize_root_key(generate, automation=False):
|
||||
authorized = []
|
||||
myname = collective.get_myname()
|
||||
for currkey in glob.glob('/root/.ssh/*.pub'):
|
||||
authorized.append(currkey)
|
||||
if generate and not authorized:
|
||||
if generate and not authorized and not automation:
|
||||
subprocess.check_call(['ssh-keygen', '-t', 'ed25519', '-f', '/root/.ssh/id_ed25519', '-N', ''])
|
||||
for currkey in glob.glob('/root/.ssh/*.pub'):
|
||||
authorized.append(currkey)
|
||||
if automation and generate:
|
||||
subprocess.check_call(
|
||||
['ssh-keygen', '-t', 'ed25519',
|
||||
'-f','/etc/confluent/ssh/automation', '-N', get_passphrase(),
|
||||
'-C', 'Confluent Automation by {}'.format(myname)],
|
||||
preexec_fn=normalize_uid)
|
||||
authorized = ['/etc/confluent/ssh/automation.pub']
|
||||
try:
|
||||
os.makedirs('/var/lib/confluent/public/site/ssh', mode=0o755)
|
||||
neededuid = os.stat('/etc/confluent').st_uid
|
||||
@@ -79,15 +142,19 @@ def initialize_root_key(generate):
|
||||
if e.errno != 17:
|
||||
raise
|
||||
neededuid = os.stat('/etc/confluent').st_uid
|
||||
if automation:
|
||||
suffix = 'automationpubkey'
|
||||
else:
|
||||
suffix = 'rootpubkey'
|
||||
for auth in authorized:
|
||||
shutil.copy(
|
||||
auth,
|
||||
'/var/lib/confluent/public/site/ssh/{0}.rootpubkey'.format(
|
||||
myname))
|
||||
os.chmod('/var/lib/confluent/public/site/ssh/{0}.rootpubkey'.format(
|
||||
myname), 0o644)
|
||||
os.chown('/var/lib/confluent/public/site/ssh/{0}.rootpubkey'.format(
|
||||
myname), neededuid, -1)
|
||||
'/var/lib/confluent/public/site/ssh/{0}.{1}'.format(
|
||||
myname, suffix))
|
||||
os.chmod('/var/lib/confluent/public/site/ssh/{0}.{1}'.format(
|
||||
myname, suffix), 0o644)
|
||||
os.chown('/var/lib/confluent/public/site/ssh/{0}.{1}'.format(
|
||||
myname, suffix), neededuid, -1)
|
||||
|
||||
|
||||
def ca_exists():
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2021 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 glob
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import confluent.sshutil as sshutil
|
||||
import eventlet.green.subprocess as subprocess
|
||||
import eventlet
|
||||
|
||||
def mkdirp(path):
|
||||
try:
|
||||
os.makedirs(path)
|
||||
except OSError as e:
|
||||
if e.errno != 17:
|
||||
raise
|
||||
|
||||
class SyncList(object):
|
||||
def __init__(self, filename):
|
||||
slist = None
|
||||
self.replacemap = {}
|
||||
self.appendmap = {}
|
||||
self.mergemap = {}
|
||||
with open(filename, 'r') as slfile:
|
||||
slist = slfile.read()
|
||||
entries = slist.split('\n')
|
||||
currmap = self.replacemap
|
||||
for ent in entries:
|
||||
try:
|
||||
cmtidx = ent.index('#')
|
||||
ent = ent[:cmtidx]
|
||||
except ValueError:
|
||||
pass
|
||||
for special in '!@$%^&*()|{}':
|
||||
if special in ent:
|
||||
raise Exception(
|
||||
'Special character "{}" reserved for future use'.format(special))
|
||||
ent = ent.strip()
|
||||
if not ent:
|
||||
continue
|
||||
if ent[-1] == ':':
|
||||
if ent == 'MERGE:':
|
||||
currmap = self.mergemap
|
||||
else:
|
||||
raise Exception(
|
||||
'Section "{}" is not currently supported in syncfiles'.format(ent[:-1]))
|
||||
continue
|
||||
if '->' in ent:
|
||||
k, v = ent.split('->')
|
||||
k = k.strip()
|
||||
v = v.strip()
|
||||
else:
|
||||
k = ent
|
||||
v = ent
|
||||
currmap[k] = v
|
||||
|
||||
|
||||
def sync_list_to_node(synclist, node, suffixes):
|
||||
targdir = tempfile.mkdtemp('.syncto{}'.format(node))
|
||||
output = ''
|
||||
try:
|
||||
sl = SyncList(synclist)
|
||||
for ent in sl.replacemap:
|
||||
stage_ent(sl.replacemap, ent, targdir)
|
||||
if 'append' in suffixes:
|
||||
while suffixes['append'] and suffixes['append'][0] == '/':
|
||||
suffixes['append'] = suffixes['append'][1:]
|
||||
for ent in sl.appendmap:
|
||||
stage_ent(sl.appendmap, ent,
|
||||
os.path.join(targdir, suffixes['append']))
|
||||
if 'merge' in suffixes:
|
||||
while suffixes['merge'] and suffixes['merge'][0] == '/':
|
||||
suffixes['merge'] = suffixes['merge'][1:]
|
||||
for ent in sl.mergemap:
|
||||
stage_ent(sl.mergemap, ent,
|
||||
os.path.join(targdir, suffixes['merge']))
|
||||
sshutil.prep_ssh_key('/etc/confluent/ssh/automation')
|
||||
output = subprocess.check_output(
|
||||
['rsync', '-rvL', targdir + '/', 'root@{}:/'.format(node)])
|
||||
finally:
|
||||
shutil.rmtree(targdir)
|
||||
return output
|
||||
|
||||
def stage_ent(currmap, ent, targdir):
|
||||
dst = currmap[ent]
|
||||
everyfent = []
|
||||
allfents = ent.split()
|
||||
for tmpent in allfents:
|
||||
fents = glob.glob(tmpent)
|
||||
if fents:
|
||||
everyfent.extend(fents)
|
||||
else:
|
||||
everyfent.extend(os.path.dirname(tmpent))
|
||||
if not everyfent:
|
||||
raise Exception('No matching files for "{}"'.format(ent))
|
||||
while dst and dst[0] == '/':
|
||||
dst = dst[1:]
|
||||
fulltarg = os.path.join(targdir, dst)
|
||||
if dst[-1] == '/' or len(everyfent) > 1 or os.path.isdir(everyfent[0]):
|
||||
# target *must* be a directory
|
||||
fulltargdir = fulltarg
|
||||
else:
|
||||
fulltargdir = os.path.join(targdir, os.path.dirname(dst))
|
||||
mkdirp(fulltargdir)
|
||||
for targ in everyfent:
|
||||
if fulltargdir == fulltarg:
|
||||
os.symlink(
|
||||
targ, os.path.join(
|
||||
fulltargdir, os.path.basename(targ)))
|
||||
else:
|
||||
os.symlink(targ, fulltarg)
|
||||
|
||||
syncrunners = {}
|
||||
|
||||
|
||||
def start_syncfiles(nodename, cfg, suffixes):
|
||||
deployinfo = cfg.get_node_attributes(
|
||||
nodename, ('deployment.*',))
|
||||
deployinfo = deployinfo.get(nodename, {})
|
||||
profile = deployinfo.get(
|
||||
'deployment.pendingprofile', {}).get('value', '')
|
||||
if not profile:
|
||||
profile = deployinfo.get(
|
||||
'deployment.stagedprofile', {}).get('value', '')
|
||||
if not profile:
|
||||
profile = deployinfo.get(
|
||||
'deployment.profile', {}).get('value', '')
|
||||
if not profile:
|
||||
raise Exception('Cannot perform syncfiles without profile assigned')
|
||||
synclist = '/var/lib/confluent/public/os/{}/syncfiles'.format(profile)
|
||||
if not os.path.exists(synclist):
|
||||
return '200 OK' # not running
|
||||
syncrunners[nodename] = eventlet.spawn(
|
||||
sync_list_to_node, synclist, nodename, suffixes)
|
||||
return '202 Queued' # backgrounded
|
||||
|
||||
def get_syncresult(nodename):
|
||||
if nodename not in syncrunners:
|
||||
return ('204 Not Running', '')
|
||||
if not syncrunners[nodename].dead:
|
||||
return ('200 OK', '')
|
||||
result = syncrunners[nodename].wait()
|
||||
del syncrunners[nodename]
|
||||
return ('200 OK', result)
|
||||
@@ -0,0 +1,5 @@
|
||||
vtbufferd: vtbufferd.c tmt.c
|
||||
gcc -O3 -o vtbufferd vtbufferd.c tmt.c
|
||||
|
||||
clean:
|
||||
rm vtbufferd
|
||||
@@ -0,0 +1,22 @@
|
||||
VERSION=`git describe|cut -d- -f 1`
|
||||
NUMCOMMITS=`git describe|cut -d- -f 2`
|
||||
if [ "$NUMCOMMITS" != "$VERSION" ]; then
|
||||
VERSION=$VERSION.dev$NUMCOMMITS.g`git describe|cut -d- -f 3`
|
||||
fi
|
||||
mkdir -p dist/confluent_vtbufferd-$VERSION
|
||||
cp *.c *.h Makefile dist/confluent_vtbufferd-$VERSION
|
||||
cd dist
|
||||
tar czvf confluent_vtbufferd-$VERSION.tar.gz confluent_vtbufferd-$VERSION
|
||||
cd -
|
||||
cp dist/confluent_vtbufferd-$VERSION.tar.gz ~/rpmbuild/SOURCES
|
||||
sed -e 's/#VERSION#/'$VERSION/ confluent_vtbufferd.spec.tmpl > ~/rpmbuild/SPECS/confluent_vtbufferd.spec
|
||||
rpmbuild -ba ~/rpmbuild/SPECS/confluent_vtbufferd.spec 2> /dev/null |grep ^Wrote:
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "[ERROR] rpmbuild returned non-zero, run: rpmbuild -ba ~/rpmbuild/SPECS/$PKGNAME.spec"
|
||||
exit 1
|
||||
else
|
||||
# Clean up the generated files in this directory
|
||||
rm -rf dist
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
%define name confluent_vtbufferd
|
||||
%define version #VERSION#
|
||||
%define release 1
|
||||
%define debug_package %{nil}
|
||||
%define _build_id_links none
|
||||
|
||||
Summary: Console buffer manager for confluent
|
||||
Name: %{name}
|
||||
Version: %{version}
|
||||
Release: %{release}
|
||||
Source0: %{name}-%{version}.tar.gz
|
||||
License: Apache2
|
||||
Group: Development/Libraries
|
||||
Vendor: Lenovo HPC Organization <hpchelp@lenovo.com>
|
||||
Url: https://github.com/lenovo/confluent/
|
||||
|
||||
%description
|
||||
Service for managing in-memory VT emulation for confluent.
|
||||
|
||||
Contains third party open source code:
|
||||
|
||||
Copyright (c) 2017 Rob King
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the copyright holder nor the
|
||||
names of contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
%prep
|
||||
%setup -n %{name}-%{version} -n %{name}-%{version}
|
||||
|
||||
|
||||
%build
|
||||
make
|
||||
|
||||
%install
|
||||
mkdir -p $RPM_BUILD_ROOT/opt/confluent/bin
|
||||
cp vtbufferd $RPM_BUILD_ROOT/opt/confluent/bin/
|
||||
@@ -0,0 +1,521 @@
|
||||
/* Copyright (c) 2017 Rob King
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name of the copyright holder nor the
|
||||
* names of contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS,
|
||||
* COPYRIGHT HOLDERS, OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
||||
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "tmt.h"
|
||||
|
||||
#define BUF_MAX 100
|
||||
#define PAR_MAX 8
|
||||
#define TAB 8
|
||||
#define MAX(x, y) (((size_t)(x) > (size_t)(y)) ? (size_t)(x) : (size_t)(y))
|
||||
#define MIN(x, y) (((size_t)(x) < (size_t)(y)) ? (size_t)(x) : (size_t)(y))
|
||||
#define CLINE(vt) (vt)->screen.lines[MIN((vt)->curs.r, (vt)->screen.nline - 1)]
|
||||
|
||||
#define P0(x) (vt->pars[x])
|
||||
#define P1(x) (vt->pars[x]? vt->pars[x] : 1)
|
||||
#define CB(vt, m, a) ((vt)->cb? (vt)->cb(m, vt, a, (vt)->p) : (void)0)
|
||||
#define INESC ((vt)->state)
|
||||
|
||||
#define COMMON_VARS \
|
||||
TMTSCREEN *s = &vt->screen; \
|
||||
TMTPOINT *c = &vt->curs; \
|
||||
TMTLINE *l = CLINE(vt); \
|
||||
TMTCHAR *t = vt->tabs->chars
|
||||
|
||||
#define HANDLER(name) static void name (TMT *vt) { COMMON_VARS;
|
||||
|
||||
struct TMT{
|
||||
TMTPOINT curs, oldcurs;
|
||||
TMTATTRS attrs, oldattrs;
|
||||
|
||||
bool dirty, acs, decdraw, ignored;
|
||||
TMTSCREEN screen;
|
||||
TMTLINE *tabs;
|
||||
|
||||
TMTCALLBACK cb;
|
||||
void *p;
|
||||
const wchar_t *acschars;
|
||||
const wchar_t *decchars;
|
||||
|
||||
mbstate_t ms;
|
||||
size_t nmb;
|
||||
char mb[BUF_MAX + 1];
|
||||
|
||||
size_t pars[PAR_MAX];
|
||||
size_t npar;
|
||||
size_t arg;
|
||||
enum {S_NUL, S_ESC, S_ARG, S_SCS} state;
|
||||
};
|
||||
|
||||
static TMTATTRS defattrs = {.fg = TMT_COLOR_DEFAULT, .bg = TMT_COLOR_DEFAULT};
|
||||
static void writecharatcurs(TMT *vt, wchar_t w);
|
||||
|
||||
static wchar_t
|
||||
decchar(const TMT *vt, unsigned char c)
|
||||
{
|
||||
if (c > 94 && c < 127)
|
||||
return vt->decchars[c - 95];
|
||||
return (wchar_t)c;
|
||||
}
|
||||
|
||||
static wchar_t
|
||||
tacs(const TMT *vt, unsigned char c)
|
||||
{
|
||||
/* The terminfo alternate character set for ANSI. */
|
||||
static unsigned char map[] = {0020U, 0021U, 0030U, 0031U, 0333U, 0004U,
|
||||
0261U, 0370U, 0361U, 0260U, 0331U, 0277U,
|
||||
0332U, 0300U, 0305U, 0176U, 0304U, 0304U,
|
||||
0304U, 0137U, 0303U, 0264U, 0301U, 0302U,
|
||||
0263U, 0363U, 0362U, 0343U, 0330U, 0234U,
|
||||
0376U};
|
||||
for (size_t i = 0; i < sizeof(map); i++) if (map[i] == c)
|
||||
return vt->acschars[i];
|
||||
return (wchar_t)c;
|
||||
}
|
||||
|
||||
static void
|
||||
dirtylines(TMT *vt, size_t s, size_t e)
|
||||
{
|
||||
vt->dirty = true;
|
||||
for (size_t i = s; i < e; i++)
|
||||
vt->screen.lines[i]->dirty = true;
|
||||
}
|
||||
|
||||
static void
|
||||
clearline(TMT *vt, TMTLINE *l, size_t s, size_t e)
|
||||
{
|
||||
vt->dirty = l->dirty = true;
|
||||
for (size_t i = s; i < e && i < vt->screen.ncol; i++){
|
||||
l->chars[i].a = defattrs;
|
||||
l->chars[i].c = L' ';
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
clearlines(TMT *vt, size_t r, size_t n)
|
||||
{
|
||||
for (size_t i = r; i < r + n && i < vt->screen.nline; i++)
|
||||
clearline(vt, vt->screen.lines[i], 0, vt->screen.ncol);
|
||||
}
|
||||
|
||||
static void
|
||||
scrup(TMT *vt, size_t r, size_t n)
|
||||
{
|
||||
n = MIN(n, vt->screen.nline - 1 - r);
|
||||
|
||||
if (n){
|
||||
TMTLINE *buf[n];
|
||||
|
||||
memcpy(buf, vt->screen.lines + r, n * sizeof(TMTLINE *));
|
||||
memmove(vt->screen.lines + r, vt->screen.lines + r + n,
|
||||
(vt->screen.nline - n - r) * sizeof(TMTLINE *));
|
||||
memcpy(vt->screen.lines + (vt->screen.nline - n),
|
||||
buf, n * sizeof(TMTLINE *));
|
||||
|
||||
clearlines(vt, vt->screen.nline - n, n);
|
||||
dirtylines(vt, r, vt->screen.nline);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
scrdn(TMT *vt, size_t r, size_t n)
|
||||
{
|
||||
n = MIN(n, vt->screen.nline - 1 - r);
|
||||
|
||||
if (n){
|
||||
TMTLINE *buf[n];
|
||||
|
||||
memcpy(buf, vt->screen.lines + (vt->screen.nline - n),
|
||||
n * sizeof(TMTLINE *));
|
||||
memmove(vt->screen.lines + r + n, vt->screen.lines + r,
|
||||
(vt->screen.nline - n - r) * sizeof(TMTLINE *));
|
||||
memcpy(vt->screen.lines + r, buf, n * sizeof(TMTLINE *));
|
||||
|
||||
clearlines(vt, r, n);
|
||||
dirtylines(vt, r, vt->screen.nline);
|
||||
}
|
||||
}
|
||||
|
||||
HANDLER(ed)
|
||||
size_t b = 0;
|
||||
size_t e = s->nline;
|
||||
|
||||
switch (P0(0)){
|
||||
case 0: b = c->r + 1; clearline(vt, l, c->c, vt->screen.ncol); break;
|
||||
case 1: e = c->r - 1; clearline(vt, l, 0, c->c); break;
|
||||
case 2: /* use defaults */ break;
|
||||
default: /* do nothing */ return;
|
||||
}
|
||||
|
||||
clearlines(vt, b, e - b);
|
||||
}
|
||||
|
||||
HANDLER(ich)
|
||||
size_t n = P1(0); /* XXX use MAX */
|
||||
if (n > s->ncol - c->c - 1) n = s->ncol - c->c - 1;
|
||||
|
||||
memmove(l->chars + c->c + n, l->chars + c->c,
|
||||
MIN(s->ncol - 1 - c->c,
|
||||
(s->ncol - c->c - n - 1)) * sizeof(TMTCHAR));
|
||||
clearline(vt, l, c->c, n);
|
||||
}
|
||||
|
||||
HANDLER(dch)
|
||||
size_t n = P1(0); /* XXX use MAX */
|
||||
if (n > s->ncol - c->c) n = s->ncol - c->c;
|
||||
else if (n == 0) return;
|
||||
|
||||
memmove(l->chars + c->c, l->chars + c->c + n,
|
||||
(s->ncol - c->c - n) * sizeof(TMTCHAR));
|
||||
|
||||
clearline(vt, l, s->ncol - n, s->ncol);
|
||||
/* VT102 manual says the attribute for the newly empty characters
|
||||
* should be the same as the last character moved left, which isn't
|
||||
* what clearline() currently does.
|
||||
*/
|
||||
}
|
||||
|
||||
HANDLER(el)
|
||||
switch (P0(0)){
|
||||
case 0: clearline(vt, l, c->c, vt->screen.ncol); break;
|
||||
case 1: clearline(vt, l, 0, MIN(c->c + 1, s->ncol - 1)); break;
|
||||
case 2: clearline(vt, l, 0, vt->screen.ncol); break;
|
||||
}
|
||||
}
|
||||
|
||||
HANDLER(sgr)
|
||||
#define FGBG(c) *(P0(i) < 40? &vt->attrs.fg : &vt->attrs.bg) = c
|
||||
for (size_t i = 0; i < vt->npar; i++) switch (P0(i)){
|
||||
case 0: vt->attrs = defattrs; break;
|
||||
case 1: case 22: vt->attrs.bold = P0(0) < 20; break;
|
||||
case 2: case 23: vt->attrs.dim = P0(0) < 20; break;
|
||||
case 4: case 24: vt->attrs.underline = P0(0) < 20; break;
|
||||
case 5: case 25: vt->attrs.blink = P0(0) < 20; break;
|
||||
case 7: case 27: vt->attrs.reverse = P0(0) < 20; break;
|
||||
case 8: case 28: vt->attrs.invisible = P0(0) < 20; break;
|
||||
case 10: case 11: vt->acs = P0(0) > 10; break;
|
||||
case 30: case 40: FGBG(TMT_COLOR_BLACK); break;
|
||||
case 31: case 41: FGBG(TMT_COLOR_RED); break;
|
||||
case 32: case 42: FGBG(TMT_COLOR_GREEN); break;
|
||||
case 33: case 43: FGBG(TMT_COLOR_YELLOW); break;
|
||||
case 34: case 44: FGBG(TMT_COLOR_BLUE); break;
|
||||
case 35: case 45: FGBG(TMT_COLOR_MAGENTA); break;
|
||||
case 36: case 46: FGBG(TMT_COLOR_CYAN); break;
|
||||
case 37: case 47: FGBG(TMT_COLOR_WHITE); break;
|
||||
case 39: case 49: FGBG(TMT_COLOR_DEFAULT); break;
|
||||
}
|
||||
}
|
||||
|
||||
HANDLER(rep)
|
||||
if (!c->c) return;
|
||||
wchar_t r = l->chars[c->c - 1].c;
|
||||
for (size_t i = 0; i < P1(0); i++)
|
||||
writecharatcurs(vt, r);
|
||||
}
|
||||
|
||||
HANDLER(dsr)
|
||||
char r[BUF_MAX + 1] = {0};
|
||||
snprintf(r, BUF_MAX, "\033[%zd;%zdR", c->r + 1, c->c + 1);
|
||||
CB(vt, TMT_MSG_ANSWER, (const char *)r);
|
||||
}
|
||||
|
||||
HANDLER(resetparser)
|
||||
memset(vt->pars, 0, sizeof(vt->pars));
|
||||
vt->state = vt->npar = vt->arg = vt->ignored = (bool)0;
|
||||
}
|
||||
|
||||
HANDLER(consumearg)
|
||||
if (vt->npar < PAR_MAX)
|
||||
vt->pars[vt->npar++] = vt->arg;
|
||||
vt->arg = 0;
|
||||
}
|
||||
|
||||
HANDLER(fixcursor)
|
||||
c->r = MIN(c->r, s->nline - 1);
|
||||
c->c = MIN(c->c, s->ncol - 1);
|
||||
}
|
||||
|
||||
static bool
|
||||
handlechar(TMT *vt, char i)
|
||||
{
|
||||
COMMON_VARS;
|
||||
|
||||
char cs[] = {i, 0};
|
||||
#define ON(S, C, A) if (vt->state == (S) && strchr(C, i)){ A; return true;}
|
||||
#define DO(S, C, A) ON(S, C, consumearg(vt); if (!vt->ignored) {A;} \
|
||||
fixcursor(vt); resetparser(vt););
|
||||
|
||||
DO(S_NUL, "\x07", CB(vt, TMT_MSG_BELL, NULL))
|
||||
DO(S_NUL, "\x08", if (c->c) c->c--)
|
||||
DO(S_NUL, "\x09", while (++c->c < s->ncol - 1 && t[c->c].c != L'*'))
|
||||
DO(S_NUL, "\x0a", c->r < s->nline - 1? (void)c->r++ : scrup(vt, 0, 1))
|
||||
DO(S_NUL, "\x0d", c->c = 0)
|
||||
ON(S_NUL, "\x1b", vt->state = S_ESC)
|
||||
ON(S_ESC, "\x1b", vt->state = S_ESC)
|
||||
DO(S_ESC, "H", t[c->c].c = L'*')
|
||||
DO(S_ESC, "7", vt->oldcurs = vt->curs; vt->oldattrs = vt->attrs)
|
||||
DO(S_ESC, "8", vt->curs = vt->oldcurs; vt->attrs = vt->oldattrs)
|
||||
ON(S_ESC, "+*)", vt->ignored = true; vt->state = S_ARG)
|
||||
ON(S_ESC, "(", vt->state = S_SCS)
|
||||
DO(S_SCS, "0", vt->decdraw = true)
|
||||
DO(S_SCS, "B", vt->decdraw = false)
|
||||
DO(S_ESC, "c", tmt_reset(vt))
|
||||
DO(S_ESC, "M", if (c->r) c->r--)
|
||||
ON(S_ESC, "[", vt->state = S_ARG)
|
||||
ON(S_ARG, "\x1b", vt->state = S_ESC)
|
||||
ON(S_ARG, ";", consumearg(vt))
|
||||
ON(S_ARG, "?", (void)0)
|
||||
ON(S_ARG, "0123456789", vt->arg = vt->arg * 10 + atoi(cs))
|
||||
DO(S_ARG, "A", c->r = MAX(c->r - P1(0), 0))
|
||||
DO(S_ARG, "B", c->r = MIN(c->r + P1(0), s->nline - 1))
|
||||
DO(S_ARG, "C", c->c = MIN(c->c + P1(0), s->ncol - 1))
|
||||
DO(S_ARG, "D", c->c = MIN(c->c - P1(0), c->c))
|
||||
DO(S_ARG, "E", c->c = 0; c->r = MIN(c->r + P1(0), s->nline - 1))
|
||||
DO(S_ARG, "F", c->c = 0; c->r = MAX(c->r - P1(0), 0))
|
||||
DO(S_ARG, "G", c->c = MIN(P1(0) - 1, s->ncol - 1))
|
||||
DO(S_ARG, "d", c->r = MIN(P1(0) - 1, s->nline - 1))
|
||||
DO(S_ARG, "Hf", c->r = P1(0) - 1; c->c = P1(1) - 1)
|
||||
DO(S_ARG, "I", while (++c->c < s->ncol - 1 && t[c->c].c != L'*'))
|
||||
DO(S_ARG, "J", ed(vt))
|
||||
DO(S_ARG, "K", el(vt))
|
||||
DO(S_ARG, "L", scrdn(vt, c->r, P1(0)))
|
||||
DO(S_ARG, "M", scrup(vt, c->r, P1(0)))
|
||||
DO(S_ARG, "P", dch(vt))
|
||||
DO(S_ARG, "S", scrup(vt, 0, P1(0)))
|
||||
DO(S_ARG, "T", scrdn(vt, 0, P1(0)))
|
||||
DO(S_ARG, "X", clearline(vt, l, c->c, P1(0) + c->c))
|
||||
DO(S_ARG, "Z", while (c->c && t[--c->c].c != L'*'))
|
||||
DO(S_ARG, "b", rep(vt));
|
||||
DO(S_ARG, "c", CB(vt, TMT_MSG_ANSWER, "\033[?6c"))
|
||||
DO(S_ARG, "g", if (P0(0) == 3) clearline(vt, vt->tabs, 0, s->ncol))
|
||||
DO(S_ARG, "m", sgr(vt))
|
||||
DO(S_ARG, "n", if (P0(0) == 6) dsr(vt))
|
||||
DO(S_ARG, "h", if (P0(0) == 25) CB(vt, TMT_MSG_CURSOR, "t"))
|
||||
DO(S_ARG, "i", (void)0)
|
||||
DO(S_ARG, "l", if (P0(0) == 25) CB(vt, TMT_MSG_CURSOR, "f"))
|
||||
DO(S_ARG, "s", vt->oldcurs = vt->curs; vt->oldattrs = vt->attrs)
|
||||
DO(S_ARG, "u", vt->curs = vt->oldcurs; vt->attrs = vt->oldattrs)
|
||||
DO(S_ARG, "@", ich(vt))
|
||||
|
||||
return resetparser(vt), false;
|
||||
}
|
||||
|
||||
static void
|
||||
notify(TMT *vt, bool update, bool moved)
|
||||
{
|
||||
if (update) CB(vt, TMT_MSG_UPDATE, &vt->screen);
|
||||
if (moved) CB(vt, TMT_MSG_MOVED, &vt->curs);
|
||||
}
|
||||
|
||||
static TMTLINE *
|
||||
allocline(TMT *vt, TMTLINE *o, size_t n, size_t pc)
|
||||
{
|
||||
TMTLINE *l = realloc(o, sizeof(TMTLINE) + n * sizeof(TMTCHAR));
|
||||
if (!l) return NULL;
|
||||
|
||||
clearline(vt, l, pc, n);
|
||||
return l;
|
||||
}
|
||||
|
||||
static void
|
||||
freelines(TMT *vt, size_t s, size_t n, bool screen)
|
||||
{
|
||||
for (size_t i = s; vt->screen.lines && i < s + n; i++){
|
||||
free(vt->screen.lines[i]);
|
||||
vt->screen.lines[i] = NULL;
|
||||
}
|
||||
if (screen) free(vt->screen.lines);
|
||||
}
|
||||
|
||||
TMT *
|
||||
tmt_open(size_t nline, size_t ncol, TMTCALLBACK cb, void *p,
|
||||
const wchar_t *acs)
|
||||
{
|
||||
TMT *vt = calloc(1, sizeof(TMT));
|
||||
if (!nline || !ncol || !vt) return free(vt), NULL;
|
||||
|
||||
/* ASCII-safe defaults for box-drawing characters. */
|
||||
vt->acschars = acs? acs : L"→←↑↓■◆▒°±▒┘┐┌└┼⎺───⎽├┤┴┬│≤≥π≠£•"; //L"><^v#+:o##+++++~---_++++|<>*!fo";
|
||||
vt->decchars = L" ◆▒\t\f\r\n°±\n\v┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π≠£•";
|
||||
vt->cb = cb;
|
||||
vt->p = p;
|
||||
|
||||
if (!tmt_resize(vt, nline, ncol)) return tmt_close(vt), NULL;
|
||||
return vt;
|
||||
}
|
||||
|
||||
void
|
||||
tmt_close(TMT *vt)
|
||||
{
|
||||
free(vt->tabs);
|
||||
freelines(vt, 0, vt->screen.nline, true);
|
||||
free(vt);
|
||||
}
|
||||
|
||||
bool
|
||||
tmt_resize(TMT *vt, size_t nline, size_t ncol)
|
||||
{
|
||||
if (nline < 2 || ncol < 2) return false;
|
||||
if (nline < vt->screen.nline)
|
||||
freelines(vt, nline, vt->screen.nline - nline, false);
|
||||
|
||||
TMTLINE **l = realloc(vt->screen.lines, nline * sizeof(TMTLINE *));
|
||||
if (!l) return false;
|
||||
|
||||
size_t pc = vt->screen.ncol;
|
||||
vt->screen.lines = l;
|
||||
vt->screen.ncol = ncol;
|
||||
for (size_t i = 0; i < nline; i++){
|
||||
TMTLINE *nl = NULL;
|
||||
if (i >= vt->screen.nline)
|
||||
nl = vt->screen.lines[i] = allocline(vt, NULL, ncol, 0);
|
||||
else
|
||||
nl = allocline(vt, vt->screen.lines[i], ncol, pc);
|
||||
|
||||
if (!nl) return false;
|
||||
vt->screen.lines[i] = nl;
|
||||
}
|
||||
vt->screen.nline = nline;
|
||||
|
||||
vt->tabs = allocline(vt, vt->tabs, ncol, 0);
|
||||
if (!vt->tabs) return free(l), false;
|
||||
vt->tabs->chars[0].c = vt->tabs->chars[ncol - 1].c = L'*';
|
||||
for (size_t i = 0; i < ncol; i++) if (i % TAB == 0)
|
||||
vt->tabs->chars[i].c = L'*';
|
||||
|
||||
fixcursor(vt);
|
||||
dirtylines(vt, 0, nline);
|
||||
notify(vt, true, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
writecharatcurs(TMT *vt, wchar_t w)
|
||||
{
|
||||
COMMON_VARS;
|
||||
|
||||
#ifdef TMT_HAS_WCWIDTH
|
||||
extern int wcwidth(wchar_t c);
|
||||
if (wcwidth(w) > 1) w = TMT_INVALID_CHAR;
|
||||
if (wcwidth(w) < 0) return;
|
||||
#endif
|
||||
|
||||
CLINE(vt)->chars[vt->curs.c].c = w;
|
||||
CLINE(vt)->chars[vt->curs.c].a = vt->attrs;
|
||||
CLINE(vt)->dirty = vt->dirty = true;
|
||||
|
||||
if (c->c < s->ncol - 1)
|
||||
c->c++;
|
||||
else{
|
||||
c->c = 0;
|
||||
c->r++;
|
||||
}
|
||||
|
||||
if (c->r >= s->nline){
|
||||
c->r = s->nline - 1;
|
||||
scrup(vt, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static inline size_t
|
||||
testmbchar(TMT *vt)
|
||||
{
|
||||
mbstate_t ts = vt->ms;
|
||||
return vt->nmb? mbrtowc(NULL, vt->mb, vt->nmb, &ts) : (size_t)-2;
|
||||
}
|
||||
|
||||
static inline wchar_t
|
||||
getmbchar(TMT *vt)
|
||||
{
|
||||
wchar_t c = 0;
|
||||
size_t n = mbrtowc(&c, vt->mb, vt->nmb, &vt->ms);
|
||||
vt->nmb = 0;
|
||||
return (n == (size_t)-1 || n == (size_t)-2)? TMT_INVALID_CHAR : c;
|
||||
}
|
||||
|
||||
void
|
||||
tmt_write(TMT *vt, const char *s, size_t n)
|
||||
{
|
||||
TMTPOINT oc = vt->curs;
|
||||
n = n? n : strlen(s);
|
||||
|
||||
for (size_t p = 0; p < n; p++){
|
||||
if (handlechar(vt, s[p]))
|
||||
continue;
|
||||
else if (vt->acs)
|
||||
writecharatcurs(vt, tacs(vt, (unsigned char)s[p]));
|
||||
else if (vt->decdraw)
|
||||
writecharatcurs(vt, decchar(vt, (unsigned char)s[p]));
|
||||
else if (vt->nmb >= BUF_MAX)
|
||||
writecharatcurs(vt, getmbchar(vt));
|
||||
else{
|
||||
switch (testmbchar(vt)){
|
||||
case (size_t)-1: writecharatcurs(vt, getmbchar(vt)); break;
|
||||
case (size_t)-2: vt->mb[vt->nmb++] = s[p]; break;
|
||||
}
|
||||
|
||||
if (testmbchar(vt) <= MB_LEN_MAX)
|
||||
writecharatcurs(vt, getmbchar(vt));
|
||||
}
|
||||
}
|
||||
|
||||
notify(vt, vt->dirty, memcmp(&oc, &vt->curs, sizeof(oc)) != 0);
|
||||
}
|
||||
|
||||
const TMTSCREEN *
|
||||
tmt_screen(const TMT *vt)
|
||||
{
|
||||
return &vt->screen;
|
||||
}
|
||||
|
||||
const TMTPOINT *
|
||||
tmt_cursor(const TMT *vt)
|
||||
{
|
||||
return &vt->curs;
|
||||
}
|
||||
|
||||
void
|
||||
tmt_clean(TMT *vt)
|
||||
{
|
||||
for (size_t i = 0; i < vt->screen.nline; i++)
|
||||
vt->dirty = vt->screen.lines[i]->dirty = false;
|
||||
}
|
||||
|
||||
void
|
||||
tmt_reset(TMT *vt)
|
||||
{
|
||||
vt->curs.r = vt->curs.c = vt->oldcurs.r = vt->oldcurs.c = vt->acs = (bool)0;
|
||||
resetparser(vt);
|
||||
vt->attrs = vt->oldattrs = defattrs;
|
||||
memset(&vt->ms, 0, sizeof(vt->ms));
|
||||
clearlines(vt, 0, vt->screen.nline);
|
||||
CB(vt, TMT_MSG_CURSOR, "t");
|
||||
notify(vt, true, true);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/* Copyright (c) 2017 Rob King
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name of the copyright holder nor the
|
||||
* names of contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS,
|
||||
* COPYRIGHT HOLDERS, OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
||||
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#ifndef TMT_H
|
||||
#define TMT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <wchar.h>
|
||||
|
||||
/**** INVALID WIDE CHARACTER */
|
||||
#ifndef TMT_INVALID_CHAR
|
||||
#define TMT_INVALID_CHAR ((wchar_t)0xfffd)
|
||||
#endif
|
||||
|
||||
/**** INPUT SEQUENCES */
|
||||
#define TMT_KEY_UP "\033[A"
|
||||
#define TMT_KEY_DOWN "\033[B"
|
||||
#define TMT_KEY_RIGHT "\033[C"
|
||||
#define TMT_KEY_LEFT "\033[D"
|
||||
#define TMT_KEY_HOME "\033[H"
|
||||
#define TMT_KEY_END "\033[Y"
|
||||
#define TMT_KEY_INSERT "\033[L"
|
||||
#define TMT_KEY_BACKSPACE "\x08"
|
||||
#define TMT_KEY_ESCAPE "\x1b"
|
||||
#define TMT_KEY_BACK_TAB "\033[Z"
|
||||
#define TMT_KEY_PAGE_UP "\033[V"
|
||||
#define TMT_KEY_PAGE_DOWN "\033[U"
|
||||
#define TMT_KEY_F1 "\033OP"
|
||||
#define TMT_KEY_F2 "\033OQ"
|
||||
#define TMT_KEY_F3 "\033OR"
|
||||
#define TMT_KEY_F4 "\033OS"
|
||||
#define TMT_KEY_F5 "\033OT"
|
||||
#define TMT_KEY_F6 "\033OU"
|
||||
#define TMT_KEY_F7 "\033OV"
|
||||
#define TMT_KEY_F8 "\033OW"
|
||||
#define TMT_KEY_F9 "\033OX"
|
||||
#define TMT_KEY_F10 "\033OY"
|
||||
|
||||
/**** BASIC DATA STRUCTURES */
|
||||
typedef struct TMT TMT;
|
||||
|
||||
typedef enum{
|
||||
TMT_COLOR_DEFAULT = 0,
|
||||
TMT_COLOR_BLACK = 1,
|
||||
TMT_COLOR_RED,
|
||||
TMT_COLOR_GREEN,
|
||||
TMT_COLOR_YELLOW,
|
||||
TMT_COLOR_BLUE,
|
||||
TMT_COLOR_MAGENTA,
|
||||
TMT_COLOR_CYAN,
|
||||
TMT_COLOR_WHITE,
|
||||
TMT_COLOR_MAX
|
||||
} tmt_color_t;
|
||||
|
||||
typedef struct TMTATTRS TMTATTRS;
|
||||
struct TMTATTRS{
|
||||
bool bold;
|
||||
bool dim;
|
||||
bool underline;
|
||||
bool blink;
|
||||
bool reverse;
|
||||
bool invisible;
|
||||
tmt_color_t fg;
|
||||
tmt_color_t bg;
|
||||
};
|
||||
|
||||
typedef struct TMTCHAR TMTCHAR;
|
||||
struct TMTCHAR{
|
||||
wchar_t c;
|
||||
TMTATTRS a;
|
||||
};
|
||||
|
||||
typedef struct TMTPOINT TMTPOINT;
|
||||
struct TMTPOINT{
|
||||
size_t r;
|
||||
size_t c;
|
||||
};
|
||||
|
||||
typedef struct TMTLINE TMTLINE;
|
||||
struct TMTLINE{
|
||||
bool dirty;
|
||||
TMTCHAR chars[];
|
||||
};
|
||||
|
||||
typedef struct TMTSCREEN TMTSCREEN;
|
||||
struct TMTSCREEN{
|
||||
size_t nline;
|
||||
size_t ncol;
|
||||
|
||||
TMTLINE **lines;
|
||||
};
|
||||
|
||||
/**** CALLBACK SUPPORT */
|
||||
typedef enum{
|
||||
TMT_MSG_MOVED,
|
||||
TMT_MSG_UPDATE,
|
||||
TMT_MSG_ANSWER,
|
||||
TMT_MSG_BELL,
|
||||
TMT_MSG_CURSOR
|
||||
} tmt_msg_t;
|
||||
|
||||
typedef void (*TMTCALLBACK)(tmt_msg_t m, struct TMT *v, const void *r, void *p);
|
||||
|
||||
/**** PUBLIC FUNCTIONS */
|
||||
TMT *tmt_open(size_t nline, size_t ncol, TMTCALLBACK cb, void *p,
|
||||
const wchar_t *acs);
|
||||
void tmt_close(TMT *vt);
|
||||
bool tmt_resize(TMT *vt, size_t nline, size_t ncol);
|
||||
void tmt_write(TMT *vt, const char *s, size_t n);
|
||||
const TMTSCREEN *tmt_screen(const TMT *vt);
|
||||
const TMTPOINT *tmt_cursor(const TMT *vt);
|
||||
void tmt_clean(TMT *vt);
|
||||
void tmt_reset(TMT *vt);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,209 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <locale.h>
|
||||
#include <unistd.h>
|
||||
#include "tmt.h"
|
||||
#define HASHSIZE 2053
|
||||
#define MAXNAMELEN 256
|
||||
#define MAXDATALEN 8192
|
||||
struct terment {
|
||||
struct terment *next;
|
||||
char *name;
|
||||
TMT *vt;
|
||||
};
|
||||
|
||||
#define SETNODE 1
|
||||
#define WRITE 2
|
||||
#define READBUFF 0
|
||||
static struct terment *buffers[HASHSIZE];
|
||||
|
||||
unsigned long hash(char *str)
|
||||
/* djb2a */
|
||||
{
|
||||
unsigned long idx = 5381;
|
||||
int c;
|
||||
|
||||
while ((c = *str++))
|
||||
idx = ((idx << 5) + idx) + c;
|
||||
return idx % HASHSIZE;
|
||||
}
|
||||
|
||||
TMT *get_termentbyname(char *name) {
|
||||
struct terment *ret;
|
||||
for (ret = buffers[hash(name)]; ret != NULL; ret = ret->next)
|
||||
if (strcmp(name, ret->name) == 0)
|
||||
return ret->vt;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
TMT *set_termentbyname(char *name) {
|
||||
struct terment *ret;
|
||||
int idx;
|
||||
|
||||
idx = hash(name);
|
||||
for (ret = buffers[idx]; ret != NULL; ret = ret->next)
|
||||
if (strcmp(name, ret->name) == 0)
|
||||
return ret->vt;
|
||||
ret = (struct terment *)malloc(sizeof(*ret));
|
||||
ret->next = buffers[idx];
|
||||
ret->name = strdup(name);
|
||||
ret->vt = tmt_open(31, 100, NULL, NULL, L"→←↑↓■◆▒°±▒┘┐┌└┼⎺───⎽├┤┴┬│≤≥π≠£•");
|
||||
buffers[idx] = ret;
|
||||
return ret->vt;
|
||||
}
|
||||
|
||||
void dump_vt(TMT* outvt) {
|
||||
const TMTSCREEN *out = tmt_screen(outvt);
|
||||
const TMTPOINT *curs = tmt_cursor(outvt);
|
||||
int line, idx, maxcol, maxrow;
|
||||
bool bold = false;
|
||||
bool dim = false;
|
||||
bool underline = false;
|
||||
bool blink = false;
|
||||
bool reverse = false;
|
||||
bool invisible = false;
|
||||
bool intensitychg = false;
|
||||
tmt_color_t fg = TMT_COLOR_DEFAULT;
|
||||
tmt_color_t bg = TMT_COLOR_DEFAULT;
|
||||
wchar_t sgrline[30];
|
||||
size_t srgidx = 0;
|
||||
char colorcode = 0;
|
||||
wprintf(L"\033c");
|
||||
maxcol = 0;
|
||||
maxrow = 0;
|
||||
for (line = out->nline - 1; line >= 0; --line) {
|
||||
for (idx = out->ncol - 1; idx > maxcol; --idx) {
|
||||
if (out->lines[line]->chars[idx].c != L' ') {
|
||||
if (maxrow < line)
|
||||
maxrow = line;
|
||||
maxcol = idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (line = 0; line <= maxrow; line++) {
|
||||
for (idx = 0; idx <= maxcol; idx++) {
|
||||
sgrline[0] = L'\x00';
|
||||
intensitychg = false;
|
||||
if (out->lines[line]->chars[idx].a.bold != bold) {
|
||||
bold = out->lines[line]->chars[idx].a.bold;
|
||||
intensitychg = true; // Can't unbold without changing dim
|
||||
}
|
||||
if (out->lines[line]->chars[idx].a.dim != dim) {
|
||||
dim = out->lines[line]->chars[idx].a.dim;
|
||||
intensitychg = true; // Can't undim without changing bold
|
||||
}
|
||||
if (intensitychg) {
|
||||
intensitychg = false;
|
||||
wcscat(sgrline, L"22;");
|
||||
if (bold)
|
||||
wcscat(sgrline, L"1;");
|
||||
if (dim)
|
||||
wcscat(sgrline, L"2;");
|
||||
}
|
||||
if (out->lines[line]->chars[idx].a.underline != underline) {
|
||||
underline = out->lines[line]->chars[idx].a.underline;
|
||||
if (underline)
|
||||
wcscat(sgrline, L"4;");
|
||||
else
|
||||
wcscat(sgrline, L"24;");
|
||||
}
|
||||
if (out->lines[line]->chars[idx].a.blink != blink) {
|
||||
blink = out->lines[line]->chars[idx].a.blink;
|
||||
if (blink)
|
||||
wcscat(sgrline, L"5;");
|
||||
else
|
||||
wcscat(sgrline, L"25;");
|
||||
}
|
||||
if (out->lines[line]->chars[idx].a.reverse != reverse) {
|
||||
reverse = out->lines[line]->chars[idx].a.reverse;
|
||||
if (reverse)
|
||||
wcscat(sgrline, L"7;");
|
||||
else
|
||||
wcscat(sgrline, L"27;");
|
||||
}
|
||||
if (out->lines[line]->chars[idx].a.invisible != invisible) {
|
||||
invisible = out->lines[line]->chars[idx].a.invisible;
|
||||
if (invisible)
|
||||
wcscat(sgrline, L"8;");
|
||||
else
|
||||
wcscat(sgrline, L"28;");
|
||||
}
|
||||
if (out->lines[line]->chars[idx].a.fg != fg) {
|
||||
fg = out->lines[line]->chars[idx].a.fg;
|
||||
if (fg == TMT_COLOR_DEFAULT)
|
||||
colorcode = 39;
|
||||
else
|
||||
colorcode = 29 + fg;
|
||||
swprintf(sgrline + wcslen(sgrline), 4, L"%d;", colorcode);
|
||||
}
|
||||
if (out->lines[line]->chars[idx].a.bg != bg) {
|
||||
bg = out->lines[line]->chars[idx].a.bg;
|
||||
if (bg == TMT_COLOR_DEFAULT)
|
||||
colorcode = 49;
|
||||
else
|
||||
colorcode = 39 + bg;
|
||||
swprintf(sgrline + wcslen(sgrline), 4, L"%d;", colorcode);
|
||||
}
|
||||
if (sgrline[0] != 0) {
|
||||
sgrline[wcslen(sgrline) - 1] = 0; // Trim last ;
|
||||
wprintf(L"\033[%lsm", sgrline);
|
||||
}
|
||||
wprintf(L"%lc", out->lines[line]->chars[idx].c);
|
||||
}
|
||||
if (line < maxrow)
|
||||
wprintf(L"\r\n");
|
||||
}
|
||||
fflush(stdout);
|
||||
wprintf(L"\x1b[%ld;%ldH", curs->r + 1, curs->c + 1);
|
||||
fflush(stdout);
|
||||
idx = write(1, "\x00", 1);
|
||||
if (idx < 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
int cmd, length;
|
||||
setlocale(LC_ALL, "");
|
||||
char cmdbuf[MAXDATALEN];
|
||||
char currnode[MAXNAMELEN];
|
||||
TMT *currvt = NULL;
|
||||
TMT *outvt = NULL;
|
||||
stdin = freopen(NULL, "rb", stdin);
|
||||
if (stdin == NULL) {
|
||||
exit(1);
|
||||
}
|
||||
while (1) {
|
||||
length = fread(&cmd, 4, 1, stdin);
|
||||
if (length < 0)
|
||||
continue;
|
||||
length = cmd & 536870911;
|
||||
cmd = cmd >> 29;
|
||||
if (cmd == SETNODE) {
|
||||
currnode[length] = 0;
|
||||
cmd = fread(currnode, 1, length, stdin);
|
||||
if (cmd < 0)
|
||||
continue;
|
||||
currvt = set_termentbyname(currnode);
|
||||
} else if (cmd == WRITE) {
|
||||
if (currvt == NULL)
|
||||
currvt = set_termentbyname("");
|
||||
cmdbuf[length] = 0;
|
||||
cmd = fread(cmdbuf, 1, length, stdin);
|
||||
if (cmd < 0)
|
||||
continue;
|
||||
tmt_write(currvt, cmdbuf, length);
|
||||
} else if (cmd == READBUFF) {
|
||||
cmdbuf[length] = 0;
|
||||
cmd = fread(cmdbuf, 1, length, stdin);
|
||||
if (cmd < 0)
|
||||
continue;
|
||||
outvt = get_termentbyname(cmdbuf);
|
||||
if (outvt != NULL) {
|
||||
dump_vt(outvt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user