diff options
-rw-r--r-- | ipalib/cli.py | 44 | ||||
-rw-r--r-- | ipalib/plugins/baseldap.py | 42 | ||||
-rw-r--r-- | ipalib/plugins/dns.py | 159 |
3 files changed, 225 insertions, 20 deletions
diff --git a/ipalib/cli.py b/ipalib/cli.py index 99f236bb4..5e1365dc3 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -527,6 +527,47 @@ class textui(backend.Backend): return None return self.decode(data) + def prompt_yesno(self, label, default=None): + """ + Prompt user for yes/no input. This method returns True/False according + to user response. + + Parameter "default" should be True, False or None + + If Default parameter is not None, user can enter an empty input instead + of Yes/No answer. Value passed to Default is returned in that case. + + If Default parameter is None, user is asked for Yes/No answer until + a correct answer is provided. Answer is then returned. + + In case of an error, a None value may returned + """ + + default_prompt = None + if default is not None: + if default: + default_prompt = "Yes" + else: + default_prompt = "No" + + if default_prompt: + prompt = u'%s Yes/No (default %s): ' % (label, default_prompt) + else: + prompt = u'%s Yes/No: ' % label + + while True: + try: + data = raw_input(self.encode(prompt)).lower() + except EOFError: + return None + + if data in (u'yes', u'y'): + return True + elif data in ( u'n', u'no'): + return False + elif default is not None and data == u'': + return default + def prompt_password(self, label): """ Prompt user for a password or read it in via stdin depending @@ -1032,6 +1073,9 @@ class cli(backend.Executioner): param.label ) + for callback in getattr(cmd, 'INTERACTIVE_PROMPT_CALLBACKS', []): + callback(kw) + def load_files(self, cmd, kw): """ Load files from File parameters. diff --git a/ipalib/plugins/baseldap.py b/ipalib/plugins/baseldap.py index 3908dfe3e..7d4552576 100644 --- a/ipalib/plugins/baseldap.py +++ b/ipalib/plugins/baseldap.py @@ -482,12 +482,17 @@ class CallbackInterface(Method): self.__class__.POST_CALLBACKS = [] if not hasattr(self.__class__, 'EXC_CALLBACKS'): self.__class__.EXC_CALLBACKS = [] + if not hasattr(self.__class__, 'INTERACTIVE_PROMPT_CALLBACKS'): + self.__class__.INTERACTIVE_PROMPT_CALLBACKS = [] if hasattr(self, 'pre_callback'): self.register_pre_callback(self.pre_callback, True) if hasattr(self, 'post_callback'): self.register_post_callback(self.post_callback, True) if hasattr(self, 'exc_callback'): self.register_exc_callback(self.exc_callback, True) + if hasattr(self, 'interactive_prompt_callback'): + self.register_interactive_prompt_callback( + self.interactive_prompt_callback, True) #pylint: disable=E1101 super(Method, self).__init__() @classmethod @@ -520,6 +525,16 @@ class CallbackInterface(Method): else: klass.EXC_CALLBACKS.append(callback) + @classmethod + def register_interactive_prompt_callback(klass, callback, first=False): + assert callable(callback) + if not hasattr(klass, 'INTERACTIVE_PROMPT_CALLBACKS'): + klass.INTERACTIVE_PROMPT_CALLBACKS = [] + if first: + klass.INTERACTIVE_PROMPT_CALLBACKS.insert(0, callback) + else: + klass.INTERACTIVE_PROMPT_CALLBACKS.append(callback) + def _call_exc_callbacks(self, args, options, exc, call_func, *call_args, **call_kwargs): rv = None for i in xrange(len(getattr(self, 'EXC_CALLBACKS', []))): @@ -670,6 +685,9 @@ class LDAPCreate(CallbackInterface, crud.Create): def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): raise exc + def interactive_prompt_callback(self, kw): + return + # list of attributes we want exported to JSON json_friendly_attributes = ( 'takes_args', 'takes_options', @@ -795,6 +813,9 @@ class LDAPRetrieve(LDAPQuery): def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): raise exc + def interactive_prompt_callback(self, kw): + return + class LDAPUpdate(LDAPQuery, crud.Update): """ @@ -959,6 +980,9 @@ class LDAPUpdate(LDAPQuery, crud.Update): def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): raise exc + def interactive_prompt_callback(self, kw): + return + class LDAPDelete(LDAPMultiQuery): """ @@ -1046,6 +1070,9 @@ class LDAPDelete(LDAPMultiQuery): def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): raise exc + def interactive_prompt_callback(self, kw): + return + class LDAPModMember(LDAPQuery): """ @@ -1191,6 +1218,9 @@ class LDAPAddMember(LDAPModMember): def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): raise exc + def interactive_prompt_callback(self, kw): + return + class LDAPRemoveMember(LDAPModMember): """ @@ -1297,6 +1327,9 @@ class LDAPRemoveMember(LDAPModMember): def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): raise exc + def interactive_prompt_callback(self, kw): + return + class LDAPSearch(CallbackInterface, crud.Search): """ @@ -1501,6 +1534,9 @@ class LDAPSearch(CallbackInterface, crud.Search): def exc_callback(self, args, options, exc, call_func, *call_args, **call_kwargs): raise exc + def interactive_prompt_callback(self, kw): + return + # list of attributes we want exported to JSON json_friendly_attributes = ( 'takes_options', @@ -1644,6 +1680,9 @@ class LDAPAddReverseMember(LDAPModReverseMember): def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): raise exc + def interactive_prompt_callback(self, kw): + return + class LDAPRemoveReverseMember(LDAPModReverseMember): """ Remove other LDAP entries from members in reverse. @@ -1753,3 +1792,6 @@ class LDAPRemoveReverseMember(LDAPModReverseMember): def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): raise exc + + def interactive_prompt_callback(self, kw): + return diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py index 3f8753d11..42ca498c9 100644 --- a/ipalib/plugins/dns.py +++ b/ipalib/plugins/dns.py @@ -1,5 +1,6 @@ # Authors: # Pavel Zuna <pzuna@redhat.com> +# Martin Kosek <mkosek@redhat.com> # # Copyright (C) 2010 Red Hat # see file 'COPYING' for use and warranty information @@ -49,6 +50,28 @@ EXAMPLES: ipa dnsrecord-add example.com _ldap._tcp --srv-rec="0 1 389 slow.example.com" ipa dnsrecord-add example.com _ldap._tcp --srv-rec="1 1 389 backup.example.com" + When dnsrecord-add command is executed with no option to add a specific record + an interactive mode is started. The mode interactively prompts for the most + typical record types for the respective zone: + ipa dnsrecord-add example.com www + [A record]: 1.2.3.4,11.22.33.44 (2 interactively entered random IPs) + [AAAA record]: (no AAAA address entered) + Record name: www + A record: 1.2.3.4, 11.22.33.44 + + The interactive mode can also be used for deleting the DNS records: + ipa dnsrecord-del example.com www + No option to delete specific record provided. + Delete all? Yes/No (default No): (do not delete all records) + Current DNS record contents: + + A record: 1.2.3.4, 11.22.33.44 + + Delete A record '1.2.3.4'? Yes/No (default No): + Delete A record '11.22.33.44'? Yes/No (default No): y + Record name: www + A record: 1.2.3.4 (A record 11.22.33.44 has been deleted) + Show zone example.com: ipa dnszone-show example.com @@ -71,7 +94,6 @@ EXAMPLES: if one is not included): ipa dns-resolve www.example.com ipa dns-resolve www - """ import netaddr @@ -93,6 +115,14 @@ _record_types = ( u'TSIG', u'TXT', ) +# DNS zone record identificator +_dns_zone_record = u'@' + +# most used record types, always ask for those in interactive prompt +_top_record_types = ('A', 'AAAA', ) +_rev_top_record_types = ('PTR', ) +_zone_top_record_types = ('NS', 'MX', 'LOC', ) + # attributes derived from record types _record_attributes = [str('%srecord' % t.lower()) for t in _record_types] @@ -195,6 +225,14 @@ _valid_reverse_zones = { '.ip6.arpa.' : 32, } +def zone_is_reverse(zone_name): + for rev_zone_name in _valid_reverse_zones.keys(): + if zone_name.endswith(rev_zone_name): + return True + + return False + + def has_cli_options(entry, no_option_msg): entry = dict((t, entry.get(t, [])) for t in _record_attributes) numattr = reduce(lambda x,y: x+y, @@ -505,7 +543,7 @@ class dnsrecord(LDAPObject): def is_pkey_zone_record(self, *keys): idnsname = keys[-1] - if idnsname == '@' or idnsname == ('%s.' % keys[-2]): + if idnsname == str(_dns_zone_record) or idnsname == ('%s.' % keys[-2]): return True return False @@ -533,25 +571,40 @@ class dnsrecord_cmd_w_record_options(Command): def get_record_options(self): for t in _record_types: t = t.encode('utf-8') - doc = self.record_param_doc % t - validator = _record_validators.get(t) - if validator: - yield List( - '%srecord?' % t.lower(), validator, - cli_name='%s_rec' % t.lower(), doc=doc, - label='%s record' % t, attribute=True - ) - else: - yield List( - '%srecord?' % t.lower(), cli_name='%s_rec' % t.lower(), - doc=doc, label='%s record' % t, attribute=True - ) + yield self.get_record_option(t) def record_options_2_entry(self, **options): entries = dict((t, options.get(t, [])) for t in _record_attributes) entries.update(dict((k, []) for (k,v) in entries.iteritems() if v == None )) return entries + def get_record_option(self, rec_type): + doc = self.record_param_doc % rec_type + validator = _record_validators.get(rec_type) + if validator: + return List( + '%srecord?' % rec_type.lower(), validator, + cli_name='%s_rec' % rec_type.lower(), doc=doc, + label='%s record' % rec_type, attribute=True + ) + else: + return List( + '%srecord?' % rec_type.lower(), cli_name='%s_rec' % rec_type.lower(), + doc=doc, label='%s record' % rec_type, attribute=True + ) + + def prompt_record_options(self, rec_type_list): + user_options = {} + # ask for all usual record types + for rec_type in rec_type_list: + rec_option = self.get_record_option(rec_type) + raw = self.Backend.textui.prompt(rec_option.label,optional=True) + rec_value = rec_option(raw) + if rec_value is not None: + user_options[rec_option.name] = rec_value + + return user_options + class dnsrecord_mod_record(LDAPQuery, dnsrecord_cmd_w_record_options): """ @@ -599,7 +652,7 @@ class dnsrecord_mod_record(LDAPQuery, dnsrecord_cmd_w_record_options): self.obj.handle_not_found(*keys) if self.obj.is_pkey_zone_record(*keys): - entry_attrs[self.obj.primary_key.name] = [u'@'] + entry_attrs[self.obj.primary_key.name] = [_dns_zone_record] retval = self.post_callback(keys, entry_attrs) if retval: @@ -637,7 +690,8 @@ class dnsrecord_add(LDAPCreate, dnsrecord_cmd_w_record_options): """ Add new DNS resource record. """ - no_option_msg = 'No options to add a specific record provided.' + no_option_msg = 'No options to add a specific record provided.\n' \ + "Command help may be consulted for all supported record types." takes_options = LDAPCreate.takes_options + ( Flag('force', label=_('Force'), @@ -693,6 +747,24 @@ class dnsrecord_add(LDAPCreate, dnsrecord_cmd_w_record_options): return dn + def interactive_prompt_callback(self, kw): + for param in kw.keys(): + if param in _record_attributes: + # some record type entered, skip this helper + return + + # check zone type + if kw['idnsname'] == _dns_zone_record: + top_record_types = _zone_top_record_types + elif zone_is_reverse(kw['dnszoneidnsname']): + top_record_types = _rev_top_record_types + else: + top_record_types = _top_record_types + + # ask for all usual record types + user_options = self.prompt_record_options(top_record_types) + kw.update(user_options) + def pre_callback(self, ldap, dn, entry_attrs, *keys, **options): for rtype in options: rtype_cb = '_%s_pre_callback' % rtype @@ -727,7 +799,8 @@ class dnsrecord_del(dnsrecord_mod_record): """ Delete DNS resource record. """ - no_option_msg = _('Neither --del-all nor options to delete a specific record provided.') + no_option_msg = _('Neither --del-all nor options to delete a specific record provided.\n'\ + "Command help may be consulted for all supported record types.") takes_options = ( Flag('del_all', default=False, @@ -745,6 +818,52 @@ class dnsrecord_del(dnsrecord_mod_record): entry = super(dnsrecord_del, self).record_options_2_entry(**options) return has_cli_options(entry, self.no_option_msg) + def interactive_prompt_callback(self, kw): + if kw.get('del_all', False): + return + for param in kw.keys(): + if param in _record_attributes: + # we have something to delete, skip this helper + return + + # get DNS record first so that the NotFound exception is raised + # before the helper would start + dns_record = api.Command['dnsrecord_show'](kw['dnszoneidnsname'], kw['idnsname'])['result'] + rec_types = [rec_type for rec_type in dns_record if rec_type in _record_attributes] + + self.Backend.textui.print_plain(_("No option to delete specific record provided.")) + user_del_all = self.Backend.textui.prompt_yesno(_("Delete all?"), default=False) + + if user_del_all is True: + kw['del_all'] = True + return + + # ask user for records to be removed + dns_record = api.Command['dnsrecord_show'](kw['dnszoneidnsname'], kw['idnsname'])['result'] + rec_types = [rec_type for rec_type in dns_record if rec_type in _record_attributes] + + self.Backend.textui.print_plain(_(u'Current DNS record contents:\n')) + present_params = [] + for param in self.params(): + if param.name in _record_attributes and param.name in dns_record: + present_params.append(param) + rec_type_content = u', '.join(dns_record[param.name]) + self.Backend.textui.print_plain(u'%s: %s' % (param.label, rec_type_content)) + self.Backend.textui.print_plain(u'') + + # ask what records to remove + for param in present_params: + deleted_values = [] + for rec_value in dns_record[param.name]: + user_del_value = self.Backend.textui.prompt_yesno( + _(u"Delete %s '%s'?" + % (param.label, rec_value)), default=False) + if user_del_value is True: + deleted_values.append(rec_value) + if deleted_values: + deleted_list = u','.join(deleted_values) + kw[param.name] = param(deleted_list) + def update_old_entry_callback(self, entry_attrs, old_entry_attrs): for (a, v) in entry_attrs.iteritems(): if not isinstance(v, (list, tuple)): @@ -776,7 +895,7 @@ class dnsrecord_show(LDAPRetrieve, dnsrecord_cmd_w_record_options): def post_callback(self, ldap, dn, entry_attrs, *keys, **options): if self.obj.is_pkey_zone_record(*keys): - entry_attrs[self.obj.primary_key.name] = [u'@'] + entry_attrs[self.obj.primary_key.name] = [_dns_zone_record] return dn api.register(dnsrecord_show) @@ -805,7 +924,7 @@ class dnsrecord_find(LDAPSearch, dnsrecord_cmd_w_record_options): zone_obj = self.api.Object[self.obj.parent_object] zone_dn = zone_obj.get_dn(args[0]) if entries[0][0] == zone_dn: - entries[0][1][zone_obj.primary_key.name] = [u'@'] + entries[0][1][zone_obj.primary_key.name] = [_dns_zone_record] api.register(dnsrecord_find) |