Hi,

the attached patches implement another part of <https://fedorahosted.org/freeipa/ticket/4468>.


Honza

--
Jan Cholasta
>From 9ebc623478065d582a449dde56f39ec927f12628 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Tue, 9 Jun 2015 11:33:13 +0000
Subject: [PATCH 1/4] install: Handle Knob cli_name and cli_aliases values
 consistently

https://fedorahosted.org/freeipa/ticket/4468
---
 ipapython/install/cli.py            | 11 ++++++-----
 ipaserver/install/server/install.py | 36 ++++++++++++++++++------------------
 2 files changed, 24 insertions(+), 23 deletions(-)

diff --git a/ipapython/install/cli.py b/ipapython/install/cli.py
index b83fd9a..e121f3d 100644
--- a/ipapython/install/cli.py
+++ b/ipapython/install/cli.py
@@ -114,8 +114,8 @@ class ConfigureTool(admintool.AdminTool):
                 short_opt_str = '-{0}'.format(knob_cls.cli_short_name)
             else:
                 short_opt_str = ''
-            cli_name = knob_cls.cli_name or name
-            opt_str = '--{0}'.format(cli_name.replace('_', '-'))
+            cli_name = knob_cls.cli_name or name.replace('_', '-')
+            opt_str = '--{0}'.format(cli_name)
             if not knob_cls.deprecated:
                 help = knob_cls.description
             else:
@@ -127,8 +127,9 @@ class ConfigureTool(admintool.AdminTool):
             )
 
             if knob_cls.cli_aliases:
+                opt_strs = ['--{0}'.format(a) for a in knob_cls.cli_aliases]
                 opt_group.add_option(
-                    *knob_cls.cli_aliases,
+                    *opt_strs,
                     help=optparse.SUPPRESS_HELP,
                     **kwargs
                 )
@@ -201,8 +202,8 @@ class ConfigureTool(admintool.AdminTool):
             cfgr = transformed_cls(**kwargs)
         except core.KnobValueError as e:
             knob_cls = getattr(transformed_cls, e.name)
-            cli_name = knob_cls.cli_name or e.name
-            opt_str = '--{0}'.format(cli_name.replace('_', '-'))
+            cli_name = knob_cls.cli_name or e.name.replace('_', '-')
+            opt_str = '--{0}'.format(cli_name)
             self.option_parser.error("option {0}: {1}".format(opt_str, e))
         except RuntimeError as e:
             self.option_parser.error(str(e))
diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py
index 7a5aa3c..59a9d1e 100644
--- a/ipaserver/install/server/install.py
+++ b/ipaserver/install/server/install.py
@@ -1165,8 +1165,8 @@ class ServerCA(common.Installable, core.Group, core.Composite):
         (list, str), None,
         description=("File containing the IPA CA certificate and the external "
                      "CA certificate chain (can be specified multiple times)"),
-        cli_name='external_cert_file',
-        cli_aliases=['--external_cert_file', '--external_ca_file'],
+        cli_name='external-cert-file',
+        cli_aliases=['external_cert_file', 'external_ca_file'],
         cli_metavar='FILE',
     )
 
@@ -1184,8 +1184,8 @@ class ServerCA(common.Installable, core.Group, core.Composite):
         (list, str), None,
         description=("File containing the Directory Server SSL certificate "
                      "and private key"),
-        cli_name='dirsrv_cert_file',
-        cli_aliases=['--dirsrv_pkcs12'],
+        cli_name='dirsrv-cert-file',
+        cli_aliases=['dirsrv_pkcs12'],
         cli_metavar='FILE',
     )
 
@@ -1193,8 +1193,8 @@ class ServerCA(common.Installable, core.Group, core.Composite):
         (list, str), None,
         description=("File containing the Apache Server SSL certificate and "
                      "private key"),
-        cli_name='http_cert_file',
-        cli_aliases=['--http_pkcs12'],
+        cli_name='http-cert-file',
+        cli_aliases=['http_pkcs12'],
         cli_metavar='FILE',
     )
 
@@ -1202,8 +1202,8 @@ class ServerCA(common.Installable, core.Group, core.Composite):
         (list, str), None,
         description=("File containing the Kerberos KDC SSL certificate and "
                      "private key"),
-        cli_name='pkinit_cert_file',
-        cli_aliases=['--pkinit_pkcs12'],
+        cli_name='pkinit-cert-file',
+        cli_aliases=['pkinit_pkcs12'],
         cli_metavar='FILE',
     )
 
@@ -1211,7 +1211,7 @@ class ServerCA(common.Installable, core.Group, core.Composite):
         str, None,
         sensitive=True,
         description="The password to unlock the Directory Server private key",
-        cli_aliases=['--dirsrv_pin'],
+        cli_aliases=['dirsrv_pin'],
         cli_metavar='PIN',
     )
 
@@ -1219,7 +1219,7 @@ class ServerCA(common.Installable, core.Group, core.Composite):
         str, None,
         sensitive=True,
         description="The password to unlock the Apache Server private key",
-        cli_aliases=['--http_pin'],
+        cli_aliases=['http_pin'],
         cli_metavar='PIN',
     )
 
@@ -1227,7 +1227,7 @@ class ServerCA(common.Installable, core.Group, core.Composite):
         str, None,
         sensitive=True,
         description="The password to unlock the Kerberos KDC private key",
-        cli_aliases=['--pkinit_pin'],
+        cli_aliases=['pkinit_pin'],
         cli_metavar='PIN',
     )
 
@@ -1253,8 +1253,8 @@ class ServerCA(common.Installable, core.Group, core.Composite):
         (list, str), None,
         description=("File containing CA certificates for the service "
                      "certificate files"),
-        cli_name='ca_cert_file',
-        cli_aliases=['--root-ca-file'],
+        cli_name='ca-cert-file',
+        cli_aliases=['root-ca-file'],
         cli_metavar='FILE',
     )
 
@@ -1308,7 +1308,7 @@ class ServerDNS(common.Installable, core.Group, core.Composite):
         (list, str), [],
         description=("The reverse DNS zone to use. This option can be used "
                      "multiple times"),
-        cli_name='reverse_zone',
+        cli_name='reverse-zone',
     )
 
     no_reverse = Knob(
@@ -1387,7 +1387,7 @@ class Server(common.Installable, common.Interactive, core.Composite):
         str, None,
         sensitive=True,
         description="Directory Manager password",
-        cli_name='ds_password',
+        cli_name='ds-password',
         cli_short_name='p',
     )
 
@@ -1428,7 +1428,7 @@ class Server(common.Installable, common.Interactive, core.Composite):
     domainlevel = Knob(
         int, constants.MAX_DOMAIN_LEVEL,
         description="IPA domain level",
-        cli_name='domain_level',
+        cli_name='domain-level',
     )
 
     @domainlevel.validator
@@ -1447,7 +1447,7 @@ class Server(common.Installable, common.Interactive, core.Composite):
         (list, 'ip-local'), None,
         description=("Master Server IP Address. This option can be used "
                      "multiple times"),
-        cli_name='ip_address',
+        cli_name='ip-address',
     )
 
     no_ntp = Knob(
@@ -1473,7 +1473,7 @@ class Server(common.Installable, common.Interactive, core.Composite):
     no_hbac_allow = Knob(
         bool, False,
         description="Don't install allow_all HBAC rule",
-        cli_aliases=['--no_hbac_allow'],
+        cli_name='no_hbac_allow',
     )
 
     no_ui_redirect = Knob(
-- 
2.1.0

>From a7fe1a89a5c61bbb1a80eaa690e67be74a20d0fc Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Tue, 9 Jun 2015 11:41:09 +0000
Subject: [PATCH 2/4] install: Add support for positional arguments in CLI
 tools

https://fedorahosted.org/freeipa/ticket/4468
---
 ipapython/install/cli.py | 140 +++++++++++++++++++++++++++++++++++------------
 1 file changed, 106 insertions(+), 34 deletions(-)

diff --git a/ipapython/install/cli.py b/ipapython/install/cli.py
index e121f3d..be7f218 100644
--- a/ipapython/install/cli.py
+++ b/ipapython/install/cli.py
@@ -19,12 +19,16 @@ __all__ = ['install_tool', 'uninstall_tool']
 
 
 def install_tool(configurable_class, command_name, log_file_name,
-                 debug_option=False, uninstall_log_file_name=None):
-    if uninstall_log_file_name is not None:
+                 positional_arguments=None, debug_option=False,
+                 uninstall_log_file_name=None,
+                 uninstall_positional_arguments=None):
+    if (uninstall_log_file_name is not None or
+            uninstall_positional_arguments is not None):
         uninstall_kwargs = dict(
             configurable_class=configurable_class,
             command_name=command_name,
             log_file_name=uninstall_log_file_name,
+            positional_arguments=uninstall_positional_arguments,
             debug_option=debug_option,
         )
     else:
@@ -37,6 +41,7 @@ def install_tool(configurable_class, command_name, log_file_name,
             configurable_class=configurable_class,
             command_name=command_name,
             log_file_name=log_file_name,
+            positional_arguments=positional_arguments,
             debug_option=debug_option,
             uninstall_kwargs=uninstall_kwargs,
         )
@@ -44,7 +49,7 @@ def install_tool(configurable_class, command_name, log_file_name,
 
 
 def uninstall_tool(configurable_class, command_name, log_file_name,
-                   debug_option=False):
+                   positional_arguments=None, debug_option=False):
     return type(
         'uninstall_tool({0})'.format(configurable_class.__name__),
         (UninstallTool,),
@@ -52,6 +57,7 @@ def uninstall_tool(configurable_class, command_name, log_file_name,
             configurable_class=configurable_class,
             command_name=command_name,
             log_file_name=log_file_name,
+            positional_arguments=positional_arguments,
             debug_option=debug_option,
         )
     )
@@ -60,6 +66,7 @@ def uninstall_tool(configurable_class, command_name, log_file_name,
 class ConfigureTool(admintool.AdminTool):
     configurable_class = None
     debug_option = False
+    positional_arguments = None
 
     @staticmethod
     def _transform(configurable_class):
@@ -77,6 +84,8 @@ class ConfigureTool(admintool.AdminTool):
             knob_cls = getattr(owner_cls, name)
             if not knob_cls.initializable:
                 continue
+            if cls.positional_arguments and name in cls.positional_arguments:
+                continue
 
             group_cls = owner_cls.group()
             try:
@@ -88,17 +97,6 @@ class ConfigureTool(admintool.AdminTool):
             kwargs = dict()
             if knob_cls.type is bool:
                 kwargs['type'] = None
-            elif knob_cls.type is int:
-                kwargs['type'] = 'int'
-            elif knob_cls.type is long:
-                kwargs['type'] = 'long'
-            elif knob_cls.type is float:
-                kwargs['type'] = 'float'
-            elif knob_cls.type is complex:
-                kwargs['type'] = 'complex'
-            elif isinstance(knob_cls.type, set):
-                kwargs['type'] = 'choice'
-                kwargs['choices'] = list(knob_cls.type)
             else:
                 kwargs['type'] = 'string'
             kwargs['dest'] = name
@@ -150,41 +148,110 @@ class ConfigureTool(admintool.AdminTool):
                                               debug_option=cls.debug_option)
 
     @classmethod
-    def _option_callback(cls, option, opt_str, value, parser, knob):
-        if knob.type is bool:
-            value_type = bool
+    def _option_callback(cls, option, opt_str, value, parser, knob_cls):
+        old_value = getattr(parser.values, option.dest, None)
+        try:
+            value = cls._parse_knob(knob_cls, old_value, value)
+        except ValueError as e:
+            raise optparse.OptionValueError(
+                "option {0}: {1}".format(opt_str, e))
+
+        setattr(parser.values, option.dest, value)
+
+    @classmethod
+    def _parse_knob(cls, knob_cls, old_value, value):
+        if knob_cls.type is bool:
+            parse = bool
             is_list = False
             value = True
         else:
-            if isinstance(knob.type, tuple):
-                assert knob.type[0] is list
-                value_type = knob.type[1]
+            if isinstance(knob_cls.type, tuple):
+                assert knob_cls.type[0] is list
+                value_type = knob_cls.type[1]
                 is_list = True
             else:
-                value_type = knob.type
+                value_type = knob_cls.type
                 is_list = False
 
-            if value_type == 'ip':
-                value_type = CheckedIPAddress
+            if value_type is int:
+                def parse(value):
+                    try:
+                        return int(value, 0)
+                    except ValueError:
+                        raise ValueError(
+                            "invalid integer value: {0}".format(repr(value)))
+            elif value_type is long:
+                def parse(value):
+                    try:
+                        return long(value, 0)
+                    except ValueError:
+                        raise ValueError(
+                            "invalid long integer value: {0}".format(
+                                repr(value)))
+            elif value_type == 'ip':
+                def parse(value):
+                    try:
+                        return CheckedIPAddress(value)
+                    except Exception as e:
+                        raise ValueError("invalid IP address {0}: {1}".format(
+                            value, e))
             elif value_type == 'ip-local':
-                value_type = lambda v: CheckedIPAddress(v, match_local=True)
+                def parse(value):
+                    try:
+                        return CheckedIPAddress(value, match_local=True)
+                    except Exception as e:
+                        raise ValueError("invalid IP address {0}: {1}".format(
+                            value, e))
+            elif isinstance(value_type, set):
+                def parse(value):
+                    if value not in value_type:
+                        raise ValueError(
+                            "invalid choice {0} (choose from {1})".format(
+                                repr(value), ', '.join(repr(value_type))))
+                    return value
+            else:
+                parse = value_type
 
-        try:
-            value = value_type(value)
-        except ValueError as e:
-            raise optparse.OptionValueError(
-                "option {0}: {1}".format(opt_str, e))
+        value = parse(value)
 
         if is_list:
-            old_value = getattr(parser.values, option.dest) or []
+            old_value = old_value or []
             old_value.append(value)
             value = old_value
 
-        setattr(parser.values, option.dest, value)
+        return value
 
     def validate_options(self, needs_root=True):
         super(ConfigureTool, self).validate_options(needs_root=needs_root)
 
+        if self.positional_arguments:
+            if len(self.args) > len(self.positional_arguments):
+                self.option_parser.error("Too many arguments provided")
+
+            index = 0
+
+            transformed_cls = self._transform(self.configurable_class)
+            for owner_cls, name in transformed_cls.knobs():
+                knob_cls = getattr(owner_cls, name)
+                if name not in self.positional_arguments:
+                    continue
+
+                try:
+                    value = self.args[index]
+                except IndexError:
+                    break
+
+                old_value = getattr(self.options, name, None)
+                try:
+                    value = self._parse_knob(knob_cls, old_value, value)
+                except ValueError as e:
+                    self.option_parser.error(
+                        "argument {0}: {1}".format(index + 1, e))
+
+                setattr(self.options, name, value)
+
+                index += 1
+
     def run(self):
         kwargs = {}
 
@@ -202,9 +269,14 @@ class ConfigureTool(admintool.AdminTool):
             cfgr = transformed_cls(**kwargs)
         except core.KnobValueError as e:
             knob_cls = getattr(transformed_cls, e.name)
-            cli_name = knob_cls.cli_name or e.name.replace('_', '-')
-            opt_str = '--{0}'.format(cli_name)
-            self.option_parser.error("option {0}: {1}".format(opt_str, e))
+            try:
+                index = self.positional_arguments.index(e.name)
+            except IndexError:
+                cli_name = knob_cls.cli_name or e.name.replace('_', '-')
+                desc = "option --{0}".format(cli_name)
+            else:
+                desc = "argument {0}".format(index + 1)
+            self.option_parser.error("{0}: {1}".format(desc, e))
         except RuntimeError as e:
             self.option_parser.error(str(e))
 
-- 
2.1.0

>From 71a8725c1f668428a75c7f7ad0bddf1e0d36d88c Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Tue, 9 Jun 2015 11:42:20 +0000
Subject: [PATCH 3/4] install: Allow setting usage in CLI tools

https://fedorahosted.org/freeipa/ticket/4468
---
 ipapython/install/cli.py | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/ipapython/install/cli.py b/ipapython/install/cli.py
index be7f218..b526ea7 100644
--- a/ipapython/install/cli.py
+++ b/ipapython/install/cli.py
@@ -19,16 +19,18 @@ __all__ = ['install_tool', 'uninstall_tool']
 
 
 def install_tool(configurable_class, command_name, log_file_name,
-                 positional_arguments=None, debug_option=False,
+                 positional_arguments=None, usage=None, debug_option=False,
                  uninstall_log_file_name=None,
-                 uninstall_positional_arguments=None):
+                 uninstall_positional_arguments=None, uninstall_usage=None):
     if (uninstall_log_file_name is not None or
-            uninstall_positional_arguments is not None):
+            uninstall_positional_arguments is not None or
+            uninstall_usage is not None):
         uninstall_kwargs = dict(
             configurable_class=configurable_class,
             command_name=command_name,
             log_file_name=uninstall_log_file_name,
             positional_arguments=uninstall_positional_arguments,
+            usage=uninstall_usage,
             debug_option=debug_option,
         )
     else:
@@ -42,6 +44,7 @@ def install_tool(configurable_class, command_name, log_file_name,
             command_name=command_name,
             log_file_name=log_file_name,
             positional_arguments=positional_arguments,
+            usage=usage,
             debug_option=debug_option,
             uninstall_kwargs=uninstall_kwargs,
         )
@@ -49,7 +52,7 @@ def install_tool(configurable_class, command_name, log_file_name,
 
 
 def uninstall_tool(configurable_class, command_name, log_file_name,
-                   positional_arguments=None, debug_option=False):
+                   positional_arguments=None, usage=None, debug_option=False):
     return type(
         'uninstall_tool({0})'.format(configurable_class.__name__),
         (UninstallTool,),
@@ -58,6 +61,7 @@ def uninstall_tool(configurable_class, command_name, log_file_name,
             command_name=command_name,
             log_file_name=log_file_name,
             positional_arguments=positional_arguments,
+            usage=usage,
             debug_option=debug_option,
         )
     )
-- 
2.1.0

>From 15fa38932bdc68b895f7b63fec526292f9cad864 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Tue, 9 Jun 2015 11:42:50 +0000
Subject: [PATCH 4/4] install: Migrate ipa-replica-install to the install
 framework

https://fedorahosted.org/freeipa/ticket/4468
---
 install/tools/ipa-replica-install          | 151 +-------------
 ipaserver/install/server/__init__.py       |   3 +-
 ipaserver/install/server/replicainstall.py | 322 +++++++++++++++++++++++------
 3 files changed, 275 insertions(+), 201 deletions(-)

diff --git a/install/tools/ipa-replica-install b/install/tools/ipa-replica-install
index 791a272..10a1082 100755
--- a/install/tools/ipa-replica-install
+++ b/install/tools/ipa-replica-install
@@ -18,148 +18,19 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 
-import sys
-import os
-from optparse import OptionGroup
-
-from ipapython import ipautil
-from ipaserver.install import installutils
-from ipaserver.install import server
-from ipapython import version
-from ipapython.config import IPAOptionParser
-from ipapython.ipa_log_manager import root_logger, standard_logging_setup
-from ipapython.dn import DN
+from ipapython.install import cli
 from ipaplatform.paths import paths
+from ipaserver.install.server import Replica
 
-log_file_name = paths.IPAREPLICA_INSTALL_LOG
-DIRMAN_DN = DN(('cn', 'directory manager'))
-REPLICA_INFO_TOP_DIR = None
-
-
-def parse_options():
-    usage = "%prog [options] REPLICA_FILE"
-    parser = IPAOptionParser(usage=usage, version=version.VERSION)
-
-    basic_group = OptionGroup(parser, "basic options")
-    basic_group.add_option("--setup-ca", dest="setup_ca", action="store_true",
-                      default=False, help="configure a dogtag CA")
-    basic_group.add_option("--setup-kra", dest="setup_kra", action="store_true",
-                      default=False, help="configure a dogtag KRA")
-    basic_group.add_option("--ip-address", dest="ip_addresses",
-                      type="ip", ip_local=True, action="append", default=[],
-                      help="Replica server IP Address. This option can be used multiple times", metavar="IP_ADDRESS")
-    basic_group.add_option("-p", "--password", dest="password", sensitive=True,
-                      help="Directory Manager (existing master) password")
-    basic_group.add_option("-w", "--admin-password", dest="admin_password", sensitive=True,
-                      help="Admin user Kerberos password used for connection check")
-    basic_group.add_option("--mkhomedir",
-                           dest="mkhomedir",
-                           action="store_true",
-                           default=False,
-                           help="create home directories for users "
-                                "on their first login")
-    basic_group.add_option("-N", "--no-ntp", dest="conf_ntp", action="store_false",
-                      help="do not configure ntp", default=True)
-    basic_group.add_option("--no-ui-redirect", dest="ui_redirect", action="store_false",
-                      default=True, help="Do not automatically redirect to the Web UI")
-    basic_group.add_option("--ssh-trust-dns", dest="trust_sshfp", default=False, action="store_true",
-                      help="configure OpenSSH client to trust DNS SSHFP records")
-    basic_group.add_option("--no-ssh", dest="conf_ssh", default=True, action="store_false",
-                      help="do not configure OpenSSH client")
-    basic_group.add_option("--no-sshd", dest="conf_sshd", default=True, action="store_false",
-                      help="do not configure OpenSSH server")
-    basic_group.add_option("--skip-conncheck", dest="skip_conncheck", action="store_true",
-                      default=False, help="skip connection check to remote master")
-    basic_group.add_option("-d", "--debug", dest="debug", action="store_true",
-                      default=False, help="gather extra debugging information")
-    basic_group.add_option("-U", "--unattended", dest="unattended", action="store_true",
-                      default=False, help="unattended installation never prompts the user")
-    parser.add_option_group(basic_group)
-
-    cert_group = OptionGroup(parser, "certificate system options")
-    cert_group.add_option("--no-pkinit", dest="setup_pkinit", action="store_false",
-                      default=True, help="disables pkinit setup steps")
-    cert_group.add_option("--skip-schema-check", dest="skip_schema_check", action="store_true",
-                      default=False, help="skip check for updated CA DS schema on the remote master")
-    parser.add_option_group(cert_group)
-
-    dns_group = OptionGroup(parser, "DNS options")
-    dns_group.add_option("--setup-dns", dest="setup_dns", action="store_true",
-                      default=False, help="configure bind with our zone")
-    dns_group.add_option("--forwarder", dest="forwarders", action="append",
-                      type="ip", help="Add a DNS forwarder. This option can be used multiple times")
-    dns_group.add_option("--no-forwarders", dest="no_forwarders", action="store_true",
-                      default=False, help="Do not add any DNS forwarders, use root servers instead")
-    dns_group.add_option("--reverse-zone", dest="reverse_zones", default=[],
-                         action="append", help="The reverse DNS zone to use. This option can be used multiple times",
-                        metavar="REVERSE_ZONE")
-    dns_group.add_option("--no-reverse", dest="no_reverse", action="store_true",
-                      default=False, help="Do not create new reverse DNS zone")
-    dns_group.add_option("--no-dnssec-validation", dest="no_dnssec_validation", action="store_true",
-                      default=False, help="Disable DNSSEC validation")
-    dns_group.add_option("--no-host-dns", dest="no_host_dns", action="store_true",
-                      default=False,
-                      help="Do not use DNS for hostname lookup during installation")
-    dns_group.add_option("--no-dns-sshfp", dest="create_sshfp", default=True, action="store_false",
-                      help="do not automatically create DNS SSHFP records")
-    parser.add_option_group(dns_group)
-
-    options, args = parser.parse_args()
-    safe_options = parser.get_safe_opts(options)
-
-    if len(args) != 1:
-        parser.error("you must provide a file generated by ipa-replica-prepare")
-
-    if not options.setup_dns:
-        if options.forwarders:
-            parser.error("You cannot specify a --forwarder option without the --setup-dns option")
-        if options.no_forwarders:
-            parser.error("You cannot specify a --no-forwarders option without the --setup-dns option")
-        if options.reverse_zones:
-            parser.error("You cannot specify a --reverse-zone option without the --setup-dns option")
-        if options.no_reverse:
-            parser.error("You cannot specify a --no-reverse option without the --setup-dns option")
-        if options.no_dnssec_validation:
-            parser.error("You cannot specify a --no-dnssec-validation option without the --setup-dns option")
-    elif options.forwarders and options.no_forwarders:
-        parser.error("You cannot specify a --forwarder option together with --no-forwarders")
-    elif not options.forwarders and not options.no_forwarders:
-        parser.error("You must specify at least one --forwarder option or --no-forwarders option")
-    elif options.reverse_zones and options.no_reverse:
-        parser.error("You cannot specify a --reverse-zone option together with --no-reverse")
-
-    options.external_ca = None
-    options.external_cert_files = None
-
-    options.zonemgr = None
-    options.dnssec_master = False
-
-    return safe_options, options, args[0]
-
-
-def main():
-    safe_options, options, filename = parse_options()
-
-    if os.geteuid() != 0:
-        sys.exit("\nYou must be root to run this script.\n")
-
-    standard_logging_setup(log_file_name, debug=options.debug)
-    root_logger.debug('%s was invoked with argument "%s" and options: %s' % (sys.argv[0], filename, safe_options))
-    root_logger.debug('IPA version %s' % version.VENDOR_VERSION)
-
-    if not ipautil.file_exists(filename):
-        sys.exit("Replica file %s does not exist" % filename)
-
-    server.replica_install_check(filename, options)
-    server.replica_install(filename, options)
 
+ReplicaInstall = cli.install_tool(
+    Replica,
+    command_name='ipa-replica-install',
+    positional_arguments='replica_file',
+    usage='%prog [options] REPLICA_FILE',
+    log_file_name=paths.IPAREPLICA_INSTALL_LOG,
+    debug_option=True,
+)
 
-fail_message = '''
-Your system may be partly configured.
-Run /usr/sbin/ipa-server-install --uninstall to clean up.
-'''
 
-if __name__ == '__main__':
-    installutils.run_script(main, log_file_name=log_file_name,
-                            operation_name='ipa-replica-install',
-                            fail_message=fail_message)
+ReplicaInstall.run_cli()
diff --git a/ipaserver/install/server/__init__.py b/ipaserver/install/server/__init__.py
index a6db965..da2ceec 100644
--- a/ipaserver/install/server/__init__.py
+++ b/ipaserver/install/server/__init__.py
@@ -3,7 +3,6 @@
 #
 
 from .install import Server
+from .replicainstall import Replica
 
-from .replicainstall import install_check as replica_install_check
-from .replicainstall import install as replica_install
 from .upgrade import upgrade_check, upgrade
diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py
index 149d6b4..9455117 100644
--- a/ipaserver/install/server/replicainstall.py
+++ b/ipaserver/install/server/replicainstall.py
@@ -14,6 +14,9 @@ import tempfile
 
 from ipapython import dogtag, ipautil, sysrestore
 from ipapython.dn import DN
+from ipapython.install import common, core
+from ipapython.install.common import step
+from ipapython.install.core import Knob
 from ipapython.ipa_log_manager import root_logger
 from ipaplatform import services
 from ipaplatform.tasks import tasks
@@ -28,7 +31,6 @@ from ipaserver.install.replication import (
     ReplicationManager, replica_conn_check)
 
 DIRMAN_DN = DN(('cn', 'directory manager'))
-REPLICA_INFO_TOP_DIR = None
 
 
 def get_dirman_password():
@@ -162,7 +164,7 @@ def install_dns_records(config, options, remote_api):
                                         config.realm_name,
                                         config.domain_name,
                                         reverse_zone,
-                                        options.conf_ntp,
+                                        not options.no_ntp,
                                         options.setup_ca)
     except errors.NotFound, e:
         root_logger.debug('Replica DNS records could not be added '
@@ -276,60 +278,38 @@ def check_dns_resolution(host_name, dns_servers):
     return no_errors
 
 
-def remove_replica_info_dir():
+def remove_replica_info_dir(installer):
     # always try to remove decrypted replica file
     try:
-        if REPLICA_INFO_TOP_DIR:
-            shutil.rmtree(REPLICA_INFO_TOP_DIR)
+        if installer._top_dir is not None:
+            shutil.rmtree(installer._top_dir)
     except OSError:
         pass
 
 
-def init_private_ccache():
-    global original_ccache
-    global temp_ccache
-
-    (desc, temp_ccache) = tempfile.mkstemp(prefix='krbcc')
-    os.close(desc)
-
-    original_ccache = os.environ.get('KRB5CCNAME')
-
-    os.environ['KRB5CCNAME'] = temp_ccache
-
-
-def destroy_private_ccache():
-    global original_ccache
-    global temp_ccache
-
-    if original_ccache is not None:
-        os.environ['KRB5CCNAME'] = original_ccache
-    else:
-        os.environ.pop('KRB5CCNAME', None)
-
-    if os.path.exists(temp_ccache):
-        os.remove(temp_ccache)
-
-
 def common_cleanup(func):
-    def decorated(*args, **kwargs):
+    def decorated(installer):
         try:
             try:
-                func(*args, **kwargs)
+                func(installer)
             except BaseException:
-                destroy_private_ccache()
-                remove_replica_info_dir()
+                remove_replica_info_dir(installer)
                 raise
         except KeyboardInterrupt:
             sys.exit(1)
+        except Exception:
+            print(
+                "Your system may be partly configured.\n"
+                "Run /usr/sbin/ipa-server-install --uninstall to clean up.\n")
+            raise
 
     return decorated
 
 
 @common_cleanup
-def install_check(filename, options):
-    global config
-
-    init_private_ccache()
+def install_check(installer):
+    options = installer
+    filename = installer.replica_file
 
     tasks.check_selinux_status()
 
@@ -339,10 +319,8 @@ def install_check(filename, options):
                  "Please uninstall it first before configuring the replica, "
                  "using 'ipa-client-install --uninstall'.")
 
-    global sstore
     sstore = sysrestore.StateFile(paths.SYSRESTORE)
 
-    global fstore
     fstore = sysrestore.FileStore(paths.SYSRESTORE)
 
     # Check to see if httpd is already configured to listen on 443
@@ -351,7 +329,7 @@ def install_check(filename, options):
 
     check_dirsrv()
 
-    if options.conf_ntp:
+    if not options.no_ntp:
         try:
             ipaclient.ntpconf.check_timedate_services()
         except ipaclient.ntpconf.NTPConflictingService, e:
@@ -373,8 +351,7 @@ def install_check(filename, options):
             sys.exit("Directory Manager password required")
 
     config = create_replica_config(dirman_password, filename, options)
-    global REPLICA_INFO_TOP_DIR
-    REPLICA_INFO_TOP_DIR = config.top_dir
+    installer._top_dir = config.top_dir
     config.setup_ca = options.setup_ca
     config.setup_kra = options.setup_kra
 
@@ -398,7 +375,7 @@ def install_check(filename, options):
         dns.install_check(False, True, options, config.host_name)
     else:
         installutils.get_server_ip_address(config.host_name, fstore,
-                                           options.unattended, False,
+                                           not installer.interactive, False,
                                            options.ip_addresses)
 
     # check connection
@@ -407,18 +384,22 @@ def install_check(filename, options):
             config.master_host_name, config.host_name, config.realm_name,
             options.setup_ca, config.ca_ds_port, options.admin_password)
 
-    # Automatically disable pkinit w/ dogtag until that is supported
-    options.setup_pkinit = False
-
     cafile = config.dir + "/ca.crt"
     if not ipautil.file_exists(cafile):
         raise RuntimeError("CA cert file is not available. Please run "
                            "ipa-replica-prepare to create a new replica file.")
 
+    installer._fstore = fstore
+    installer._sstore = sstore
+    installer._config = config
+
 
 @common_cleanup
-def install(filename, options):
-    global config
+def install(installer):
+    options = installer
+    fstore = installer._fstore
+    sstore = installer._sstore
+    config = installer._config
 
     dogtag_constants = dogtag.install_constants
 
@@ -544,7 +525,7 @@ def install(filename, options):
                     resolution_ok = (
                         check_dns_resolution(master, dns_masters) and
                         check_dns_resolution(config.host_name, dns_masters))
-                    if not resolution_ok and not options.unattended:
+                    if not resolution_ok and installer.interactive:
                         if not ipautil.user_input("Continue?", False):
                             sys.exit(0)
             else:
@@ -562,7 +543,7 @@ def install(filename, options):
                 replman.conn.unbind()
 
         # Configure ntpd
-        if options.conf_ntp:
+        if not options.no_ntp:
             ipaclient.ntpconf.force_ntpd(sstore)
             ntp = ntpinstance.NTPInstance()
             ntp.create_instance()
@@ -584,8 +565,8 @@ def install(filename, options):
 
         ca.install(False, config, options)
 
-    krb = install_krb(config, setup_pkinit=options.setup_pkinit)
-    http = install_http(config, auto_redirect=options.ui_redirect)
+    krb = install_krb(config, setup_pkinit=not options.no_pkinit)
+    http = install_http(config, auto_redirect=not options.no_ui_redirect)
 
     otpd = otpdinstance.OtpdInstance()
     otpd.create_instance('OTPD', config.host_name, config.dirman_password,
@@ -625,13 +606,13 @@ def install(filename, options):
         args = [paths.IPA_CLIENT_INSTALL, "--on-master", "--unattended",
                 "--domain", config.domain_name, "--server", config.host_name,
                 "--realm", config.realm_name]
-        if not options.create_sshfp:
+        if options.no_dns_sshfp:
             args.append("--no-dns-sshfp")
-        if options.trust_sshfp:
+        if options.ssh_trust_dns:
             args.append("--ssh-trust-dns")
-        if not options.conf_ssh:
+        if options.no_ssh:
             args.append("--no-ssh")
-        if not options.conf_sshd:
+        if options.no_sshd:
             args.append("--no-sshd")
         if options.mkhomedir:
             args.append("--mkhomedir")
@@ -646,5 +627,228 @@ def install(filename, options):
     # Everything installed properly, activate ipa service.
     services.knownservices.ipa.enable()
 
-    destroy_private_ccache()
-    remove_replica_info_dir()
+    remove_replica_info_dir(installer)
+
+
+class ReplicaCA(common.Installable, core.Group, core.Composite):
+    description = "certificate system"
+
+    no_pkinit = Knob(
+        bool, False,
+        description="disables pkinit setup steps",
+    )
+
+    skip_schema_check = Knob(
+        bool, False,
+        description="skip check for updated CA DS schema on the remote master",
+    )
+
+
+class ReplicaDNS(common.Installable, core.Group, core.Composite):
+    description = "DNS"
+
+    setup_dns = Knob(
+        bool, False,
+        description="configure bind with our zone",
+    )
+
+    forwarders = Knob(
+        (list, 'ip'), None,
+        description=("Add a DNS forwarder. This option can be used multiple "
+                     "times"),
+        cli_name='forwarder',
+    )
+
+    no_forwarders = Knob(
+        bool, False,
+        description="Do not add any DNS forwarders, use root servers instead",
+    )
+
+    reverse_zones = Knob(
+        (list, str), [],
+        description=("The reverse DNS zone to use. This option can be used "
+                     "multiple times"),
+        cli_name='reverse-zone',
+    )
+
+    no_reverse = Knob(
+        bool, False,
+        description="Do not create new reverse DNS zone",
+    )
+
+    no_dnssec_validation = Knob(
+        bool, False,
+        description="Disable DNSSEC validation",
+    )
+
+    no_host_dns = Knob(
+        bool, False,
+        description="Do not use DNS for hostname lookup during installation",
+    )
+
+    no_dns_sshfp = Knob(
+        bool, False,
+        description="do not automatically create DNS SSHFP records",
+    )
+
+
+class Replica(common.Installable, common.Interactive, core.Composite):
+    replica_file = Knob(
+        str, None,
+        description="a file generated by ipa-replica-prepare",
+    )
+
+    setup_ca = Knob(
+        bool, False,
+        initializable=False,
+        description="configure a dogtag CA",
+    )
+
+    setup_kra = Knob(
+        bool, False,
+        initializable=False,
+        description="configure a dogtag KRA",
+    )
+
+    ip_addresses = Knob(
+        (list, 'ip-local'), None,
+        description=("Replica server IP Address. This option can be used "
+                     "multiple times"),
+        cli_name='ip-address',
+    )
+
+    password = Knob(
+        str, None,
+        sensitive=True,
+        description="Directory Manager (existing master) password",
+        cli_short_name='p',
+    )
+
+    master_password = Knob(
+        str, None,
+        sensitive=True,
+        deprecated=True,
+        description="kerberos master password (normally autogenerated)",
+        cli_short_name='P',
+    )
+
+    admin_password = Knob(
+        str, None,
+        sensitive=True,
+        description="Admin user Kerberos password used for connection check",
+        cli_short_name='w',
+    )
+
+    mkhomedir = Knob(
+        bool, False,
+        description="create home directories for users on their first login",
+    )
+
+    no_ntp = Knob(
+        bool, False,
+        description="do not configure ntp",
+    )
+
+    no_ui_redirect = Knob(
+        bool, False,
+        description="Do not automatically redirect to the Web UI",
+    )
+
+    ssh_trust_dns = Knob(
+        bool, False,
+        description="configure OpenSSH client to trust DNS SSHFP records",
+    )
+
+    no_ssh = Knob(
+        bool, False,
+        description="do not configure OpenSSH client",
+    )
+
+    no_sshd = Knob(
+        bool, False,
+        description="do not configure OpenSSH server",
+    )
+
+    skip_conncheck = Knob(
+        bool, False,
+        description="skip connection check to remote master",
+    )
+
+    def __init__(self, **kwargs):
+        super(Replica, self).__init__(**kwargs)
+
+        self._top_dir = None
+        self._config = None
+
+        #pylint: disable=no-member
+
+        if self.replica_file is None:
+            raise RuntimeError(
+                "you must provide a file generated by ipa-replica-prepare")
+        if not ipautil.file_exists(self.replica_file):
+            raise RuntimeError(
+                "Replica file %s does not exist" % self.replica_file)
+
+        if not self.dns.setup_dns:
+            if self.dns.forwarders:
+                raise RuntimeError(
+                    "You cannot specify a --forwarder option without the "
+                    "--setup-dns option")
+            if self.dns.no_forwarders:
+                raise RuntimeError(
+                    "You cannot specify a --no-forwarders option without the "
+                    "--setup-dns option")
+            if self.dns.reverse_zones:
+                raise RuntimeError(
+                    "You cannot specify a --reverse-zone option without the "
+                    "--setup-dns option")
+            if self.dns.no_reverse:
+                raise RuntimeError(
+                    "You cannot specify a --no-reverse option without the "
+                    "--setup-dns option")
+            if self.dns.no_dnssec_validation:
+                raise RuntimeError(
+                    "You cannot specify a --no-dnssec-validation option "
+                    "without the --setup-dns option")
+        elif self.dns.forwarders and self.dns.no_forwarders:
+            raise RuntimeError(
+                "You cannot specify a --forwarder option together with "
+                "--no-forwarders")
+        elif not self.dns.forwarders and not self.dns.no_forwarders:
+            raise RuntimeError(
+                "You must specify at least one --forwarder option or "
+                "--no-forwarders option")
+        elif self.dns.reverse_zones and self.dns.no_reverse:
+            raise RuntimeError(
+                "You cannot specify a --reverse-zone option together with "
+                "--no-reverse")
+
+        # Automatically disable pkinit w/ dogtag until that is supported
+        self.ca.no_pkinit = True
+
+        self.external_ca = False
+        self.external_cert_files = None
+        self.no_pkinit = self.ca.no_pkinit
+        self.skip_schema_check = self.ca.skip_schema_check
+
+        self.setup_dns = self.dns.setup_dns
+        self.forwarders = self.dns.forwarders
+        self.no_forwarders = self.dns.no_forwarders
+        self.reverse_zones = self.dns.reverse_zones
+        self.no_reverse = self.dns.no_reverse
+        self.no_dnssec_validation = self.dns.no_dnssec_validation
+        self.dnssec_master = False
+        self.zonemgr = None
+        self.no_host_dns = self.dns.no_host_dns
+        self.no_dns_sshfp = self.dns.no_dns_sshfp
+
+        self.unattended = not self.interactive
+
+    @step()
+    def main(self):
+        install_check(self)
+        yield
+        install(self)
+
+    ca = core.Component(ReplicaCA)
+    dns = core.Component(ReplicaDNS)
-- 
2.1.0

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to