mirror of
https://github.com/freeipa/ansible-freeipa.git
synced 2026-03-26 21:33:05 +00:00
Since FreeIPA version 4.10 it is possible to deploy servers that use Random Serial Number v3 support for certificates. This patch exposes the 'random_serial_numbers' parameter, as 'ipaserver_random_serial_numbers', allowing a user to have random serial numbers enabled for the domain. The use of random serial numbers is allowed on new installations only.
1207 lines
46 KiB
Python
1207 lines
46 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Authors:
|
|
# Thomas Woerner <twoerner@redhat.com>
|
|
#
|
|
# Based on ipa-client-install code
|
|
#
|
|
# Copyright (C) 2017-2022 Red Hat
|
|
# see file 'COPYING' for use and warranty information
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
from __future__ import (absolute_import, division, print_function)
|
|
|
|
__metaclass__ = type
|
|
|
|
ANSIBLE_METADATA = {
|
|
'metadata_version': '1.0',
|
|
'supported_by': 'community',
|
|
'status': ['preview'],
|
|
}
|
|
|
|
DOCUMENTATION = '''
|
|
---
|
|
module: ipaserver_test
|
|
short_description: IPA server test
|
|
description: IPA server test
|
|
options:
|
|
force:
|
|
description: Installer force parameter
|
|
type: bool
|
|
default: no
|
|
required: no
|
|
dm_password:
|
|
description: Directory Manager password
|
|
type: str
|
|
required: yes
|
|
password:
|
|
description: Admin user kerberos password
|
|
type: str
|
|
required: yes
|
|
master_password:
|
|
description: kerberos master password (normally autogenerated)
|
|
type: str
|
|
required: no
|
|
domain:
|
|
description: Primary DNS domain of the IPA deployment
|
|
type: str
|
|
required: no
|
|
realm:
|
|
description: Kerberos realm name of the IPA deployment
|
|
type: str
|
|
required: no
|
|
hostname:
|
|
description: Fully qualified name of this host
|
|
type: str
|
|
required: no
|
|
ca_cert_files:
|
|
description:
|
|
List of files containing CA certificates for the service certificate
|
|
files
|
|
type: list
|
|
elements: str
|
|
required: no
|
|
no_host_dns:
|
|
description: Do not use DNS for hostname lookup during installation
|
|
type: bool
|
|
default: no
|
|
required: no
|
|
pki_config_override:
|
|
description: Path to ini file with config overrides
|
|
type: str
|
|
required: no
|
|
skip_mem_check:
|
|
description: Skip checking for minimum required memory
|
|
type: bool
|
|
default: no
|
|
required: no
|
|
setup_adtrust:
|
|
description: Configure AD trust capability
|
|
type: bool
|
|
default: no
|
|
required: no
|
|
setup_kra:
|
|
description: Configure a dogtag KRA
|
|
type: bool
|
|
default: no
|
|
required: no
|
|
setup_dns:
|
|
description: Configure bind with our zone
|
|
type: bool
|
|
default: no
|
|
required: no
|
|
idstart:
|
|
description: The starting value for the IDs range (default random)
|
|
type: int
|
|
required: no
|
|
idmax:
|
|
description: The max value for the IDs range (default idstart+199999)
|
|
type: int
|
|
required: no
|
|
no_pkinit:
|
|
description: Disable pkinit setup steps
|
|
type: bool
|
|
default: no
|
|
required: no
|
|
dirsrv_config_file:
|
|
description:
|
|
The path to LDIF file that will be used to modify configuration of
|
|
dse.ldif during installation of the directory server instance
|
|
type: str
|
|
required: no
|
|
dirsrv_cert_files:
|
|
description:
|
|
Files containing the Directory Server SSL certificate and private key
|
|
type: list
|
|
elements: str
|
|
required: no
|
|
http_cert_files:
|
|
description:
|
|
File containing the Apache Server SSL certificate and private key
|
|
type: list
|
|
elements: str
|
|
required: no
|
|
pkinit_cert_files:
|
|
description:
|
|
File containing the Kerberos KDC SSL certificate and private key
|
|
type: list
|
|
elements: str
|
|
required: no
|
|
dirsrv_pin:
|
|
description: The password to unlock the Directory Server private key
|
|
type: str
|
|
required: no
|
|
http_pin:
|
|
description: The password to unlock the Apache Server private key
|
|
type: str
|
|
required: no
|
|
pkinit_pin:
|
|
description: The password to unlock the Kerberos KDC private key
|
|
type: str
|
|
required: no
|
|
dirsrv_cert_name:
|
|
description: Name of the Directory Server SSL certificate to install
|
|
type: str
|
|
required: no
|
|
http_cert_name:
|
|
description: Name of the Apache Server SSL certificate to install
|
|
type: str
|
|
required: no
|
|
pkinit_cert_name:
|
|
description: Name of the Kerberos KDC SSL certificate to install
|
|
type: str
|
|
required: no
|
|
ntp_servers:
|
|
description: ntp servers to use
|
|
type: list
|
|
elements: str
|
|
required: no
|
|
ntp_pool:
|
|
description: ntp server pool to use
|
|
type: str
|
|
required: no
|
|
no_ntp:
|
|
description: Do not configure ntp
|
|
type: bool
|
|
default: no
|
|
required: no
|
|
external_ca:
|
|
description: External ca setting
|
|
type: bool
|
|
default: no
|
|
required: no
|
|
external_ca_type:
|
|
description: Type of the external CA
|
|
type: str
|
|
required: no
|
|
external_ca_profile:
|
|
description:
|
|
Specify the certificate profile/template to use at the external CA
|
|
type: str
|
|
required: no
|
|
external_cert_files:
|
|
description:
|
|
File containing the IPA CA certificate and the external CA certificate
|
|
chain
|
|
type: list
|
|
elements: str
|
|
required: no
|
|
subject_base:
|
|
description:
|
|
The certificate subject base (default O=<realm-name>).
|
|
RDNs are in LDAP order (most specific RDN first).
|
|
type: str
|
|
required: no
|
|
ca_subject:
|
|
description: The installer ca_subject setting
|
|
type: str
|
|
required: no
|
|
random_serial_numbers:
|
|
description: The installer random_serial_numbers setting
|
|
type: bool
|
|
required: no
|
|
allow_zone_overlap:
|
|
description: Create DNS zone even if it already exists
|
|
type: bool
|
|
default: no
|
|
required: no
|
|
reverse_zones:
|
|
description: The reverse DNS zones to use
|
|
type: list
|
|
elements: str
|
|
required: no
|
|
no_reverse:
|
|
description: Do not create new reverse DNS zone
|
|
type: bool
|
|
default: no
|
|
required: no
|
|
auto_reverse:
|
|
description: Create necessary reverse zones
|
|
type: bool
|
|
default: no
|
|
required: no
|
|
zonemgr:
|
|
description: DNS zone manager e-mail address. Defaults to hostmaster@DOMAIN
|
|
type: str
|
|
required: no
|
|
forwarders:
|
|
description: Add DNS forwarders
|
|
type: list
|
|
elements: str
|
|
required: no
|
|
no_forwarders:
|
|
description: Do not add any DNS forwarders, use root servers instead
|
|
type: bool
|
|
default: no
|
|
required: no
|
|
auto_forwarders:
|
|
description: Use DNS forwarders configured in /etc/resolv.conf
|
|
type: bool
|
|
default: no
|
|
required: no
|
|
forward_policy:
|
|
description: DNS forwarding policy for global forwarders
|
|
type: str
|
|
choices: ['first', 'only']
|
|
required: no
|
|
no_dnssec_validation:
|
|
description: Disable DNSSEC validation
|
|
type: bool
|
|
default: no
|
|
required: no
|
|
enable_compat:
|
|
description: Enable support for trusted domains for old clients
|
|
type: bool
|
|
default: no
|
|
required: no
|
|
netbios_name:
|
|
description: NetBIOS name of the IPA domain
|
|
type: str
|
|
required: no
|
|
rid_base:
|
|
description: Start value for mapping UIDs and GIDs to RIDs
|
|
type: int
|
|
default: 1000
|
|
required: no
|
|
secondary_rid_base:
|
|
description:
|
|
Start value of the secondary range for mapping UIDs and GIDs to RIDs
|
|
type: int
|
|
default: 100000000
|
|
required: no
|
|
author:
|
|
- Thomas Woerner (@t-woerner)
|
|
'''
|
|
|
|
EXAMPLES = '''
|
|
'''
|
|
|
|
RETURN = '''
|
|
'''
|
|
|
|
import os
|
|
import sys
|
|
import random
|
|
from shutil import copyfile
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
from ansible.module_utils._text import to_native
|
|
from ansible.module_utils.ansible_ipa_server import (
|
|
check_imports,
|
|
AnsibleModuleLog, setup_logging, options, adtrust_imported, kra_imported,
|
|
PKIIniLoader, MIN_DOMAIN_LEVEL, MAX_DOMAIN_LEVEL, check_zone_overlap,
|
|
redirect_stdout, validate_dm_password, validate_admin_password,
|
|
NUM_VERSION, is_ipa_configured, sysrestore, paths, bindinstance,
|
|
read_cache, ca, tasks, check_ldap_conf, timeconf, httpinstance,
|
|
check_dirsrv, ScriptError, get_fqdn, verify_fqdn, BadHostError,
|
|
validate_domain_name, load_pkcs12, IPA_PYTHON_VERSION,
|
|
encode_certificate, check_available_memory, getargspec, adtrustinstance,
|
|
get_min_idstart, SerialNumber
|
|
)
|
|
from ansible.module_utils import six
|
|
|
|
if six.PY3:
|
|
unicode = str
|
|
|
|
|
|
def main():
|
|
ansible_module = AnsibleModule(
|
|
argument_spec=dict(
|
|
# basic
|
|
force=dict(required=False, type='bool', default=False),
|
|
dm_password=dict(required=True, type='str', no_log=True),
|
|
password=dict(required=True, type='str', no_log=True),
|
|
master_password=dict(required=False, type='str', no_log=True),
|
|
domain=dict(required=False, type='str'),
|
|
realm=dict(required=False, type='str'),
|
|
hostname=dict(required=False, type='str'),
|
|
ca_cert_files=dict(required=False, type='list', elements='str',
|
|
default=[]),
|
|
no_host_dns=dict(required=False, type='bool', default=False),
|
|
pki_config_override=dict(required=False, type='str'),
|
|
skip_mem_check=dict(required=False, type='bool', default=False),
|
|
# server
|
|
setup_adtrust=dict(required=False, type='bool', default=False),
|
|
setup_kra=dict(required=False, type='bool', default=False),
|
|
setup_dns=dict(required=False, type='bool', default=False),
|
|
idstart=dict(required=False, type='int'),
|
|
idmax=dict(required=False, type='int'),
|
|
# no_hbac_allow
|
|
no_pkinit=dict(required=False, type='bool', default=False),
|
|
# no_ui_redirect
|
|
dirsrv_config_file=dict(required=False, type='str'),
|
|
# ssl certificate
|
|
dirsrv_cert_files=dict(required=False, type='list', elements='str',
|
|
default=None),
|
|
http_cert_files=dict(required=False, type='list', elements='str',
|
|
default=None),
|
|
pkinit_cert_files=dict(required=False, type='list', elements='str',
|
|
default=None),
|
|
dirsrv_pin=dict(required=False, type='str'),
|
|
http_pin=dict(required=False, type='str'),
|
|
pkinit_pin=dict(required=False, type='str'),
|
|
dirsrv_cert_name=dict(required=False, type='str'),
|
|
http_cert_name=dict(required=False, type='str'),
|
|
pkinit_cert_name=dict(required=False, type='str'),
|
|
# client
|
|
# mkhomedir
|
|
ntp_servers=dict(required=False, type='list', elements='str',
|
|
default=None),
|
|
ntp_pool=dict(required=False, type='str', default=None),
|
|
no_ntp=dict(required=False, type='bool', default=False),
|
|
# ssh_trust_dns
|
|
# no_ssh
|
|
# no_sshd
|
|
# no_dns_sshfp
|
|
# certificate system
|
|
external_ca=dict(required=False, type='bool', default=False),
|
|
external_ca_type=dict(required=False, type='str'),
|
|
external_ca_profile=dict(required=False, type='str'),
|
|
external_cert_files=dict(required=False, type='list',
|
|
elements='str', default=None),
|
|
subject_base=dict(required=False, type='str'),
|
|
ca_subject=dict(required=False, type='str'),
|
|
random_serial_numbers=dict(required=False, type='bool',
|
|
default=False),
|
|
# ca_signing_algorithm
|
|
# dns
|
|
allow_zone_overlap=dict(required=False, type='bool',
|
|
default=False),
|
|
reverse_zones=dict(required=False, type='list', elements='str',
|
|
default=[]),
|
|
no_reverse=dict(required=False, type='bool', default=False),
|
|
auto_reverse=dict(required=False, type='bool', default=False),
|
|
zonemgr=dict(required=False, type='str'),
|
|
forwarders=dict(required=False, type='list', elements='str',
|
|
default=[]),
|
|
no_forwarders=dict(required=False, type='bool', default=False),
|
|
auto_forwarders=dict(required=False, type='bool', default=False),
|
|
forward_policy=dict(required=False, type='str',
|
|
choices=['first', 'only'], default=None),
|
|
no_dnssec_validation=dict(required=False, type='bool',
|
|
default=False),
|
|
# ad trust
|
|
enable_compat=dict(required=False, type='bool', default=False),
|
|
netbios_name=dict(required=False, type='str'),
|
|
rid_base=dict(required=False, type='int', default=1000),
|
|
secondary_rid_base=dict(required=False, type='int',
|
|
default=100000000),
|
|
# additional
|
|
),
|
|
supports_check_mode=False,
|
|
)
|
|
|
|
ansible_module._ansible_debug = True
|
|
check_imports(ansible_module)
|
|
setup_logging()
|
|
ansible_log = AnsibleModuleLog(ansible_module)
|
|
|
|
# set values ############################################################
|
|
|
|
# basic
|
|
options.force = ansible_module.params.get('force')
|
|
options.dm_password = ansible_module.params.get('dm_password')
|
|
options.admin_password = ansible_module.params.get('password')
|
|
options.master_password = ansible_module.params.get('master_password')
|
|
options.domain_name = ansible_module.params.get('domain')
|
|
options.realm_name = ansible_module.params.get('realm')
|
|
options.host_name = ansible_module.params.get('hostname')
|
|
options.ca_cert_files = ansible_module.params.get('ca_cert_files')
|
|
options.no_host_dns = ansible_module.params.get('no_host_dns')
|
|
options.pki_config_override = ansible_module.params.get(
|
|
'pki_config_override')
|
|
options.skip_mem_check = ansible_module.params.get('skip_mem_check')
|
|
# server
|
|
options.setup_adtrust = ansible_module.params.get('setup_adtrust')
|
|
options.setup_dns = ansible_module.params.get('setup_dns')
|
|
options.setup_kra = ansible_module.params.get('setup_kra')
|
|
options.idstart = ansible_module.params.get('idstart')
|
|
options.idmax = ansible_module.params.get('idmax')
|
|
# no_hbac_allow
|
|
options.no_pkinit = ansible_module.params.get('no_pkinit')
|
|
# no_ui_redirect
|
|
options.dirsrv_config_file = ansible_module.params.get(
|
|
'dirsrv_config_file')
|
|
# ssl certificate
|
|
options.dirsrv_cert_files = ansible_module.params.get('dirsrv_cert_files')
|
|
options.http_cert_files = ansible_module.params.get('http_cert_files')
|
|
options.pkinit_cert_files = ansible_module.params.get('pkinit_cert_files')
|
|
options.dirsrv_pin = ansible_module.params.get('dirsrv_pin')
|
|
options.http_pin = ansible_module.params.get('http_pin')
|
|
options.pkinit_pin = ansible_module.params.get('pkinit_pin')
|
|
options.dirsrv_cert_name = ansible_module.params.get('dirsrv_cert_name')
|
|
options.http_cert_name = ansible_module.params.get('http_cert_name')
|
|
options.pkinit_cert_name = ansible_module.params.get('pkinit_cert_name')
|
|
# client
|
|
# mkhomedir
|
|
options.ntp_servers = ansible_module.params.get('ntp_servers')
|
|
options.ntp_pool = ansible_module.params.get('ntp_pool')
|
|
options.no_ntp = ansible_module.params.get('no_ntp')
|
|
# ssh_trust_dns
|
|
# no_ssh
|
|
# no_sshd
|
|
# no_dns_sshfp
|
|
# certificate system
|
|
options.external_ca = ansible_module.params.get('external_ca')
|
|
options.external_ca_type = ansible_module.params.get('external_ca_type')
|
|
options.external_ca_profile = ansible_module.params.get(
|
|
'external_ca_profile')
|
|
options.external_cert_files = ansible_module.params.get(
|
|
'external_cert_files')
|
|
options.subject_base = ansible_module.params.get('subject_base')
|
|
options.ca_subject = ansible_module.params.get('ca_subject')
|
|
options._random_serial_numbers = ansible_module.params.get(
|
|
'random_serial_numbers')
|
|
# ca_signing_algorithm
|
|
# dns
|
|
options.allow_zone_overlap = ansible_module.params.get(
|
|
'allow_zone_overlap')
|
|
options.reverse_zones = ansible_module.params.get('reverse_zones')
|
|
options.no_reverse = ansible_module.params.get('no_reverse')
|
|
options.auto_reverse = ansible_module.params.get('auto_reverse')
|
|
options.zonemgr = ansible_module.params.get('zonemgr')
|
|
options.forwarders = ansible_module.params.get('forwarders')
|
|
options.no_forwarders = ansible_module.params.get('no_forwarders')
|
|
options.auto_forwarders = ansible_module.params.get('auto_forwarders')
|
|
options.forward_policy = ansible_module.params.get('forward_policy')
|
|
options.no_dnssec_validation = ansible_module.params.get(
|
|
'no_dnssec_validation')
|
|
# ad trust
|
|
options.enable_compat = ansible_module.params.get('enable_compat')
|
|
options.netbios_name = ansible_module.params.get('netbios_name')
|
|
options.rid_base = ansible_module.params.get('rid_base')
|
|
options.secondary_rid_base = ansible_module.params.get(
|
|
'secondary_rid_base')
|
|
|
|
# additional
|
|
options.kasp_db_file = None
|
|
|
|
# version specific ######################################################
|
|
|
|
sid_generation_always = False
|
|
if not options.setup_adtrust:
|
|
# pylint: disable=deprecated-method
|
|
argspec = getargspec(adtrustinstance.ADTRUSTInstance.__init__)
|
|
# pylint: enable=deprecated-method
|
|
if "fulltrust" in argspec.args:
|
|
sid_generation_always = True
|
|
else:
|
|
if not adtrust_imported:
|
|
ansible_module.fail_json(msg="adtrust can not be imported")
|
|
|
|
if options.setup_kra and not kra_imported:
|
|
# if "kra" not in options._allow_missing:
|
|
ansible_module.fail_json(msg="kra can not be imported")
|
|
# else:
|
|
# options.setup_kra = False
|
|
# ansible_module.warn(msg="kra is not supported, disabling")
|
|
|
|
if options.pki_config_override is not None:
|
|
if PKIIniLoader is None:
|
|
ansible_module.warn("The use of pki_config_override is not "
|
|
"supported for this IPA version")
|
|
else:
|
|
# From DogtagInstallInterface @pki_config_override.validator
|
|
try:
|
|
PKIIniLoader.verify_pki_config_override(
|
|
options.pki_config_override)
|
|
except ValueError as e:
|
|
ansible_module.fail_json(
|
|
msg="pki_config_override: %s" % str(e))
|
|
|
|
# Check if Random Serial Numbers v3 is available
|
|
if options._random_serial_numbers and SerialNumber is None:
|
|
ansible_module.fail_json(
|
|
msg="Random Serial Numbers is not supported for this IPA version"
|
|
)
|
|
|
|
# default values ########################################################
|
|
|
|
# idstart and idmax
|
|
if options.idstart is None:
|
|
options.idstart = random.randint(1, 10000) * 200000
|
|
if options.idmax is None or options.idmax == 0:
|
|
options.idmax = options.idstart + 199999
|
|
|
|
# ServerInstallInterface.__init__ #######################################
|
|
try:
|
|
self = options
|
|
|
|
# If any of the key file options are selected, all are required.
|
|
cert_file_req = (self.dirsrv_cert_files, self.http_cert_files)
|
|
cert_file_opt = (self.pkinit_cert_files,)
|
|
if not self.no_pkinit:
|
|
cert_file_req += cert_file_opt
|
|
if self.no_pkinit and self.pkinit_cert_files:
|
|
raise RuntimeError(
|
|
"--no-pkinit and --pkinit-cert-file cannot be specified "
|
|
"together"
|
|
)
|
|
if any(cert_file_req + cert_file_opt) and not all(cert_file_req):
|
|
raise RuntimeError(
|
|
"--dirsrv-cert-file, --http-cert-file, and --pkinit-cert-file "
|
|
"or --no-pkinit are required if any key file options are used."
|
|
)
|
|
|
|
if not self.interactive:
|
|
if self.dirsrv_cert_files and self.dirsrv_pin is None:
|
|
raise RuntimeError(
|
|
"You must specify --dirsrv-pin with --dirsrv-cert-file")
|
|
if self.http_cert_files and self.http_pin is None:
|
|
raise RuntimeError(
|
|
"You must specify --http-pin with --http-cert-file")
|
|
if self.pkinit_cert_files and self.pkinit_pin is None:
|
|
raise RuntimeError(
|
|
"You must specify --pkinit-pin with --pkinit-cert-file")
|
|
|
|
if not self.setup_dns:
|
|
if self.forwarders:
|
|
raise RuntimeError(
|
|
"You cannot specify a --forwarder option without the "
|
|
"--setup-dns option")
|
|
if self.auto_forwarders:
|
|
raise RuntimeError(
|
|
"You cannot specify a --auto-forwarders option without "
|
|
"the --setup-dns option")
|
|
if self.no_forwarders:
|
|
raise RuntimeError(
|
|
"You cannot specify a --no-forwarders option without the "
|
|
"--setup-dns option")
|
|
if self.forward_policy:
|
|
raise RuntimeError(
|
|
"You cannot specify a --forward-policy option without the "
|
|
"--setup-dns option")
|
|
if self.reverse_zones:
|
|
raise RuntimeError(
|
|
"You cannot specify a --reverse-zone option without the "
|
|
"--setup-dns option")
|
|
if self.auto_reverse:
|
|
raise RuntimeError(
|
|
"You cannot specify a --auto-reverse option without the "
|
|
"--setup-dns option")
|
|
if self.no_reverse:
|
|
raise RuntimeError(
|
|
"You cannot specify a --no-reverse option without the "
|
|
"--setup-dns option")
|
|
if self.no_dnssec_validation:
|
|
raise RuntimeError(
|
|
"You cannot specify a --no-dnssec-validation option "
|
|
"without the --setup-dns option")
|
|
elif self.forwarders and self.no_forwarders:
|
|
raise RuntimeError(
|
|
"You cannot specify a --forwarder option together with "
|
|
"--no-forwarders")
|
|
elif self.auto_forwarders and self.no_forwarders:
|
|
raise RuntimeError(
|
|
"You cannot specify a --auto-forwarders option together with "
|
|
"--no-forwarders")
|
|
elif self.reverse_zones and self.no_reverse:
|
|
raise RuntimeError(
|
|
"You cannot specify a --reverse-zone option together with "
|
|
"--no-reverse")
|
|
elif self.auto_reverse and self.no_reverse:
|
|
raise RuntimeError(
|
|
"You cannot specify a --auto-reverse option together with "
|
|
"--no-reverse")
|
|
|
|
if not self.setup_adtrust:
|
|
if self.add_agents:
|
|
raise RuntimeError(
|
|
"You cannot specify an --add-agents option without the "
|
|
"--setup-adtrust option")
|
|
|
|
if self.enable_compat:
|
|
raise RuntimeError(
|
|
"You cannot specify an --enable-compat option without the "
|
|
"--setup-adtrust option")
|
|
|
|
# Deactivate test for new IPA SID generation
|
|
if self.netbios_name and not sid_generation_always:
|
|
raise RuntimeError(
|
|
"You cannot specify a --netbios-name option without the "
|
|
"--setup-adtrust option")
|
|
|
|
if self.no_msdcs:
|
|
raise RuntimeError(
|
|
"You cannot specify a --no-msdcs option without the "
|
|
"--setup-adtrust option")
|
|
|
|
if not hasattr(self, 'replica_install'):
|
|
if self.external_cert_files and self.dirsrv_cert_files:
|
|
raise RuntimeError(
|
|
"Service certificate file options cannot be used with the "
|
|
"external CA options.")
|
|
|
|
if self.external_ca_type and not self.external_ca:
|
|
raise RuntimeError(
|
|
"You cannot specify --external-ca-type without "
|
|
"--external-ca")
|
|
|
|
if self.external_ca_profile and not self.external_ca:
|
|
raise RuntimeError(
|
|
"You cannot specify --external-ca-profile without "
|
|
"--external-ca")
|
|
|
|
if self.uninstalling:
|
|
if (self.realm_name or self.admin_password or
|
|
self.master_password):
|
|
raise RuntimeError(
|
|
"In uninstall mode, -a, -r and -P options are not "
|
|
"allowed")
|
|
elif not self.interactive:
|
|
if (not self.realm_name or not self.dm_password or
|
|
not self.admin_password):
|
|
raise RuntimeError(
|
|
"In unattended mode you need to provide at least -r, "
|
|
"-p and -a options")
|
|
if self.setup_dns:
|
|
if (not self.forwarders and
|
|
not self.no_forwarders and
|
|
not self.auto_forwarders):
|
|
raise RuntimeError(
|
|
"You must specify at least one of --forwarder, "
|
|
"--auto-forwarders, or --no-forwarders options")
|
|
|
|
any_ignore_option_true = any(
|
|
[self.ignore_topology_disconnect, self.ignore_last_of_role])
|
|
if any_ignore_option_true and not self.uninstalling:
|
|
raise RuntimeError(
|
|
"'--ignore-topology-disconnect/--ignore-last-of-role' "
|
|
"options can be used only during uninstallation")
|
|
|
|
if get_min_idstart is not None:
|
|
min_idstart = get_min_idstart()
|
|
if self.idstart < min_idstart:
|
|
raise RuntimeError(
|
|
"idstart (%i) must be larger than UID_MAX/GID_MAX "
|
|
"(%i) setting in /etc/login.defs." % (
|
|
self.idstart, min_idstart
|
|
)
|
|
)
|
|
|
|
if self.idmax < self.idstart:
|
|
raise RuntimeError(
|
|
"idmax (%s) cannot be smaller than idstart (%s)" %
|
|
(self.idmax, self.idstart))
|
|
else:
|
|
# replica installers
|
|
if self.servers and not self.domain_name:
|
|
raise RuntimeError(
|
|
"The --server option cannot be used without providing "
|
|
"domain via the --domain option")
|
|
|
|
if self.setup_dns:
|
|
if (not self.forwarders and
|
|
not self.no_forwarders and
|
|
not self.auto_forwarders):
|
|
raise RuntimeError(
|
|
"You must specify at least one of --forwarder, "
|
|
"--auto-forwarders, or --no-forwarders options")
|
|
|
|
except RuntimeError as e:
|
|
ansible_module.fail_json(msg=to_native(e))
|
|
|
|
# #######################################################################
|
|
|
|
# If any of the key file options are selected, all are required.
|
|
cert_file_req = (options.dirsrv_cert_files, options.http_cert_files)
|
|
cert_file_opt = (options.pkinit_cert_files,)
|
|
if not options.no_pkinit:
|
|
cert_file_req += cert_file_opt
|
|
if options.no_pkinit and options.pkinit_cert_files:
|
|
ansible_module.fail_json(
|
|
msg="no-pkinit and pkinit-cert-file cannot be specified together"
|
|
)
|
|
if any(cert_file_req + cert_file_opt) and not all(cert_file_req):
|
|
ansible_module.fail_json(
|
|
msg="dirsrv-cert-file, http-cert-file, and pkinit-cert-file "
|
|
"or no-pkinit are required if any key file options are used."
|
|
)
|
|
|
|
if not options.interactive:
|
|
if options.dirsrv_cert_files and options.dirsrv_pin is None:
|
|
ansible_module.fail_json(
|
|
msg="You must specify dirsrv-pin with dirsrv-cert-file")
|
|
if options.http_cert_files and options.http_pin is None:
|
|
ansible_module.fail_json(
|
|
msg="You must specify http-pin with http-cert-file")
|
|
if options.pkinit_cert_files and options.pkinit_pin is None:
|
|
ansible_module.fail_json(
|
|
msg="You must specify pkinit-pin with pkinit-cert-file")
|
|
|
|
if not options.setup_dns:
|
|
# lists
|
|
for x in ["forwarders", "reverse_zones"]:
|
|
if len(getattr(options, x)) > 1:
|
|
ansible_module.fail_json(
|
|
msg="You cannot specify %s without setting setup-dns" % x)
|
|
# bool and str values
|
|
for x in ["auto_forwarders", "no_forwarders",
|
|
"auto_reverse", "no_reverse", "no_dnssec_validation",
|
|
"forward_policy"]:
|
|
if getattr(options, x):
|
|
ansible_module.fail_json(
|
|
msg="You cannot specify %s without setting setup-dns" % x)
|
|
|
|
elif len(options.forwarders) > 0 and options.no_forwarders:
|
|
ansible_module.fail_json(
|
|
msg="You cannot specify forwarders together with no-forwarders")
|
|
elif options.auto_forwarders and options.no_forwarders:
|
|
ansible_module.fail_json(
|
|
msg="You cannot specify auto-forwarders together with "
|
|
"no-forwarders")
|
|
elif len(options.reverse_zones) > 0 and options.no_reverse:
|
|
ansible_module.fail_json(
|
|
msg="You cannot specify reverse-zones together with no-reverse")
|
|
elif options.auto_reverse and options.no_reverse:
|
|
ansible_module.fail_json(
|
|
msg="You cannot specify auto-reverse together with no-reverse")
|
|
|
|
if not hasattr(self, 'replica_install'):
|
|
if options.external_cert_files and options.dirsrv_cert_files:
|
|
ansible_module.fail_json(
|
|
msg="Service certificate file options cannot be used with the "
|
|
"external CA options.")
|
|
|
|
if options.external_ca_type and not options.external_ca:
|
|
ansible_module.fail_json(
|
|
msg="You cannot specify external-ca-type without external-ca")
|
|
|
|
# if options.uninstalling:
|
|
# if (options.realm_name or options.admin_password or
|
|
# options.master_password):
|
|
# ansible_module.fail_json(
|
|
# msg="In uninstall mode, -a, -r and -P options are not "
|
|
# "allowed")
|
|
# elif not options.interactive:
|
|
# if (not options.realm_name or not options.dm_password or
|
|
# not options.admin_password):
|
|
# ansible_module.fail_json(msg=
|
|
# "In unattended mode you need to provide at least -r, "
|
|
# "-p and -a options")
|
|
# if options.setup_dns:
|
|
# if (not options.forwarders and
|
|
# not options.no_forwarders and
|
|
# not options.auto_forwarders):
|
|
# ansible_module.fail_json(msg=
|
|
# "You must specify at least one of --forwarder, "
|
|
# "--auto-forwarders, or --no-forwarders options")
|
|
if (not options.realm_name or not options.dm_password or
|
|
not options.admin_password):
|
|
ansible_module.fail_json(
|
|
msg="You need to provide at least realm_name, dm_password "
|
|
"and admin_password")
|
|
if options.setup_dns:
|
|
if len(options.forwarders) < 1 and not options.no_forwarders and \
|
|
not options.auto_forwarders:
|
|
ansible_module.fail_json(
|
|
msg="You must specify at least one of forwarders, "
|
|
"auto-forwarders or no-forwarders")
|
|
|
|
# any_ignore_option_true = any(
|
|
# [options.ignore_topology_disconnect, options.ignore_last_of_role])
|
|
# if any_ignore_option_true and not options.uninstalling:
|
|
# ansible_module.fail_json(
|
|
# msg="ignore-topology-disconnect and ignore-last-of-role "
|
|
# "can be used only during uninstallation")
|
|
|
|
if options.idmax < options.idstart:
|
|
ansible_module.fail_json(
|
|
msg="idmax (%s) cannot be smaller than idstart (%s)" %
|
|
(options.idmax, options.idstart))
|
|
|
|
# validation #############################################################
|
|
|
|
if options.dm_password is None:
|
|
ansible_module.fail_json(msg="Directory Manager password required")
|
|
|
|
if options.admin_password is None:
|
|
ansible_module.fail_json(msg="IPA admin password required")
|
|
|
|
# validation ############################################################
|
|
|
|
# domain_level
|
|
if options.domain_level < MIN_DOMAIN_LEVEL:
|
|
ansible_module.fail_json(
|
|
msg="Domain Level cannot be lower than %d" % MIN_DOMAIN_LEVEL)
|
|
elif options.domain_level > MAX_DOMAIN_LEVEL:
|
|
ansible_module.fail_json(
|
|
msg="Domain Level cannot be higher than %d" % MAX_DOMAIN_LEVEL)
|
|
|
|
# dirsrv_config_file
|
|
if options.dirsrv_config_file is not None:
|
|
if not os.path.exists(options.dirsrv_config_file):
|
|
ansible_module.fail_json(
|
|
msg="File %s does not exist." % options.dirsrv_config_file)
|
|
|
|
# domain_name
|
|
# Validation is done later on in ipaserver_prepare dns.install_check
|
|
|
|
# dm_password
|
|
with redirect_stdout(ansible_log):
|
|
validate_dm_password(options.dm_password)
|
|
|
|
# admin_password
|
|
with redirect_stdout(ansible_log):
|
|
validate_admin_password(options.admin_password)
|
|
|
|
# pkinit is not supported on DL0, don't allow related options
|
|
# pylint: disable=pointless-string-statement
|
|
"""
|
|
# replica install: if not options.replica_file is None:
|
|
if (not options._replica_install and \
|
|
not options.domain_level > DOMAIN_LEVEL_0) or \
|
|
(options._replica_install and options.replica_file is not None):
|
|
if (options.no_pkinit or options.pkinit_cert_files is not None or
|
|
options.pkinit_pin is not None):
|
|
ansible_module.fail_json(
|
|
msg="pkinit on domain level 0 is not supported. Please "
|
|
"don't use any pkinit-related options.")
|
|
options.no_pkinit = True
|
|
"""
|
|
# pylint: enable=pointless-string-statement
|
|
|
|
if options.setup_dns:
|
|
if len(options.forwarders) < 1 and not options.no_forwarders and \
|
|
not options.auto_forwarders:
|
|
ansible_module.fail_json(
|
|
msg="You must specify at least one of forwarders, "
|
|
"auto-forwarders or no-forwarders")
|
|
|
|
if NUM_VERSION >= 40200 and options.master_password and \
|
|
not options.external_cert_files:
|
|
ansible_module.warn(
|
|
"Specifying kerberos master-password is deprecated")
|
|
|
|
options._installation_cleanup = True
|
|
if not options.external_ca and not options.external_cert_files and \
|
|
is_ipa_configured():
|
|
options._installation_cleanup = False
|
|
ansible_module.log(
|
|
"IPA server is already configured on this system. If you want "
|
|
"to reinstall the IPA server, please uninstall it first.")
|
|
ansible_module.exit_json(changed=False,
|
|
server_already_configured=True)
|
|
|
|
client_fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
|
|
if client_fstore.has_files():
|
|
options._installation_cleanup = False
|
|
ansible_module.log(
|
|
"IPA client is already configured on this system. "
|
|
"Please uninstall it before configuring the IPA server.")
|
|
ansible_module.exit_json(changed=False,
|
|
client_already_configured=True)
|
|
|
|
# validate reverse_zones
|
|
if not options.allow_zone_overlap:
|
|
for zone in options.reverse_zones:
|
|
with redirect_stdout(ansible_log):
|
|
check_zone_overlap(zone)
|
|
|
|
# validate zonemgr
|
|
if options.zonemgr:
|
|
if six.PY3:
|
|
with redirect_stdout(ansible_log):
|
|
bindinstance.validate_zonemgr_str(options.zonemgr)
|
|
else:
|
|
try:
|
|
# IDNA support requires unicode
|
|
encoding = getattr(sys.stdin, 'encoding', None)
|
|
if encoding is None:
|
|
encoding = 'utf-8'
|
|
value = options.zonemgr
|
|
if not isinstance(value, unicode):
|
|
value = options.zonemgr.decode(encoding)
|
|
else:
|
|
value = options.zonemgr
|
|
with redirect_stdout(ansible_log):
|
|
bindinstance.validate_zonemgr_str(value)
|
|
except ValueError as e:
|
|
# FIXME we can do this in better way
|
|
# https://fedorahosted.org/freeipa/ticket/4804
|
|
# decode to proper stderr encoding
|
|
stderr_encoding = getattr(sys.stderr, 'encoding', None)
|
|
if stderr_encoding is None:
|
|
stderr_encoding = 'utf-8'
|
|
error = unicode(e).encode(stderr_encoding)
|
|
ansible_module.fail_json(msg=error)
|
|
|
|
# external cert file paths are absolute
|
|
if options.external_cert_files:
|
|
for path in options.external_cert_files:
|
|
if not os.path.isabs(path):
|
|
ansible_module.fail_json(
|
|
msg="External cert file '%s' must use an absolute "
|
|
"path" % path)
|
|
|
|
options.setup_ca = True
|
|
# We only set up the CA if the PKCS#12 options are not given.
|
|
if options.dirsrv_cert_files and len(options.dirsrv_cert_files) > 0:
|
|
options.setup_ca = False
|
|
else:
|
|
options.setup_ca = True
|
|
|
|
if not options.setup_ca and options.ca_subject:
|
|
ansible_module.fail_json(
|
|
msg="--ca-subject cannot be used with CA-less installation")
|
|
if not options.setup_ca and options.subject_base:
|
|
ansible_module.fail_json(
|
|
msg="--subject-base cannot be used with CA-less installation")
|
|
if not options.setup_ca and options.setup_kra:
|
|
ansible_module.fail_json(
|
|
msg="--setup-kra cannot be used with CA-less installation")
|
|
|
|
# This will override any settings passed in on the cmdline
|
|
if os.path.isfile(paths.ROOT_IPA_CACHE):
|
|
# dm_password check removed, checked already
|
|
try:
|
|
cache_vars = read_cache(options.dm_password)
|
|
options.__dict__.update(cache_vars)
|
|
if cache_vars.get('external_ca', False):
|
|
options.external_ca = False
|
|
options.interactive = False
|
|
except Exception as e:
|
|
ansible_module.fail_json(
|
|
msg="Cannot process the cache file: %s" % str(e))
|
|
|
|
# ca_subject
|
|
if options.ca_subject:
|
|
ca.subject_validator(ca.VALID_SUBJECT_ATTRS, options.ca_subject)
|
|
|
|
# Memory check
|
|
if not options.skip_mem_check and check_available_memory is not None:
|
|
check_available_memory(ca=options.dirsrv_cert_files and
|
|
len(options.dirsrv_cert_files) > 0)
|
|
|
|
# IPv6 and SELinux check
|
|
tasks.check_ipv6_stack_enabled()
|
|
tasks.check_selinux_status()
|
|
if check_ldap_conf is not None:
|
|
check_ldap_conf()
|
|
|
|
_installation_cleanup = True
|
|
if not options.external_ca and not options.external_cert_files and \
|
|
is_ipa_configured():
|
|
_installation_cleanup = False
|
|
ansible_module.fail_json(
|
|
msg="IPA server is already configured on this system.")
|
|
|
|
if not options.no_ntp:
|
|
try:
|
|
timeconf.check_timedate_services()
|
|
except timeconf.NTPConflictingService as e:
|
|
ansible_module.log(
|
|
"WARNING: conflicting time&date synchronization service "
|
|
"'%s' will be disabled in favor of chronyd" %
|
|
e.conflicting_service)
|
|
except timeconf.NTPConfigurationError:
|
|
pass
|
|
|
|
if hasattr(httpinstance, "httpd_443_configured"):
|
|
# Check to see if httpd is already configured to listen on 443
|
|
if httpinstance.httpd_443_configured():
|
|
ansible_module.fail_json(
|
|
msg="httpd is already configured to listen on 443.")
|
|
|
|
if not options.external_cert_files:
|
|
# Make sure the 389-ds ports are available
|
|
try:
|
|
check_dirsrv(True)
|
|
except ScriptError as e:
|
|
ansible_module.fail_json(msg=e)
|
|
|
|
# check bind packages are installed
|
|
if options.setup_dns:
|
|
# Don't require an external DNS to say who we are if we are
|
|
# setting up a local DNS server.
|
|
options.no_host_dns = True
|
|
|
|
# host name
|
|
if options.host_name:
|
|
host_default = options.host_name
|
|
else:
|
|
host_default = get_fqdn()
|
|
|
|
try:
|
|
verify_fqdn(host_default, options.no_host_dns)
|
|
host_name = host_default
|
|
except BadHostError as e:
|
|
ansible_module.fail_json(msg=e)
|
|
|
|
host_name = host_name.lower()
|
|
|
|
if not options.domain_name:
|
|
domain_name = host_name[host_name.find(".") + 1:]
|
|
try:
|
|
validate_domain_name(domain_name)
|
|
except ValueError as e:
|
|
ansible_module.fail_json(
|
|
msg="Invalid domain name: %s" % unicode(e))
|
|
else:
|
|
domain_name = options.domain_name
|
|
|
|
domain_name = domain_name.lower()
|
|
|
|
if not options.realm_name:
|
|
realm_name = domain_name.upper()
|
|
else:
|
|
realm_name = options.realm_name.upper()
|
|
|
|
# pylint: disable=deprecated-method
|
|
argspec = getargspec(validate_domain_name)
|
|
# pylint: enable=deprecated-method
|
|
if "entity" in argspec.args:
|
|
# NUM_VERSION >= 40690:
|
|
try:
|
|
validate_domain_name(realm_name, entity="realm")
|
|
except ValueError as e:
|
|
raise ScriptError("Invalid realm name: {}".format(unicode(e)))
|
|
|
|
if not options.setup_adtrust:
|
|
# If domain name and realm does not match, IPA server will not be able
|
|
# to establish trust with Active Directory. Fail.
|
|
|
|
if domain_name.upper() != realm_name:
|
|
ansible_module.warn(
|
|
"Realm name does not match the domain name: "
|
|
"You will not be able to establish trusts with Active "
|
|
"Directory.")
|
|
|
|
# Do not ask for time source
|
|
# if not options.no_ntp and not options.unattended and not (
|
|
# options.ntp_servers or options.ntp_pool):
|
|
# options.ntp_servers, options.ntp_pool = timeconf.get_time_source()
|
|
|
|
#########################################################################
|
|
|
|
http_pkcs12_file = None
|
|
http_pkcs12_info = None
|
|
http_ca_cert = None
|
|
dirsrv_pkcs12_file = None
|
|
dirsrv_pkcs12_info = None
|
|
dirsrv_ca_cert = None
|
|
pkinit_pkcs12_file = None
|
|
pkinit_pkcs12_info = None
|
|
pkinit_ca_cert = None
|
|
|
|
if options.http_cert_files:
|
|
if options.http_pin is None:
|
|
ansible_module.fail_json(
|
|
msg="Apache Server private key unlock password required")
|
|
http_pkcs12_file, http_pin, http_ca_cert = load_pkcs12(
|
|
cert_files=options.http_cert_files,
|
|
key_password=options.http_pin,
|
|
key_nickname=options.http_cert_name,
|
|
ca_cert_files=options.ca_cert_files,
|
|
host_name=host_name)
|
|
http_pkcs12_info = (http_pkcs12_file.name, http_pin)
|
|
|
|
if options.dirsrv_cert_files:
|
|
if options.dirsrv_pin is None:
|
|
ansible_module.fail_json(
|
|
msg="Directory Server private key unlock password required")
|
|
dirsrv_pkcs12_file, dirsrv_pin, dirsrv_ca_cert = load_pkcs12(
|
|
cert_files=options.dirsrv_cert_files,
|
|
key_password=options.dirsrv_pin,
|
|
key_nickname=options.dirsrv_cert_name,
|
|
ca_cert_files=options.ca_cert_files,
|
|
host_name=host_name)
|
|
dirsrv_pkcs12_info = (dirsrv_pkcs12_file.name, dirsrv_pin)
|
|
|
|
if options.pkinit_cert_files:
|
|
if options.pkinit_pin is None:
|
|
ansible_module.fail_json(
|
|
msg="Kerberos KDC private key unlock password required")
|
|
pkinit_pkcs12_file, pkinit_pin, pkinit_ca_cert = load_pkcs12(
|
|
cert_files=options.pkinit_cert_files,
|
|
key_password=options.pkinit_pin,
|
|
key_nickname=options.pkinit_cert_name,
|
|
ca_cert_files=options.ca_cert_files,
|
|
realm_name=realm_name)
|
|
pkinit_pkcs12_info = (pkinit_pkcs12_file.name, pkinit_pin)
|
|
|
|
if options.http_cert_files and options.dirsrv_cert_files and \
|
|
http_ca_cert != dirsrv_ca_cert:
|
|
ansible_module.fail_json(
|
|
msg="Apache Server SSL certificate and Directory Server SSL "
|
|
"certificate are not signed by the same CA certificate")
|
|
|
|
if options.http_cert_files and options.pkinit_cert_files and \
|
|
http_ca_cert != pkinit_ca_cert:
|
|
ansible_module.fail_json(
|
|
msg="Apache Server SSL certificate and PKINIT KDC "
|
|
"certificate are not signed by the same CA certificate")
|
|
|
|
# done ##################################################################
|
|
|
|
# Copy pkcs12_files to make them persistent till deployment is done
|
|
# and encode certificates for ansible compatibility
|
|
if http_pkcs12_info is not None:
|
|
copyfile(http_pkcs12_file.name, "/etc/ipa/.tmp_pkcs12_http")
|
|
http_pkcs12_info = ("/etc/ipa/.tmp_pkcs12_http", http_pin)
|
|
http_ca_cert = encode_certificate(http_ca_cert)
|
|
if dirsrv_pkcs12_info is not None:
|
|
copyfile(dirsrv_pkcs12_file.name, "/etc/ipa/.tmp_pkcs12_dirsrv")
|
|
dirsrv_pkcs12_info = ("/etc/ipa/.tmp_pkcs12_dirsrv", dirsrv_pin)
|
|
dirsrv_ca_cert = encode_certificate(dirsrv_ca_cert)
|
|
if pkinit_pkcs12_info is not None:
|
|
copyfile(pkinit_pkcs12_file.name, "/etc/ipa/.tmp_pkcs12_pkinit")
|
|
pkinit_pkcs12_info = ("/etc/ipa/.tmp_pkcs12_pkinit", pkinit_pin)
|
|
pkinit_ca_cert = encode_certificate(pkinit_ca_cert)
|
|
|
|
ansible_module.exit_json(
|
|
changed=False,
|
|
ipa_python_version=IPA_PYTHON_VERSION,
|
|
# basic
|
|
domain=options.domain_name,
|
|
realm=realm_name,
|
|
hostname=host_name,
|
|
_hostname_overridden=bool(options.host_name),
|
|
no_host_dns=options.no_host_dns,
|
|
# server
|
|
setup_adtrust=options.setup_adtrust,
|
|
setup_kra=options.setup_kra,
|
|
setup_ca=options.setup_ca,
|
|
idstart=options.idstart,
|
|
idmax=options.idmax,
|
|
no_pkinit=options.no_pkinit,
|
|
# ssl certificate
|
|
_dirsrv_pkcs12_info=dirsrv_pkcs12_info,
|
|
_dirsrv_ca_cert=dirsrv_ca_cert,
|
|
_http_pkcs12_info=http_pkcs12_info,
|
|
_http_ca_cert=http_ca_cert,
|
|
_pkinit_pkcs12_info=pkinit_pkcs12_info,
|
|
_pkinit_ca_cert=pkinit_ca_cert,
|
|
# certificate system
|
|
external_ca=options.external_ca,
|
|
external_ca_type=options.external_ca_type,
|
|
external_ca_profile=options.external_ca_profile,
|
|
# ad trust
|
|
rid_base=options.rid_base,
|
|
secondary_rid_base=options.secondary_rid_base,
|
|
# client
|
|
ntp_servers=options.ntp_servers,
|
|
ntp_pool=options.ntp_pool,
|
|
# additional
|
|
_installation_cleanup=_installation_cleanup,
|
|
domainlevel=options.domainlevel,
|
|
sid_generation_always=sid_generation_always,
|
|
random_serial_numbers=options._random_serial_numbers,
|
|
)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|