Introduce platform-specific adaptation for services used by FreeIPA.

Refactor FreeIPA code to allow abstracting all calls to external processes and
dependencies on modification of system-wide configuration. A platform provider
would give its own implementation of those methods and FreeIPA would use it
based on what's built in packaging process.

https://fedorahosted.org/freeipa/ticket/1605
This commit is contained in:
Alexander Bokovoy 2011-09-13 00:01:23 +03:00 committed by Martin Kosek
parent d4a2851873
commit b73b017897
11 changed files with 437 additions and 98 deletions

View File

@ -8,6 +8,8 @@ PRJ_PREFIX=freeipa
RPMBUILD ?= $(PWD)/rpmbuild RPMBUILD ?= $(PWD)/rpmbuild
TARGET ?= master TARGET ?= master
SUPPORTED_PLATFORM=redhat
# After updating the version in VERSION you should run the version-update # After updating the version in VERSION you should run the version-update
# target. # target.
@ -109,6 +111,12 @@ version-update: release-update
ipa-client/ipa-client.spec.in > ipa-client/ipa-client.spec ipa-client/ipa-client.spec.in > ipa-client/ipa-client.spec
sed -e s/__VERSION__/$(IPA_VERSION)/ ipa-client/version.m4.in \ sed -e s/__VERSION__/$(IPA_VERSION)/ ipa-client/version.m4.in \
> ipa-client/version.m4 > ipa-client/version.m4
if [ "$(SUPPORTED_PLATFORM)" != "" ]; then \
sed -e s/SUPPORTED_PLATFORM/$(SUPPORTED_PLATFORM)/ ipapython/services.py.in \
> ipapython/services.py; \
fi
if [ "$(SKIP_API_VERSION_CHECK)" != "yes" ]; then \ if [ "$(SKIP_API_VERSION_CHECK)" != "yes" ]; then \
./makeapi --validate; \ ./makeapi --validate; \
fi fi

View File

@ -535,7 +535,9 @@ fi
%defattr(-,root,root,-) %defattr(-,root,root,-)
%doc COPYING README Contributors.txt %doc COPYING README Contributors.txt
%dir %{python_sitelib}/ipapython %dir %{python_sitelib}/ipapython
%dir %{python_sitelib}/ipapython/platform
%{python_sitelib}/ipapython/*.py* %{python_sitelib}/ipapython/*.py*
%{python_sitelib}/ipapython/platform/*.py*
%dir %{python_sitelib}/ipalib %dir %{python_sitelib}/ipalib
%{python_sitelib}/ipalib/* %{python_sitelib}/ipalib/*
%{python_sitearch}/default_encoding_utf8.so %{python_sitearch}/default_encoding_utf8.so
@ -547,6 +549,9 @@ fi
%ghost %attr(0644,root,apache) %config(noreplace) %{_sysconfdir}/ipa/default.conf %ghost %attr(0644,root,apache) %config(noreplace) %{_sysconfdir}/ipa/default.conf
%changelog %changelog
* Mon Sep 12 2011 Alexander Bokovoy <abokovoy@redhat.com> - 2.1.1-1
- Make sure platform adaptation is packaged in -python sub-package
* Fri Sep 9 2011 Martin Kosek <mkosek@redhat.com> - 2.1.0-4 * Fri Sep 9 2011 Martin Kosek <mkosek@redhat.com> - 2.1.0-4
- Add soft dependency for bind and bind-dyndb-ldap required versions - Add soft dependency for bind and bind-dyndb-ldap required versions

View File

@ -27,7 +27,7 @@ clean:
done done
distclean: clean distclean: clean
rm -f setup.py ipa-python.spec version.py rm -f setup.py ipa-python.spec version.py services.py
@for subdir in $(SUBDIRS); do \ @for subdir in $(SUBDIRS); do \
(cd $$subdir && $(MAKE) $@) || exit 1; \ (cd $$subdir && $(MAKE) $@) || exit 1; \
done done

View File

@ -1,6 +1,6 @@
# Authors: Simo Sorce <ssorce@redhat.com> # Authors: Simo Sorce <ssorce@redhat.com>
# #
# Copyright (C) 2007 Red Hat # Copyright (C) 2007-2011 Red Hat
# see file 'COPYING' for use and warranty information # see file 'COPYING' for use and warranty information
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
@ -1053,51 +1053,6 @@ def get_gsserror(e):
return (major, minor) return (major, minor)
def service_stop(service_name, instance_name="", capture_output=True):
run(["/sbin/service", service_name, "stop", instance_name],
capture_output=capture_output)
def service_start(service_name, instance_name="", capture_output=True):
run(["/sbin/service", service_name, "start", instance_name],
capture_output=capture_output)
def service_restart(service_name, instance_name="", capture_output=True):
run(["/sbin/service", service_name, "restart", instance_name],
capture_output=capture_output)
def service_is_running(service_name, instance_name=""):
ret = True
try:
run(["/sbin/service", service_name, "status", instance_name])
except CalledProcessError:
ret = False
return ret
def service_is_installed(service_name):
installed = True
try:
run(["/sbin/service", service_name, "status"])
except CalledProcessError, e:
if e.returncode == 1:
# service is not installed or there is other serious issue
installed = False
return installed
def service_is_enabled(service_name):
(stdout, stderr, returncode) = run(["/sbin/chkconfig", service_name], raiseonerr=False)
return (returncode == 0)
def chkconfig_on(service_name):
run(["/sbin/chkconfig", service_name, "on"])
def chkconfig_off(service_name):
run(["/sbin/chkconfig", service_name, "off"])
def chkconfig_add(service_name):
run(["/sbin/chkconfig", "--add", service_name])
def chkconfig_del(service_name):
run(["/sbin/chkconfig", "--del", service_name])
def host_port_open(host, port, socket_stream=True, socket_timeout=None): def host_port_open(host, port, socket_stream=True, socket_timeout=None):
families = (socket.AF_INET, socket.AF_INET6) families = (socket.AF_INET, socket.AF_INET6)
@ -1171,3 +1126,4 @@ def bind_port_responder(port, socket_stream=True, socket_timeout=None, responder
s.sendto(responder_data, addr) s.sendto(responder_data, addr)
finally: finally:
s.close() s.close()

View File

@ -0,0 +1,23 @@
# Authors:
# Alexander Bokovoy <abokovoy@redhat.com>
#
# Copyright (C) 2011 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/>.
"""
Sub-package containing all platform-specific adaptation for ipapython.services.
Should not be used directly.
"""

150
ipapython/platform/base.py Normal file
View File

@ -0,0 +1,150 @@
# Authors: Alexander Bokovoy <abokovoy@redhat.com>
#
# Copyright (C) 2011 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 ipalib.plugable import MagicDict
# Canonical names of services as IPA wants to see them. As we need to have *some* naming,
# set them as in Red Hat distributions. Actual implementation should make them available
# through knownservices.<name> and take care of remapping internally, if needed
wellknownservices = ['certmonger', 'dirsrv', 'httpd', 'ipa', 'krb5kdc', 'messagebus',
'nslcd', 'nscd', 'ntpd', 'portmap', 'rpcbind']
class AuthConfig(object):
"""
AuthConfig class implements system-independent interface to configure
system authentication resources. In Red Hat systems this is done with
authconfig(8) utility.
AuthConfig class is nothing more than a tool to gather configuration options
and execute their processing. These options then converted by an actual implementation
to series of a system calls to appropriate utilities performing real configuration.
IPA *expects* names of AuthConfig's options to follow authconfig(8) naming scheme!
Actual implementation should be done in ipapython/platform/<platform>.py by inheriting from
platform.AuthConfig and redefining __build_args() and execute() methods.
from ipapython.platform import platform
class PlatformAuthConfig(platform.AuthConfig):
def __build_args():
...
def execute():
...
authconfig = PlatformAuthConfig
....
See ipapython/platform/redhat.py for a sample implementation that uses authconfig(8) as its backend.
From IPA code perspective, the authentication configuration should be done with use of ipapython.services.authconfig:
from ipapython import services as ipaservices
auth_config = ipaservices.authconfig()
auth_config.disable("ldap").\
disable("krb5").\
disable("sssd").\
disable("sssdauth").\
disable("mkhomedir").\
add_option("update").\
enable("nis").\
add_parameter("nisdomain","foobar")
auth_config.execute()
If you need to re-use existing AuthConfig instance for multiple runs, make sure to
call 'AuthConfig.reset()' between the runs.
"""
def __init__(self):
self.parameters = {}
def enable(self, option):
self.parameters[option] = True
return self
def disable(self, option):
self.parameters[option] = False
return self
def add_option(self, option):
self.parameters[option] = None
return self
def add_parameter(self, option, value):
self.parameters[option] = [value]
return self
def __build_args(self):
# do nothing
return None
def execute(self):
# do nothing
return None
def reset(self):
self.parameters = {}
return self
class PlatformService(object):
"""
PlatformService abstracts out external process running on the system which is possible
to administer (start, stop, check status, etc).
"""
def __init__(self, service_name):
self.service_name = service_name
def start(self, instance_name="", capture_output=True):
return
def stop(self, instance_name="", capture_output=True):
return
def restart(self, instance_name="", capture_output=True):
return
def is_running(self):
return False
def is_installed(self):
return False
def is_enabled(self):
return False
def enable(self):
return
def disable(self):
return
def install(self):
return
def remove(self):
return
class KnownServices(MagicDict):
"""
KnownServices is an abstract class factory that should give out instances of well-known
platform services. Actual implementation must create these instances as its own attributes
on first access (or instance creation) and cache them.
"""

View File

@ -0,0 +1,176 @@
# Authors: Simo Sorce <ssorce@redhat.com>
# Alexander Bokovoy <abokovoy@redhat.com>
#
# Copyright (C) 2007-2011 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/>.
#
import tempfile
import re
import os
import stat
import sys
from ipapython import ipautil
from ipapython.platform import base
# All what we allow exporting directly from this module
# Everything else is made available through these symbols when they directly imported into ipapython.services:
# authconfig -- class reference for platform-specific implementation of authconfig(8)
# service -- class reference for platform-specific implementation of a PlatformService class
# knownservices -- factory instance to access named services IPA cares about, names are ipapython.services.wellknownservices
# backup_and_replace_hostname -- platform-specific way to set hostname and make it persistent over reboots
# restore_context -- platform-sepcific way to restore security context, if applicable
__all__ = ['authconfig', 'service', 'knownservices', 'backup_and_replace_hostname', 'restore_context']
class RedHatService(base.PlatformService):
def stop(self, instance_name="", capture_output=True):
ipautil.run(["/sbin/service", self.service_name, "stop", instance_name], capture_output=capture_output)
def start(self, instance_name="", capture_output=True):
ipautil.run(["/sbin/service", self.service_name, "start", instance_name], capture_output=capture_output)
def restart(self, instance_name="", capture_output=True):
ipautil.run(["/sbin/service", self.service_name, "restart", instance_name], capture_output=capture_output)
def is_running(self, instance_name=""):
ret = True
try:
(sout,serr,rcode) = ipautil.run(["/sbin/service", self.service_name, "status", instance_name])
if sout.find("is stopped") >= 0:
ret = False
except ipautil.CalledProcessError:
ret = False
return ret
def is_installed(self):
installed = True
try:
ipautil.run(["/sbin/service", self.service_name, "status"])
except ipautil.CalledProcessError, e:
if e.returncode == 1:
# service is not installed or there is other serious issue
installed = False
return installed
def is_enabled(self):
(stdout, stderr, returncode) = ipautil.run(["/sbin/chkconfig", self.service_name],raiseonerr=False)
return (returncode == 0)
def enable(self):
ipautil.run(["/sbin/chkconfig", self.service_name, "on"])
def disable(self):
ipautil.run(["/sbin/chkconfig", self.service_name, "off"])
def install(self):
ipautil.run(["/sbin/chkconfig", "--add", self.service_name])
def remove(self):
ipautil.run(["/sbin/chkconfig", "--del", self.service_name])
class RedHatAuthConfig(base.AuthConfig):
"""
AuthConfig class implements system-independent interface to configure
system authentication resources. In Red Hat-produced systems this is done with
authconfig(8) utility.
"""
def __build_args(self):
args = []
for (option, value) in self.parameters.items():
if type(value) is bool:
if value:
args.append("--enable%s" % (option))
else:
args.append("--disable%s" % (option))
elif type(value) in (tuple, list):
args.append("--%s" % (option))
args.append("%s" % (value[0]))
elif value is None:
args.append("--%s" % (option))
else:
args.append("--%s%s" % (option,value))
return args
def execute(self):
args = self.__build_args()
ipautil.run(["/usr/sbin/authconfig"]+args)
class RedHatServices(base.KnownServices):
def __init__(self):
services = dict()
for s in base.wellknownservices:
services[s] = RedHatService(s)
# Call base class constructor. This will lock services to read-only
super(RedHatServices, self).__init__(services)
authconfig = RedHatAuthConfig
service = RedHatService
knownservices = RedHatServices()
def restore_context(filepath):
"""
restore security context on the file path
SE Linux equivalent is /sbin/restorecon <filepath>
"""
ipautil.run(["/sbin/restorecon", filepath])
def backup_and_replace_hostname(fstore, statestore, hostname):
network_filename = "/etc/sysconfig/network"
# Backup original /etc/sysconfig/network
fstore.backup_file(network_filename)
hostname_pattern = re.compile('''
(^
\s*
(?P<option> [^\#;]+?)
(\s*=\s*)
(?P<value> .+?)?
(\s*((\#|;).*)?)?
$)''', re.VERBOSE)
temp_filename = None
with tempfile.NamedTemporaryFile(delete=False) as new_config:
temp_filename = new_config.name
with open(network_filename, 'r') as f:
for line in f:
new_line = line
m = hostname_pattern.match(line)
if m:
option, value = m.group('option', 'value')
if option is not None and option == 'HOSTNAME':
if value is not None and hostname != value:
new_line = u"HOSTNAME=%s\n" % (hostname)
statestore.backup_state('network', 'hostname', value)
new_config.write(new_line)
new_config.flush()
# Make sure the resulting file is readable by others before installing it
os.fchmod(new_config.fileno(), stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
os.fchown(new_config.fileno(), 0, 0)
# At this point new_config is closed but not removed due to 'delete=False' above
# Now, install the temporary file as configuration and ensure old version is available as .orig
# While .orig file is not used during uninstall, it is left there for administrator.
ipautil.install_file(temp_filename, network_filename)
try:
ipautil.run(['/bin/hostname', hostname])
except ipautil.CalledProcessError, e:
print >>sys.stderr, "Failed to set this machine hostname to %s (%s)." % (hostname, str(e))
# For SE Linux environments it is important to reset SE labels to the expected ones
try:
restore_context(network_filename)
except ipautil.CalledProcessError, e:
print >>sys.stderr, "Failed to set permissions for %s (%s)." % (network_filename, str(e))

48
ipapython/services.py.in Normal file
View File

@ -0,0 +1,48 @@
# Authors: Alexander Bokovoy <abokovoy@redhat.com>
#
# Copyright (C) 2011 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/>.
# authconfig is an entry point to platform-provided AuthConfig implementation
# (instance of ipapython.platform.base.AuthConfig)
authconfig = None
# knownservices is an entry point to known platform services
# (instance of ipapython.platform.base.KnownServices)
knownservices = None
# service is a class to instantiate ipapython.platform.base.PlatformService
service = None
# restore context default implementation that does nothing
def restore_context_default(filepath):
return
# Restore security context for a path
# If the platform has security features where context is important, implement your own
# version in platform services
restore_context = restore_context_default
# Default implementation of backup and replace hostname that does nothing
def backup_and_replace_hostname_default(fstore, statestore, hostname):
return
# Backup and replace system's hostname
# Since many platforms have their own way how to store system's hostname, this method must be
# implemented in platform services
backup_and_replace_hostname = backup_and_replace_hostname_default
from ipapython.platform.SUPPORTED_PLATFORM import *

View File

@ -65,7 +65,7 @@ def setup_package():
classifiers=filter(None, CLASSIFIERS.split('\n')), classifiers=filter(None, CLASSIFIERS.split('\n')),
platforms = ["Linux", "Solaris", "Unix"], platforms = ["Linux", "Solaris", "Unix"],
package_dir = {'ipapython': ''}, package_dir = {'ipapython': ''},
packages = [ "ipapython" ], packages = [ "ipapython", "ipapython.platform" ],
) )
finally: finally:
del sys.path[0] del sys.path[0]

View File

@ -32,6 +32,7 @@ import random
import string import string
from ipapython import ipautil from ipapython import ipautil
from ipapython import services as ipaservices
SYSRESTORE_PATH = "/tmp" SYSRESTORE_PATH = "/tmp"
SYSRESTORE_INDEXFILE = "sysrestore.index" SYSRESTORE_INDEXFILE = "sysrestore.index"
@ -165,7 +166,7 @@ class FileStore:
os.chown(path, int(uid), int(gid)) os.chown(path, int(uid), int(gid))
os.chmod(path, int(mode)) os.chmod(path, int(mode))
ipautil.run(["/sbin/restorecon", path]) ipaservices.restore_context(path)
del self.files[filename] del self.files[filename]
self.save() self.save()
@ -196,7 +197,7 @@ class FileStore:
os.chown(path, int(uid), int(gid)) os.chown(path, int(uid), int(gid))
os.chmod(path, int(mode)) os.chmod(path, int(mode))
ipautil.run(["/sbin/restorecon", path]) ipaservices.restore_context(path)
#force file to be deleted #force file to be deleted
self.files = {} self.files = {}

View File

@ -22,6 +22,7 @@ import os, socket
import tempfile import tempfile
from ipapython import sysrestore from ipapython import sysrestore
from ipapython import ipautil from ipapython import ipautil
from ipapython import services as ipaservices
from ipalib import errors from ipalib import errors
import ldap import ldap
from ipaserver import ipaldap from ipaserver import ipaldap
@ -40,36 +41,6 @@ SERVICE_LIST = {
'CA':('pki-cad', 50) 'CA':('pki-cad', 50)
} }
def stop(service_name, instance_name="", capture_output=True):
ipautil.service_stop(service_name, instance_name, capture_output)
def start(service_name, instance_name="", capture_output=True):
ipautil.service_start(service_name, instance_name, capture_output)
def restart(service_name, instance_name="", capture_output=True):
ipautil.service_restart(service_name, instance_name, capture_output)
def is_running(service_name, instance_name=""):
return ipautil.service_is_running(service_name, instance_name)
def is_installed(service_name):
return ipautil.service_is_installed(service_name)
def chkconfig_on(service_name):
ipautil.chkconfig_on(service_name)
def chkconfig_off(service_name):
ipautil.chkconfig_on(service_name)
def chkconfig_add(service_name):
ipautil.chkconfig_on(service_name)
def chkconfig_del(service_name):
ipautil.chkconfig_on(service_name)
def is_enabled(service_name):
return ipautil.service_is_enabled(service_name)
def print_msg(message, output_fd=sys.stdout): def print_msg(message, output_fd=sys.stdout):
logging.debug(message) logging.debug(message)
output_fd.write(message) output_fd.write(message)
@ -79,6 +50,7 @@ def print_msg(message, output_fd=sys.stdout):
class Service(object): class Service(object):
def __init__(self, service_name, sstore=None, dm_password=None): def __init__(self, service_name, sstore=None, dm_password=None):
self.service_name = service_name self.service_name = service_name
self.service = ipaservices.service(service_name)
self.steps = [] self.steps = []
self.output_fd = sys.stdout self.output_fd = sys.stdout
self.dm_password = dm_password self.dm_password = dm_password
@ -213,31 +185,31 @@ class Service(object):
self.output_fd = fd self.output_fd = fd
def stop(self, instance_name="", capture_output=True): def stop(self, instance_name="", capture_output=True):
stop(self.service_name, instance_name, capture_output=capture_output) self.service.stop(instance_name, capture_output=capture_output)
def start(self, instance_name="", capture_output=True): def start(self, instance_name="", capture_output=True):
start(self.service_name, instance_name, capture_output=capture_output) self.service.start(instance_name, capture_output=capture_output)
def restart(self, instance_name="", capture_output=True): def restart(self, instance_name="", capture_output=True):
restart(self.service_name, instance_name, capture_output=capture_output) self.service.restart(instance_name, capture_output=capture_output)
def is_running(self): def is_running(self):
return is_running(self.service_name) return self.service.is_running()
def chkconfig_add(self): def install(self):
chkconfig_add(self.service_name) self.service.install()
def chkconfig_del(self): def remove(self):
chkconfig_del(self.service_name) self.service.remove()
def chkconfig_on(self): def enable(self):
chkconfig_on(self.service_name) self.service.enable()
def chkconfig_off(self): def disable(self):
chkconfig_off(self.service_name) self.service.disable()
def is_enabled(self): def is_enabled(self):
return is_enabled(self.service_name) return self.service.is_enabled()
def backup_state(self, key, value): def backup_state(self, key, value):
self.sstore.backup_state(self.service_name, key, value) self.sstore.backup_state(self.service_name, key, value)
@ -300,7 +272,7 @@ class Service(object):
return conn return conn
def ldap_enable(self, name, fqdn, dm_password, ldap_suffix): def ldap_enable(self, name, fqdn, dm_password, ldap_suffix):
self.chkconfig_off() self.disable()
conn = self.__get_conn(fqdn, dm_password) conn = self.__get_conn(fqdn, dm_password)
entry_name = "cn=%s,cn=%s,%s,%s" % (name, fqdn, entry_name = "cn=%s,cn=%s,%s,%s" % (name, fqdn,
@ -336,10 +308,10 @@ class SimpleServiceInstance(Service):
self.restart() self.restart()
def __enable(self): def __enable(self):
self.chkconfig_add() self.enable()
self.backup_state("enabled", self.is_enabled()) self.backup_state("enabled", self.is_enabled())
if self.gensvc_name == None: if self.gensvc_name == None:
self.chkconfig_on() self.enable()
else: else:
self.ldap_enable(self.gensvc_name, self.fqdn, self.ldap_enable(self.gensvc_name, self.fqdn,
self.dm_password, self.suffix) self.dm_password, self.suffix)
@ -354,5 +326,5 @@ class SimpleServiceInstance(Service):
if not running is None and not running: if not running is None and not running:
self.stop() self.stop()
if not enabled is None and not enabled: if not enabled is None and not enabled:
self.chkconfig_off() self.disable()
self.chkconfig_del() self.remove()