Commit 8a0b4b16 authored by Xavier Thompson's avatar Xavier Thompson

WIP

parent 07359452
...@@ -25,31 +25,43 @@ ...@@ -25,31 +25,43 @@
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
# #
############################################################################## ##############################################################################
import configparser
import ipaddress
import logging
import netifaces
from netifaces import AF_INET, AF_INET6
from typing import List, Union
def do_format(conf): def do_format(conf):
# load configuration
computer = Computer(conf) computer = Computer(conf)
computer.check() # sanity checks
computer.validate()
# format
computer.format() computer.format()
# ??
computer.update() computer.update()
# send to master
computer.send() computer.send()
class FormatConfig(object): class Parameters(object):
class Parameters(object):
master_url: str master_url: str
computer_id: str computer_id: str
instance_root: str instance_root: str
software_root: str software_root: str
partition_amount: int partition_amount: int
class FileOptions(object): class Options(object):
input_definition_file: str = None
computer_cache_file: str = None
key_file: str = None key_file: str = None
cert_file: str = None cert_file: str = None
master_ca_file: str = None
input_definition_file: str = None
class Options(object):
log_file: str = None log_file: str = None
dry_run: bool = False dry_run: bool = False
...@@ -72,6 +84,18 @@ class FormatConfig(object): ...@@ -72,6 +84,18 @@ class FormatConfig(object):
tun_ipv6: bool = True tun_ipv6: bool = True
tun_ipv4_network: str = '172.16.0.0/12' tun_ipv4_network: str = '172.16.0.0/12'
class FormatConfig(Parameters, Options):
DEPRECATED = [
'bridge_name',
'no_bridge_',
'master_ca_file',
'alter_network',
'alter_user',
'computer_json',
'computer_xml',
]
CHECK_FILES = ['key_file', 'cert_file', 'input_definition_file']
logger : logging.Logger logger : logging.Logger
def __init__(self, logger): def __init__(self, logger):
...@@ -85,54 +109,64 @@ class FormatConfig(object): ...@@ -85,54 +109,64 @@ class FormatConfig(object):
def parse(self, name, value, t): def parse(self, name, value, t):
if not isinstance(value, str): if not isinstance(value, str):
if not isinstance(value, t): if not isinstance(value, t):
self.error("Option %s is not of type %s", name, t.__name__) self.error("Option %s takes type %s, not %r", name, t.__name__, value)
return value return value
if t in (int,): if t in (int,):
try: try:
return t(value) return t(value)
except ValueError: except ValueError:
self.error("Option %s must be an integer, not %r", name, value) self.error("Option %s takes type %s, not %r", name, t.__name__, value)
if t is bool: if t is bool:
try: try:
return {'true': True, 'false': False}[value.lower()] return {'true': True, 'false': False}[value.lower()]
except KeyError: except KeyError:
self.error("Option %r must be 'True' or 'False', not %r", name, value) self.error("Option %r must be 'true' or 'false', not %r", name, value)
def get(self, option):
try:
return gettatr(self, option)
except AttributeError:
self.error("Parameter %r is not defined", option)
def mergeConfig(self, args, configp): def mergeConfig(self, args, configp):
for cls in (self.FileOptions, self.Options):
self.__dict__.update({k: cls.__dict__[k] for k in cls.__annotations__})
# args (from command line) override configp (from cfg) options # args (from command line) override configp (from cfg) options
for section in ('slapformat', 'slapos'): for section in ('slapformat', 'slapos'):
self.__dict__.update(configp.items(section)) self.__dict__.update(configp.items(section))
self.__dict__.update(args.__dict__) self.__dict__.update(args.__dict__)
def setConfig(self): def setConfig(self):
for parameter, t in self.Parameters.__annotations__.items(): for option in self.DEPRECATED:
try: if option in self.__dict__:
value = gettatr(self, parameter) if option == 'computer_xml':
except AttributeError: self.error(
raise UsageError("Parameter %r is not defined" % option) "Option %r is no longer supported\n"
setattr(self, parameter, self.parse(parameters, value, t)) "Use --output_definition_file to migrate existing computer_xml"
", then use the generated file as input_definition_file",
option
)
else:
self.error("Option %r is no longer supported" % option)
for option, t in Parameters.__annotations__.items():
setattr(self, option, self.parse(option, self.get(option), t))
for option, t in self.Options.__annotations__.items(): for option, t in self.__annotations__.items():
setattr(self, option, self.parse(option, getattr(self, option), t)) setattr(self, option, self.parse(option, getattr(self, option), t))
for option in self.FileOptions.__annotations__: for option in self.CHECK_FILES:
path = getattr(self, option) path = getattr(self, option)
if path is not None and not os.path.exists(path): if path is not None and not os.path.exists(path):
self.error("File %r does not exist or is not readable", path) self.error("File %r does not exist or is not readable", path)
setattr(self, option, os.path.abspath(path)) setattr(self, option, os.path.abspath(path))
# XXX # XXX Check command line tools + Logs
# Check command line tools
# Logs
class Computer(object): class Computer(object):
reference : str reference : str
interface : Interface interface : Interface
partitions : [Partitions] partitions : List[Partition]
address : ipaddress.IPv4Interface, ipaddress.IPv6Interface address : Union[ipaddress.IPv4Interface, ipaddress.IPv6Interface]
user : User user : User
conf : FormatConfig conf : FormatConfig
...@@ -141,64 +175,96 @@ class Computer(object): ...@@ -141,64 +175,96 @@ class Computer(object):
self.reference = conf.computer_id self.reference = conf.computer_id
self.interface = Interface(conf) self.interface = Interface(conf)
if conf.input_definition_file: if conf.input_definition_file:
computer_definition = configparser.ConfigParser(interpolation=None) definition = configparser.ConfigParser(interpolation=None)
computer_definition.read(conf.input_definition_file) definition.read(conf.input_definition_file)
self.from_definition(conf, computer_definition) self.from_definition(conf, definition)
else: else:
self.from_conf(conf) self.from_conf(conf)
def from_definition(self, conf, computer_definition) def from_definition(self, conf, definition)
self.partitions = [ amount = conf.partition_amount
Partition.from_definition(index, conf, computer_definition) self.partitions = [Partition(i, conf, definition) for i in range(amount)]
for index in conf.partition_amount address = definition.get('computer', 'address', fallback=None) # XXX fallback
]
address = computer_definition.get('computer', 'address', fallback=None) # XXX
self.address = ipaddress.ip_interface(address) self.address = ipaddress.ip_interface(address)
username = computer_definition.get( username = definition.get('computer', 'software_user', fallback=conf.software_user)
'computer', 'software_user', fallback=conf.software_user) self.user = User(username)
self.user = User(conf, username)
def from_conf(self, conf): def from_conf(self, conf):
self.partitions = [ amount = conf.partition_amount
Partition.from_conf(conf) self.partitions = [Partition(i, conf) for i in range(amount)]
for index in conf.partition_amount
]
self.address = None # XXX self.address = None # XXX
self.user = User(conf, conf.software_user) self.user = User(conf.software_user)
def check(self): def validate(self):
pass pass
def format(self): def format(self):
pass pass
def update(self):
pass
def send(self):
pass
class Interface(object): class Interface(object):
ipv4_interface : str ipv4_interface : str
ipv6_interface : str ipv6_interface : str
ipv4_local_network : ipaddress.IPv4Network ipv4_network : ipaddress.IPv4Network
ipv6_network : ipaddress.IPv6Network ipv6_network : ipaddress.IPv6Network
conf : FormatConfig conf : FormatConfig
def __init__(self, conf): def __init__(self, conf):
self.conf self.conf = conf
self.ipv4_interface = conf.interface_name self.ipv4_interface = conf.interface_name
self.ipv6_interface = conf.ipv6_interface or conf.interface_name self.ipv6_interface = conf.ipv6_interface or conf.interface_name
self.ipv4_local_network = conf.ipv4_local_network self.ipv4_network = self.getIPv4Network(conf.ipv4_local_network)
self.ipv6_interface_address = self.getIPv6Network() self.ipv6_network = self.getIPv6Network()
def getIPv4Network(self, cidr):
if cidr:
# XXX allow ipv4_local_network to be None ?
return ipaddress.IPv4Network(cidr, strict=False)
def getIPv6InterfaceAddress(self): def getPartitionIPv4(self, index):
return self.ipv4_network[index + 2]
def getIPv6Network(self):
try: try:
if_addresses = netifaces.ifaddresses(self.ipv6_interface)[socket.AF_INET6] addresses = netifaces.ifaddresses(self.ipv6_interface)[AF_INET6]
except KeyError: except KeyError:
self.conf.error("%s must have at least one IPv6 address assigned", self.ipv6_interface) self.conf.error(
addr = None "%s must have at least one IPv6 address assigned",
for a in if_addresses: self.ipv6_interface
ip = ipaddress.ip_interface((q['addr']split('%')[0], q['netmask'])) )
if isGlobalScopeAddress(ip): result = None
if not addr or ip.network.num_addresses > addr.network.num_addresses: for a in addresses:
addr = ip address = a['addr'].split('%')[0]
return addr netmask = a['netmask'].split('/')[-1]
ip = ipaddress.IPv6Interface('%s/%s' % (address, netmask))
network = ip.network
if network.is_global:
if not result or network.prefixlen < result.prefixlen:
result = network
return result
def getComputerIPv6Addr(self):
network = self.ipv6_network
return ipaddress.ip_interface((network[1], network.prefixlen))
def getPartitionIPv6Addr(self, index):
network = self.ipv6_network
return ipaddress.ip_interface((network[index + 2], network.prefixlen))
def getPartitionIPv6Range(self, index):
network = self.ipv6_network
prefixlen = network.prefixlen + 16
if prefixlen > 128:
self.conf.error("IPv6 network %s is too small for IPv6 ranges", network)
bits = 128 - network.prefixlen
addr = network[(1 << (bits - 2)) + (i << 14)]
return ipaddress.ip_interface((addr, prefixlen))
class Partition(object): class Partition(object):
...@@ -206,78 +272,76 @@ class Partition(object): ...@@ -206,78 +272,76 @@ class Partition(object):
index: int index: int
path : str path : str
user : User user : User
address_list : list ipv4_list: List[ipaddress.IPv4Interface]
ipv6_list: List[ipaddress.IPv6Interface]
ipv6_range: ipaddress.IPv6Interface
tap : Tap tap : Tap
tun : Tun tun : Tun
external_storage_list: list
def __init__(self, **kwargs): def __init__(self, index, computer, definition=None):
self.__dict__.update(kwargs) self.from_conf(index, computer)
if definition:
self.from_definition(index, computer.conf, definition)
@classmethod def from_definition(cls, index, conf, definition):
def from_definition(cls, index, conf, computer_definition):
section = 'partition_%d' % index section = 'partition_%d' % index
if computer_definition.has_section(section): options = {}
reference = computer_definition.get(section, 'pathname') if definition.has_section('default'):
path = os.path.join(conf.instance_root, reference) options.update(definition.items('default'))
user = User(conf, computer_definition.get(section, 'user'), path) if definition.has_section(section):
address_list = [ options.update(definition.items(section))
ipaddress.ip_interface(addr) if 'pathname' in options:
for add in computer_definition.get(section, 'address') self.reference = options['pathname']
] # XXX fallback self.path = os.path.join(conf.instance_root, self.reference)
if 'user' in options:
self.user = User(options['user'], self.path)
if 'address' in options:
address_list = [ipaddress.ip_interface(a) for a in options['address']]
for v in (4, 6):
ip_list = [ip for ip in ip_addresses if ip.version == v]
if ip_list:
setattr(self, 'ipv%d_list' % v, ip_list)
# tap = Tap(computer_definition.get(section, 'network_interface')) # tap = Tap(computer_definition.get(section, 'network_interface'))
# tun = Tun.load(conf, index) # tun = Tun.load(conf, index)
return Partition(
reference = reference,
index = index,
path = path,
user = user,
address_list = address_list,
# tap = tap,
# tun = tun
)
# XXX [default] ??
else:
return cls.from_conf(index, conf)
@classmethod def from_conf(self, index, computer):
def from_conf(cls, conf): conf = computer.conf
reference = '%s%d' % (conf.partition_base_name, i) self.reference = '%s%d' % (conf.partition_base_name, index)
path = os.path.join(conf.instance_root, reference) self.path = os.path.join(conf.instance_root, self.reference)
user = User(conf, '%s%d' % (conf.user_base_name, i), path) self.user = User('%s%d' % (conf.user_base_name, index), self.path)
address_list = None # XXX self.ipv4_list = [computer.interface.getPartitionIPv4(index)]
return Partition( self.ipv6_list = [computer.interface.getPartitionIPv6(index)]
reference = reference, # XXX Tap & tun
path = path,
user = user, def createPath(self):
address_list = address_list, self.path = os.path.abspath(self.path)
# tap = tap, owner = self.user if self.user else User('root')
# tun = tun, if not os.path.exists(self.path):
) os.mkdir(self.path, 0o750)
owner_pw = pwd.getpwnam(owner.name)
os.chown(self.path, owner_pw.pw_uid, owner_pw.pw_gid)
os.chmod(self.path, 0o750)
class User(object): class User(object):
name: str name: str
path: str path: str
additional_group_list: list groups: List[str]
SHELL = '/bin/sh' SHELL = '/bin/sh'
def __init__(self, conf, name, path=None, additional_group_list=None): def __init__(self, name, path, groups=None):
self.alter_user = conf.alter_user
self.name = name self.name = name
self.path = path self.path = path
self.additional_group_list = additional_group_list self.groups = groups
def apply(self): def create(self):
if not self.alter_user:
return
grpname = 'grp_' + self.name if sys.platform == 'cygwin' else self.name grpname = 'grp_' + self.name if sys.platform == 'cygwin' else self.name
if not self.isGroupAvailable(grpname): if not self.isGroupAvailable(grpname):
callAndRead(['groupadd', grpname]) callAndRead(['groupadd', grpname])
user_parameter_list = ['-d', self.path, '-g', self.name, '-s', self.SHELL] user_parameter_list = ['-d', self.path, '-g', self.name, '-s', self.SHELL]
if self.additional_group_list: if self.groups:
user_parameter_list.extend(['-G', ','.join(self.additional_group_list)]) user_parameter_list.extend(['-G', ','.join(self.groups), '-a'])
user_parameter_list.append(self.name) user_parameter_list.append(self.name)
if self.isUserAvailable(self.name): if self.isUserAvailable(self.name):
# if the user is already created and used we should not fail # if the user is already created and used we should not fail
...@@ -305,10 +369,17 @@ class User(object): ...@@ -305,10 +369,17 @@ class User(object):
return False return False
# Utils # Utilities
def isGlobalScopeAddress(ip): def callAndRead(argument_list, raise_on_error=True):
return ( popen = subprocess.Popen(
not ip.is_link_local and not ip.is_loopback argument_list,
and not ip.is_reserved and not ip.is_multicast stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
) )
result = popen.communicate()[0]
if raise_on_error and popen.returncode != 0:
raise ValueError('Issue while invoking %r, result was:\n%s' % (
argument_list, result))
return popen.returncode, result
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment