summaryrefslogtreecommitdiffstats
path: root/ipalib
diff options
context:
space:
mode:
authorRob Crittenden <rcritten@redhat.com>2010-07-26 17:54:38 -0400
committerRob Crittenden <rcritten@redhat.com>2010-08-16 10:35:27 -0400
commit1df10a88cd8b36be8b9b4d47c49dd9e7d1d12bc0 (patch)
tree965da3c4c157e0aaba6b876b578ebcf8a7dc190d /ipalib
parent3e6f0f5721f76977475792f09758f6b8dcc4ed4e (diff)
downloadfreeipa-1df10a88cd8b36be8b9b4d47c49dd9e7d1d12bc0.tar.gz
freeipa-1df10a88cd8b36be8b9b4d47c49dd9e7d1d12bc0.tar.xz
freeipa-1df10a88cd8b36be8b9b4d47c49dd9e7d1d12bc0.zip
Add support for client failover to the ipa command-line.
This adds a new global option to the ipa command, -f/--no-fallback. If this is included then just the server configured in /etc/ipa/default.conf is used. Otherwise that is tried first then all servers in DNS with the ldap SRV record are tried. Create a new Local() Command class for local-only commands. The help command is one of these. It shouldn't need a remote connection to execute. ticket #15
Diffstat (limited to 'ipalib')
-rw-r--r--ipalib/backend.py3
-rw-r--r--ipalib/cli.py5
-rw-r--r--ipalib/constants.py1
-rw-r--r--ipalib/frontend.py15
-rw-r--r--ipalib/plugable.py7
-rw-r--r--ipalib/plugins/ping.py42
-rw-r--r--ipalib/rpc.py86
7 files changed, 142 insertions, 17 deletions
diff --git a/ipalib/backend.py b/ipalib/backend.py
index b17c517ab..58d063555 100644
--- a/ipalib/backend.py
+++ b/ipalib/backend.py
@@ -109,7 +109,8 @@ class Executioner(Backend):
if self.env.in_server:
self.Backend.ldap2.connect(ccache=ccache)
else:
- self.Backend.xmlclient.connect(verbose=(self.env.verbose >= 2))
+ self.Backend.xmlclient.connect(verbose=(self.env.verbose >= 2),
+ fallback=self.env.fallback)
if client_ip is not None:
setattr(context, "client_ip", client_ip)
diff --git a/ipalib/cli.py b/ipalib/cli.py
index 81269cd45..d19e397a0 100644
--- a/ipalib/cli.py
+++ b/ipalib/cli.py
@@ -572,7 +572,7 @@ class textui(backend.Backend):
self.print_line('')
return selection
-class help(frontend.Command):
+class help(frontend.Local):
"""
Display help for a command or topic.
"""
@@ -778,12 +778,13 @@ class cli(backend.Executioner):
if len(argv) == 0:
self.Command.help()
return
- self.create_context()
(key, argv) = (argv[0], argv[1:])
name = from_cli(key)
if name not in self.Command or self.Command[name].INTERNAL:
raise CommandError(name=key)
cmd = self.Command[name]
+ if not isinstance(cmd, frontend.Local):
+ self.create_context()
kw = self.parse(cmd, argv)
if self.env.interactive:
self.prompt_interactively(cmd, kw)
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 66f13f25c..26ff6236c 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -130,6 +130,7 @@ DEFAULT_CONFIG = (
# Special CLI:
('prompt_all', False),
('interactive', True),
+ ('fallback', True),
# Enable certain optional plugins:
('enable_ra', False),
diff --git a/ipalib/frontend.py b/ipalib/frontend.py
index 1c4fea8cb..e505f5354 100644
--- a/ipalib/frontend.py
+++ b/ipalib/frontend.py
@@ -930,6 +930,21 @@ class LocalOrRemote(Command):
return self.execute(*args, **options)
+class Local(Command):
+ """
+ A command that is explicitly executed locally.
+
+ This is for commands that makes sense to execute only locally
+ such as the help command.
+ """
+
+ def run(self, *args, **options):
+ """
+ Dispatch to forward() onlly.
+ """
+ return self.forward(*args, **options)
+
+
class Object(HasParam):
backend = None
methods = None
diff --git a/ipalib/plugable.py b/ipalib/plugable.py
index fd5f31a76..cb87ebeab 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -455,6 +455,10 @@ class API(DictProxy):
dest='interactive',
help='Prompt for NO values (even if required)'
)
+ parser.add_option('-f', '--no-fallback', action='store_false',
+ dest='fallback',
+ help='Only use the server configured in /etc/ipa/default.conf'
+ )
topics = optparse.OptionGroup(parser, "Available help topics",
"ipa help topics")
cmds = optparse.OptionGroup(parser, "Available commands",
@@ -479,7 +483,8 @@ class API(DictProxy):
# --Jason, 2008-10-31
pass
overrides[str(key.strip())] = value.strip()
- for key in ('conf', 'debug', 'verbose', 'prompt_all', 'interactive'):
+ for key in ('conf', 'debug', 'verbose', 'prompt_all', 'interactive',
+ 'fallback'):
value = getattr(options, key, None)
if value is not None:
overrides[key] = value
diff --git a/ipalib/plugins/ping.py b/ipalib/plugins/ping.py
new file mode 100644
index 000000000..a18f3aa52
--- /dev/null
+++ b/ipalib/plugins/ping.py
@@ -0,0 +1,42 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2010 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; version 2 only
+#
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+"""
+Ping the remote IPA server
+"""
+
+from ipalib import api
+from ipalib import Command
+from ipalib import output
+
+class ping(Command):
+ """
+ ping a remote server
+ """
+ has_output = (
+ output.summary,
+ )
+
+ def execute(self):
+ """
+ A possible enhancement would be to take an argument and echo it
+ back but a fixed value works for now.
+ """
+ return dict(summary=u'pong')
+
+api.register(ping)
diff --git a/ipalib/rpc.py b/ipalib/rpc.py
index 686df3cba..472e0628b 100644
--- a/ipalib/rpc.py
+++ b/ipalib/rpc.py
@@ -37,13 +37,14 @@ import errno
from xmlrpclib import Binary, Fault, dumps, loads, ServerProxy, Transport, ProtocolError
import kerberos
from ipalib.backend import Connectible
-from ipalib.errors import public_errors, PublicError, UnknownError, NetworkError
+from ipalib.errors import public_errors, PublicError, UnknownError, NetworkError, KerberosError
from ipalib import errors
from ipalib.request import context
-from ipapython import ipautil
+from ipapython import ipautil, dnsclient
import httplib
from ipapython.nsslib import NSSHTTPS
from nss.error import NSPRError
+from urllib2 import urlparse
# Some Kerberos error definitions from krb5.h
KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN = (-1765328377L)
@@ -255,12 +256,70 @@ class xmlclient(Connectible):
super(xmlclient, self).__init__()
self.__errors = dict((e.errno, e) for e in public_errors)
- def create_connection(self, ccache=None, verbose=False):
- kw = dict(allow_none=True, encoding='UTF-8')
- if self.env.xmlrpc_uri.startswith('https://'):
- kw['transport'] = KerbTransport()
- kw['verbose'] = verbose
- return ServerProxy(self.env.xmlrpc_uri, **kw)
+ def reconstruct_url(self):
+ """
+ The URL directly isn't stored in the ServerProxy. We can't store
+ it in the connection object itself but we can reconstruct it
+ from the ServerProxy.
+ """
+ if not hasattr(self.conn, '_ServerProxy__transport'):
+ return None
+ if isinstance(self.conn._ServerProxy__transport, KerbTransport):
+ scheme = "https"
+ else:
+ scheme = "http"
+ server = '%s://%s%s' % (scheme, self.conn._ServerProxy__host, self.conn._ServerProxy__handler)
+ return server
+
+ def get_url_list(self):
+ """
+ Create a list of urls consisting of the available IPA servers.
+ """
+ # the configured URL defines what we use for the discovered servers
+ (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(self.env.xmlrpc_uri)
+ servers = []
+ name = '_ldap._tcp.%s.' % self.env.domain
+ rs = dnsclient.query(name, dnsclient.DNS_C_IN, dnsclient.DNS_T_SRV)
+ for r in rs:
+ if r.dns_type == dnsclient.DNS_T_SRV:
+ rsrv = r.rdata.server.rstrip('.')
+ servers.append('https://%s%s' % (rsrv, path))
+ servers = list(set(servers))
+ # the list/set conversion won't preserve order so stick in the
+ # local config file version here.
+ servers.insert(0, self.env.xmlrpc_uri)
+ return servers
+
+ def create_connection(self, ccache=None, verbose=False, fallback=True):
+ servers = self.get_url_list()
+ serverproxy = None
+ for server in servers:
+ kw = dict(allow_none=True, encoding='UTF-8')
+ kw['verbose'] = verbose
+ if server.startswith('https://'):
+ kw['transport'] = KerbTransport()
+ self.log.info('trying %s' % server)
+ serverproxy = ServerProxy(server, **kw)
+ if len(servers) == 1 or not fallback:
+ # if we have only 1 server to try then let the main
+ # requester handle any errors
+ return serverproxy
+ try:
+ command = getattr(serverproxy, 'ping')
+ response = command()
+ # We don't care about the response, just that we got one
+ break
+ except KerberosError, krberr:
+ # kerberos error on one server is likely on all
+ raise errors.KerberosError(major=str(krberr), minor='')
+ except Exception, e:
+ if not fallback:
+ raise e
+ serverproxy = None
+
+ if serverproxy is None:
+ raise NetworkError(uri='any of the configured servers', error=', '.join(servers))
+ return serverproxy
def destroy_connection(self):
pass
@@ -280,7 +339,8 @@ class xmlclient(Connectible):
raise ValueError(
'%s.forward(): %r not in api.Command' % (self.name, name)
)
- self.info('Forwarding %r to server %r', name, self.env.xmlrpc_uri)
+ server = self.reconstruct_url()
+ self.info('Forwarding %r to server %r', name, server)
command = getattr(self.conn, name)
params = [args, kw]
try:
@@ -289,16 +349,16 @@ class xmlclient(Connectible):
except Fault, e:
e = decode_fault(e)
self.debug('Caught fault %d from server %s: %s', e.faultCode,
- self.env.xmlrpc_uri, e.faultString)
+ server, e.faultString)
if e.faultCode in self.__errors:
error = self.__errors[e.faultCode]
raise error(message=e.faultString)
raise UnknownError(
code=e.faultCode,
error=e.faultString,
- server=self.env.xmlrpc_uri,
+ server=server,
)
except NSPRError, e:
- raise NetworkError(uri=self.env.xmlrpc_uri, error=str(e))
+ raise NetworkError(uri=server, error=str(e))
except ProtocolError, e:
- raise NetworkError(uri=self.env.xmlrpc_uri, error=e.errmsg)
+ raise NetworkError(uri=server, error=e.errmsg)