Hi,

I did as Jan suggested, everything is now a new command 'ipa-sshupdate', 
(so it's based on Jan's 'ipa-certupdate', yeah, a bit of copy-paste),
rest is based on ipa-client-install's code. I'm not sure if this is
correct, but you might want to change ipa-client-install to just 'import
ipaclient.ipa_sshupdate' for ssh update, or not - I'm not sure how this
is compatible with 'code deduplication', 're-usage', etc.

Another open point from my side is PEP8 compliance, I've ran the new
code through pep8 utility with defaults and it's 'OK'. But so is code in
my employer's project and they look slightly 'different', mainly for
brackets, strings, etc. Please, have a look to that, too, I'm happy for
any guidance.

Martin

On Št, 2016-02-25 at 14:36 +0100, Jan Cholasta wrote:
> Hi,
> 
> On 25.2.2016 14:23, Martin Basti wrote:
> > 
> > 
> > 
> > On 22.02.2016 22:13, Martin Štefany wrote:
> > > 
> > > Hi,
> > > 
> > > please, review the attached patch which adds --ssh-update to ipa-
> > > client-
> > > install.
> > > 
> > > Ticket:https://fedorahosted.org/freeipa/ticket/2655
> > Hello,
> > thank you for your patch.
> > Please attach a patch as a file next time.
> > 
> > I have doubts that this should be part of ipa-client-install, this
> > needs
> > a broader discussion.
> +1, I think it should be a separate command (ignore my earlier 
> suggestion from Trac to incorporate this into ipa-client-install, I
> was 
> young and stupid).
> 
> See client/ipa-certupdate and ipaclient/ipa_certupdate.py for an
> example 
> of how such a command should be implemented.
> 
> > 
> > 
> > Code comments inline:
> > > 
> > > 
> > > ---
> > > Martin
> > > 
> > > > 
> > > > From 4974a57f48a0cd48b83724297ae2af572bc530eb Mon Sep 17
> > > > 00:00:00 2001
> > > From: Martin Stefany <martin stefany eu>
> > > Date: Mon, 22 Feb 2016 20:58:13 +0000
> > > Subject: [PATCH] Add new parameter --ssh-update to ipa-client-
> > > install
> > > 
> > > Add a new parameter '--ssh-update' which can be used later after
> > > freeipa
> > > client is installed to update SSH hostkeys and SSHFP DNS records
> > > for
> > > host.
> > > 
> > > https://fedorahosted.org/freeipa/ticket/2655
> > > ---
> > >   ipa-client/ipa-install/ipa-client-install | 102
> > > +++++++++++++++++++++++++++++-
> > >   1 file changed, 99 insertions(+), 3 deletions(-)
> > > 
> > > diff --git a/ipa-client/ipa-install/ipa-client-install b/ipa-
> > > client/ipa-
> > > install/ipa-client-install
> > > index
> > > 789ff591591673744ee3b922e5c0181233ad553c..97adfb6b449fb441bddada89
> > > a3b151
> > > 33e398ca50 100755
> > > --- a/ipa-client/ipa-install/ipa-client-install
> > > +++ b/ipa-client/ipa-install/ipa-client-install
> > > @@ -71,6 +71,7 @@ CLIENT_INSTALL_ERROR = 1
> > >   CLIENT_NOT_CONFIGURED = 2
> > >   CLIENT_ALREADY_CONFIGURED = 3
> > >   CLIENT_UNINSTALL_ERROR = 4 # error after restoring files/state
> > > +CLIENT_SSHUPDATE_ERROR = 5 # error during update of SSH public
> > > keys
> > > 
> > >   def parse_options():
> > >       def validate_ca_cert_file_option(option, opt, value,
> > > parser):
> > > @@ -215,6 +216,12 @@ def parse_options():
> > >                                             "be run with --
> > > unattended
> > > option")
> > >       parser.add_option_group(uninstall_group)
> > > 
> > > +    sshupdate_group = OptionGroup(parser, "SSH key update
> > > options")
> > > +    sshupdate_group.add_option("--ssh-update", dest="ssh_update",
> > > +                      action="store_true", default=False,
> > > +                      help="update local host's SSH public keys
> > > in host
> > > entry and DNS.")
> > > +    parser.add_option_group(sshupdate_group)
> > > +
> > >       options, args = parser.parse_args()
> > >       safe_opts = parser.get_safe_opts(options)
> > > 
> > > @@ -840,6 +847,92 @@ def uninstall(options, env):
> > > 
> > >       return rv
> > > 
> > > +def sshupdate(options, env):
> > > +    if not is_ipa_client_installed():
> > > +        root_logger.error("IPA client is not configured on this
> > > system.")
> > > +        return CLIENT_NOT_CONFIGURED
> > > +
> > > +    api.bootstrap(context='cli_installer', debug=options.debug)
> > > +    api.finalize()
> > > +    if 'config_loaded' not in api.env:
> > > +        root_logger.error("Failed to initialize IPA API.")
> > > +        return CLIENT_SSHUPDATE_ERROR
> > > +
> > > +    # Now, let's try to connect to the server's RPC interface
> > > +    connected = False
> > > +    try:
> > > +        api.Backend.rpcclient.connect()
> > > +        connected = True
> > > +        root_logger.debug("Try RPC connection")
> > > +        api.Backend.rpcclient.forward('ping')
> > > +    except errors.KerberosError as e:
> > > +        if connected:
> > > +            api.Backend.rpcclient.disconnect()
> > > +        root_logger.info(
> > > +            "Cannot connect to the server due to Kerberos error:
> > > %s. "
> > > +            "Trying with delegate=True", e)
> > > +        try:
> > > +            api.Backend.rpcclient.connect(delegate=True)
> > > +            root_logger.debug("Try RPC connection")
> > > +            api.Backend.rpcclient.forward('ping')
> > > +
> > > +            root_logger.info("Connection with delegate=True
> > > successful")
> > > +
> > > +            # The remote server is not capable of Kerberos
> > > S4U2Proxy
> > > +            # delegation. This features is implemented in IPA
> > > server
> > > +            # version 2.2 and higher
> > > +            root_logger.warning(
> > > +                "Target IPA server has a lower version than the
> > > enrolled "
> > > +                "client")
> > > +            root_logger.warning(
> > > +                "Some capabilities including the ipa command
> > > capability
> > > "
> > > +                "may not be available")
> > > +        except errors.PublicError as e2:
> > > +            root_logger.warning(
> > > +                "Second connect with delegate=True also failed:
> > > %s",
> > > e2)
> > > +            root_logger.error(
> > > +                "Cannot connect to the IPA server RPC interface:
> > > %s",
> > > e2)
> > > +            return CLIENT_SSHUPDATE_ERROR
> > > +    except errors.PublicError as e:
> > > +        root_logger.error(
> > > +            "Cannot connect to the server due to generic error:
> > > %s", e)
> > > +        return CLIENT_SSHUPDATE_ERROR
> > I think you should be kinited with client keytab, client is allowed
> > to
> > modify its SSHpublic keys in ldap. I'd only require to be root to
> > execute it.
> > 
> > kinit -kt /etc/krb5.keytab host/`hostname`
> > ipa host-mod `hostname` --sshpubkey="something"
> > 
> > Also this rpcconnection looks to me too much complicated, I think it
> > should be just simple rpcconnect
> > 
> > > 
> > > +
> > > +    # We need to pull IPA server address from default.conf
> > > +    try:
> > > +        parser = RawConfigParser()
> > > +        parser.read(paths.IPA_DEFAULT_CONF)
> > > +        cli_realm = parser.get('global', 'realm')
> > > +        hostname = parser.get('global', 'host')
> > > +    # TODO: consult with review team
> > > +    # except ConfigParser.NoSectionError as e:
> > > +    #     pass
> > > +    # except ConfigParser.ParsingError as e:
> > > +    #     pass
> > > +    finally:
> > > +        pass
> > You can raise error there.
> > 
> > > 
> > > +
> > > +    host_principal = 'host/%s@%s' % (hostname, cli_realm)
> > > +    # Obtain the TGT. We do it with the temporary krb5.conf, so
> > > that
> > > +    # only the KDC we're installing under is contacted.
> > > +    # Other KDCs might not have replicated the principal yet.
> > > +    # Once we have the TGT, it's usable on any server.
> > I don't think that temporary krb5.conf should be used here
> > > 
> > > +    try:
> > > +        ipautil.kinit_keytab(host_principal, paths.KRB5_KEYTAB,
> > > +                             CCACHE_FILE,
> > > +                             # config=krb_name,
> > > +                             attempts=options.kinit_attempts)
> > > +        env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] =
> > > CCACHE_FILE
> > > +    except Krb5Error as e:
> > > +        print_port_conf_info()
> > > +        root_logger.error("Failed to obtain host TGT: %s" % e)
> > > +        # failure to get ticket makes it impossible to login and
> > > bind
> > > +        # from sssd to LDAP, abort installation and rollback
> > > changes
> > > +        return CLIENT_INSTALL_ERROR
> > This is not install error.
> > 
> > > 
> > > +
> > > +    # passing server parameter seems unneccessary, thus passing
> > > only ""
> > > +    update_ssh_keys("", hostname,
> > > services.knownservices.sshd.get_config_dir(),
> > > options.create_sshfp)
> > > +
> > >   def configure_ipa_conf(fstore, cli_basedn, cli_realm,
> > > cli_domain,
> > > cli_server, hostname):
> > >       ipaconf = ipaclient.ipachangeconf.IPAChangeConf("IPA
> > > Installer")
> > >       ipaconf.setOptionAssignment(" =") @@ -2797,7 +2890,7 @@ def
> > > install(options, env, fstore,
> > > statestore):              connected = True
> > >              root_logger.debug("Try RPC connection")
> > >               api.Backend.rpcclient.forward('ping')
> > > -        except errors.KerberosError, e:
> > > +        except errors.KerberosError as e:
> > Please don't modify code that already exists and it is not related
> > to
> > this change
> > > 
> > >               if connected:
> > >                   api.Backend.rpcclient.disconnect()
> > >               root_logger.info(
> > > @@ -2820,13 +2913,13 @@ def install(options, env, fstore,
> > > statestore):
> > >                   root_logger.warning(
> > >                       "Some capabilities including the ipa command
> > > capability "
> > >                       "may not be available")
> > > -            except errors.PublicError, e2:
> > > +            except errors.PublicError as e2:
> > Remove this from patch too
> > > 
> > >                   root_logger.warning(
> > >                       "Second connect with delegate=True also
> > > failed:
> > > %s", e2)
> > >                   root_logger.error(
> > >                       "Cannot connect to the IPA server RPC
> > > interface:
> > > %s", e2)
> > >                   return CLIENT_INSTALL_ERROR
> > > -        except errors.PublicError, e:
> > > +        except errors.PublicError as e:
> > and this too
> > > 
> > >               root_logger.error(
> > >                   "Cannot connect to the server due to generic
> > > error:
> > > %s", e)
> > >               return CLIENT_INSTALL_ERROR
> > > @@ -3088,6 +3181,9 @@ def main():
> > >       if options.uninstall:
> > >           return uninstall(options, env)
> > > 
> > > +    if options.ssh_update:
> > > +        return sshupdate(options, env)
> > > +
> > >       if is_ipa_client_installed(on_master=options.on_master):
> > >           root_logger.error("IPA client is already configured on
> > > this
> > > system.")
> > >           root_logger.info(
> > > --
> > > 1.8.3.1
> > > 
> > > 
> > Martin^2
> > 
> > 
> Honza
> 
From fc24ea760d065b997613b455cb7dbb4ef45bffde Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20=C5=A0tefany?= <mar...@stefany.eu>
Date: Sat, 27 Feb 2016 20:38:20 +0100
Subject: [PATCH] Add client SSH public key update tool ipa-sshupdate

https://fedorahosted.org/freeipa/ticket/2655
---
 client/Makefile.am         |   1 +
 client/ipa-sshupdate       |  18 ++++++
 client/man/Makefile.am     |   1 +
 client/man/ipa-sshupdate.1 |  39 ++++++++++++
 freeipa.spec.in            |   2 +
 install/po/Makefile.in     |   1 +
 ipaclient/ipa_sshupdate.py | 154 +++++++++++++++++++++++++++++++++++++++++++++
 7 files changed, 216 insertions(+)
 create mode 100755 client/ipa-sshupdate
 create mode 100644 client/man/ipa-sshupdate.1
 create mode 100644 ipaclient/ipa_sshupdate.py

diff --git a/client/Makefile.am b/client/Makefile.am
index 3d135a34455983371a7050d5e078d371bf2c5d19..87b21aed4416ced2ad57d7d00f63c9a825dc765c 100644
--- a/client/Makefile.am
+++ b/client/Makefile.am
@@ -47,6 +47,7 @@ sbin_SCRIPTS =			\
 	ipa-client-install	\
 	ipa-client-automount	\
 	ipa-certupdate		\
+	ipa-sshupdate		\
 	$(NULL)
 
 ipa_getkeytab_SOURCES =		\
diff --git a/client/ipa-sshupdate b/client/ipa-sshupdate
new file mode 100755
index 0000000000000000000000000000000000000000..f1b9ab4afa1b3e40f552291ffb7100032646145f
--- /dev/null
+++ b/client/ipa-sshupdate
@@ -0,0 +1,18 @@
+#! /usr/bin/python2 -E
+# 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 ipaclient.ipa_sshupdate import SshUpdate
+
+SshUpdate.run_cli()
diff --git a/client/man/Makefile.am b/client/man/Makefile.am
index 9d8a9c03d52d6e9364e441b40dd300ce64b750f3..87da9070e8f633b56df2d52323900fe214624b89 100644
--- a/client/man/Makefile.am
+++ b/client/man/Makefile.am
@@ -10,6 +10,7 @@ man1_MANS = 				\
 		ipa-client-install.1	\
 		ipa-client-automount.1	\
 		ipa-certupdate.1	\
+		ipa-sshupdate.1		\
 		ipa-join.1
 
 man5_MANS =				\
diff --git a/client/man/ipa-sshupdate.1 b/client/man/ipa-sshupdate.1
new file mode 100644
index 0000000000000000000000000000000000000000..56a4555163a63ae73b4a87ce648c3c773559c4d8
--- /dev/null
+++ b/client/man/ipa-sshupdate.1
@@ -0,0 +1,39 @@
+.\" A man page for ipa-certupdate
+.\"
+.\" 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/>.
+.\"
+.TH "ipa-sshupdate" "1" "Feb 27 2016" "FreeIPA" "FreeIPA Manual Pages"
+.SH "NAME"
+ipa\-sshupdate \- Update host identity of local client on IPA server with current SSH public keys. By default update also SSHFP DNS records.
+.SH "SYNOPSIS"
+\fBipa\-sshupdate\fR [\fIOPTIONS\fR...]
+.SH "DESCRIPTION"
+\fBipa\-sshupdate\fR can be used to update host identity of local client on IPA server with current SSH public keys. By default update also SSHFP DNS records.
+.SH "OPTIONS"
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Print debugging information.
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+Output only errors.
+.TP
+\fB\-\-log\-file\fR=\fIFILE\fR
+Log to the given file.
+.TP
+\fB\-\-no\-dns\-sshfp\fR
+Do not update DNS SSHFP records.
+.SH "EXIT STATUS"
+0 if the command was successful
+
+1 if an error occurred
diff --git a/freeipa.spec.in b/freeipa.spec.in
index 74b260b5d4ac50d573f98808991f6c6a7c4f25ab..86d61e482ad60a8f6d2eabb378d38ee61403dea7 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -1226,6 +1226,7 @@ fi
 %{_sbindir}/ipa-client-install
 %{_sbindir}/ipa-client-automount
 %{_sbindir}/ipa-certupdate
+%{_sbindir}/ipa-sshupdate
 %{_sbindir}/ipa-getkeytab
 %{_sbindir}/ipa-rmkeytab
 %{_sbindir}/ipa-join
@@ -1234,6 +1235,7 @@ fi
 %{_mandir}/man1/ipa-client-install.1.gz
 %{_mandir}/man1/ipa-client-automount.1.gz
 %{_mandir}/man1/ipa-certupdate.1.gz
+%{_mandir}/man1/ipa-sshupdate.1.gz
 %{_mandir}/man1/ipa-join.1.gz
 
 
diff --git a/install/po/Makefile.in b/install/po/Makefile.in
index 2459ecad130c7532706223a8a0b125809c2928b4..b8be3137636bc7c0a65e728f155a04fa65de86df 100644
--- a/install/po/Makefile.in
+++ b/install/po/Makefile.in
@@ -44,6 +44,7 @@ PY_EXPLICIT_FILES = \
      client/ipa-client-install \
      client/ipa-client-automount \
      client/ipa-certupdate \
+     client/ipa-sshupdate \
      install/tools/ipa-adtrust-install \
      install/tools/ipa-advise \
      install/tools/ipa-backup \
diff --git a/ipaclient/ipa_sshupdate.py b/ipaclient/ipa_sshupdate.py
new file mode 100644
index 0000000000000000000000000000000000000000..843915b7849a42b51b39340bde9b66cfab57901d
--- /dev/null
+++ b/ipaclient/ipa_sshupdate.py
@@ -0,0 +1,154 @@
+# 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/>.
+#
+
+import os
+import tempfile
+import shutil
+
+from ipapython import admintool, ipautil, sysrestore, ssh
+from ipaplatform import services
+from ipaplatform.paths import paths
+from ipalib import api, errors
+
+
+class SshUpdate(admintool.AdminTool):
+    command_name = 'ipa-sshupdate'
+
+    usage = "%prog [options]"
+
+    description = ("Update host identity of local client on IPA server with "
+                   "current SSH public keys. By default update also SSHFP DNS "
+                   "records.")
+
+    def __init__(self, options, args):
+        super(SshUpdate, self).__init__(options, args)
+
+    @classmethod
+    def add_options(cls, parser):
+        super(SshUpdate, cls).add_options(parser)
+
+        parser.add_option("--no-dns-sshfp", dest="update_sshfp", default=True,
+                          action="store_false",
+                          help="do not update DNS SSHFP records")
+
+    def validate_options(self):
+        super(SshUpdate, self).validate_options(needs_root=True)
+
+    def run(self):
+        options = self.options
+        fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
+        if (not fstore.has_files() and
+                not os.path.exists(paths.IPA_DEFAULT_CONF)):
+            raise admintool.ScriptError(
+                "IPA client is not configured on this system.")
+
+        ssh_dir = services.knownservices.sshd.get_config_dir()
+        pubkeys = []
+        for basename in os.listdir(ssh_dir):
+            if not basename.endswith('.pub'):
+                continue
+            filename = os.path.join(ssh_dir, basename)
+
+            try:
+                f = open(filename, 'r')
+            except IOError as e:
+                self.log.warning("Failed to open '%s': %s", filename, str(e))
+                continue
+
+            for line in f:
+                line = line[:-1].lstrip()
+                if not line or line.startswith('#'):
+                    continue
+                try:
+                    pubkey = ssh.SSHPublicKey(line)
+                except ValueError as UnicodeDecodeError:
+                    continue
+                self.log.info("Adding SSH public key from %s", filename)
+                pubkeys.append(pubkey)
+
+            f.close()
+
+        api.bootstrap(context='cli_installer')
+        api.finalize()
+
+        tmpdir = tempfile.mkdtemp(prefix="tmp-")
+        ccache_name = os.path.join(tmpdir, 'ccache')
+        try:
+            principal = str('host/%s@%s' % (api.env.host, api.env.realm))
+            ipautil.kinit_keytab(principal, paths.KRB5_KEYTAB, ccache_name)
+            os.environ['KRB5CCNAME'] = ccache_name
+
+            api.Backend.rpcclient.connect()
+            try:
+                api.Backend.rpcclient.forward(
+                    'host_mod',
+                    api.env.host,
+                    ipasshpubkey=[pk.openssh() for pk in pubkeys],
+                    updatedns=False,
+                    version=u'2.26',  # this version adds support for
+                )                     # SSH public keys
+            except errors.EmptyModlist:
+                pass
+            except Exception as e:
+                self.log.info("host_mod: %s", str(e))
+                self.log.warning("Failed to upload host SSH public keys.")
+                return
+            api.Backend.rpcclient.disconnect()
+
+            if options.update_sshfp:
+                ttl = 1200
+
+                update_txt = 'debug\n'
+                update_txt += (
+                    'update delete %s. IN SSHFP\nshow\nsend\n' % api.env.host)
+                for pubkey in pubkeys:
+                    sshfp = pubkey.fingerprint_dns_sha1()
+                    if sshfp is not None:
+                        update_txt += (
+                            'update add %s. %s IN SSHFP %s\n' %
+                            (api.env.host, ttl, sshfp))
+                    sshfp = pubkey.fingerprint_dns_sha256()
+                    if sshfp is not None:
+                        update_txt += (
+                            'update add %s. %s IN SSHFP %s\n' %
+                            (api.env.host, ttl, sshfp))
+                update_txt += 'show\nsend\n'
+
+                if not self.do_nsupdate(update_txt):
+                    self.log.warning("Could not update DNS SSHFP records.")
+        finally:
+            shutil.rmtree(tmpdir)
+
+    def do_nsupdate(self, update_txt):
+        UPDATE_FILE = paths.IPA_DNS_UPDATE_TXT
+        self.log.debug("Writing nsupdate commands to %s:", UPDATE_FILE)
+        self.log.debug("%s", update_txt)
+
+        with open(UPDATE_FILE, "w") as update_fd:
+            update_fd.write(update_txt)
+            update_fd.flush()
+
+        result = False
+        try:
+            ipautil.run([paths.NSUPDATE, '-g', UPDATE_FILE])
+            result = True
+        except ipautil.CalledProcessError as e:
+            self.log.debug('nsupdate failed: %s', str(e))
+
+        try:
+            os.remove(UPDATE_FILE)
+        except Exception:
+            pass
+
+        return result
-- 
2.5.0

Attachment: signature.asc
Description: This is a digitally signed message part

-- 
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