Add support for keystone v2 - v3

Ospurge can be used with keystone v2 and v3 with the new
authentication plugin (Session), the old one is deprecated.

Change-Id: Ife87da432f3f56c13679e6a93c8571620c635615
This commit is contained in:
Pierre-Alexandre Bardina 2016-07-01 17:05:23 +02:00 committed by Pierre-Alexandre Bardina
parent 328f6d23c8
commit aa909316b7
3 changed files with 159 additions and 71 deletions

View File

@ -21,8 +21,11 @@
import logging
import time
from keystoneclient import exceptions as api_exceptions
from keystoneclient.v2_0 import client as keystone_client
from keystoneauth1 import exceptions as api_exceptions
from keystoneauth1.identity import generic as keystone_auth
from keystoneauth1 import session as keystone_session
from keystoneclient import client as keystone_client
from keystoneclient import exceptions as keystone_exceptions
from ospurge import constants
from ospurge import exceptions
@ -74,31 +77,46 @@ class Session(object):
"""
def __init__(self, username, password, project_id, auth_url,
endpoint_type="publicURL", region_name=None, insecure=False):
client = keystone_client.Client(
username=username, password=password, tenant_id=project_id,
auth_url=auth_url, region_name=region_name, insecure=insecure)
endpoint_type="publicURL", insecure=False, **kwargs):
data = {
'username': username,
'password': password,
'project_id': project_id,
'user_domain_id': kwargs.get('user_domain_id'),
'user_domain_name': kwargs.get('user_domain_name'),
'project_domain_id': kwargs.get('project_domain_id'),
'project_domain_name': kwargs.get('project_domain_name'),
'domain_id': kwargs.get('domain_id')
}
auth = keystone_auth.Password(auth_url, **data)
session = keystone_session.Session(auth=auth, verify=(not insecure))
self.client = keystone_client.Client(session=session)
# Storing username, password, project_id and auth_url for
# use by clients libraries that cannot use an existing token.
self.username = username
self.password = password
self.project_id = project_id
self.project_id = auth.auth_ref.project_id
self.auth_url = auth_url
self.region_name = region_name
self.region_name = kwargs['region_name']
self.insecure = insecure
# Session variables to be used by clients when possible
self.token = client.auth_token
self.user_id = client.user_id
self.project_name = client.project_name
self.token = auth.auth_ref.auth_token
self.user_id = auth.auth_ref.user_id
self.project_name = self.client.project_name
self.keystone_session = session
self.endpoint_type = endpoint_type
self.catalog = client.service_catalog.get_endpoints()
self.catalog = auth.auth_ref.service_catalog.get_endpoints()
try:
# Detect if we are admin or not
client.roles.list() # Only admins are allowed to do this
self.client.roles.list() # Only admins are allowed to do this
except (
# The Exception Depends on OpenStack Infrastructure.
api_exceptions.Forbidden,
api_exceptions.ConnectionRefused, # admin URL not permitted
keystone_exceptions.ConnectionRefused, # admin URL not permitted
api_exceptions.Unauthorized,
):
self.is_admin = False
@ -107,7 +125,10 @@ class Session(object):
def get_endpoint(self, service_type):
try:
return self.catalog[service_type][0][self.endpoint_type]
if self.client.version == "v2.0":
return self.catalog[service_type][0][self.endpoint_type]
else:
return self.catalog[service_type][0]['url']
except (KeyError, IndexError):
# Endpoint could not be found
raise exceptions.EndpointNotFound(service_type)

View File

@ -37,8 +37,10 @@ import glanceclient.exc
from glanceclient.v1 import client as glance_client
from heatclient import client as heat_client
import heatclient.openstack.common.apiclient.exceptions
from keystoneclient import exceptions as api_exceptions
from keystoneclient.v2_0 import client as keystone_client
from keystoneauth1 import exceptions as api_exceptions
from keystoneauth1.identity import generic as keystone_auth
from keystoneauth1 import session as keystone_session
from keystoneclient import client as keystone_client
import neutronclient.common.exceptions
from neutronclient.v2_0 import client as neutron_client
from novaclient import client as nova_client
@ -57,12 +59,15 @@ class SwiftResources(base.Resources):
super(SwiftResources, self).__init__(session)
self.endpoint = self.session.get_endpoint("object-store")
self.token = self.session.token
conn = swift_client.HTTPConnection(self.endpoint, insecure=self.session.insecure)
conn = swift_client.HTTPConnection(self.endpoint,
insecure=self.session.insecure)
self.http_conn = conn.parsed_url, conn
# This method is used to retrieve Objects as well as Containers.
def list_containers(self):
containers = swift_client.get_account(self.endpoint, self.token, http_conn=self.http_conn)[1]
containers = swift_client.get_account(self.endpoint,
self.token,
http_conn=self.http_conn)[1]
return (cont['name'] for cont in containers)
@ -72,7 +77,10 @@ class SwiftObjects(SwiftResources):
swift_objects = []
for cont in self.list_containers():
objs = [{'container': cont, 'name': obj['name']} for obj in
swift_client.get_container(self.endpoint, self.token, cont, http_conn=self.http_conn)[1]]
swift_client.get_container(self.endpoint,
self.token,
cont,
http_conn=self.http_conn)[1]]
swift_objects.extend(objs)
return swift_objects
@ -103,13 +111,8 @@ class CinderResources(base.Resources):
def __init__(self, session):
super(CinderResources, self).__init__(session)
# Cinder client library can't use an existing token. When
# using this library, we have to reauthenticate.
self.client = cinder_client.Client(
session.username, session.password,
session.project_name, session.auth_url, session.insecure,
endpoint_type=session.endpoint_type,
region_name=session.region_name)
self.client = cinder_client.Client("2.1",
session=session.keystone_session)
class CinderSnapshots(CinderResources):
@ -163,11 +166,7 @@ class NeutronResources(base.Resources):
def __init__(self, session):
super(NeutronResources, self).__init__(session)
self.client = neutron_client.Client(
username=session.username, password=session.password,
tenant_id=session.project_id, auth_url=session.auth_url,
endpoint_type=session.endpoint_type,
region_name=session.region_name, insecure=session.insecure)
self.client = neutron_client.Client(session=session.keystone_session)
self.project_id = session.project_id
# This method is used for routers and interfaces removal
@ -443,11 +442,8 @@ class NovaServers(base.Resources):
def __init__(self, session):
super(NovaServers, self).__init__(session)
self.client = nova_client.Client(
"2", session.username, session.password,
session.project_name, auth_url=session.auth_url,
endpoint_type=session.endpoint_type,
region_name=session.region_name, insecure=session.insecure)
self.client = nova_client.Client("2.1",
session=session.keystone_session)
self.project_id = session.project_id
"""Manage nova resources"""
@ -542,14 +538,38 @@ class KeystoneManager(object):
"""Manages Keystone queries."""
def __init__(self, username, password, project, auth_url, insecure,
admin_role_name, **kwargs):
self.client = keystone_client.Client(
username=username, password=password,
tenant_name=project, auth_url=auth_url,
insecure=insecure, **kwargs)
self.admin_role_name = admin_role_name
**kwargs):
data = {
'username': username,
'password': password,
'project_name': project,
}
if kwargs['user_domain_name'] is not None:
if kwargs['project_domain_name'] is None:
kwargs['project_domain_name'] = 'Default'
data.update({
'domain_id': kwargs.get('domain_id'),
'project_domain_id': kwargs.get('project_domain_id'),
'project_domain_name': kwargs.get('project_domain_name'),
'user_domain_id': kwargs.get('user_domain_id'),
'user_domain_name': kwargs.get('user_domain_name')
})
self.auth = keystone_auth.Password(auth_url, **data)
session = keystone_session.Session(auth=self.auth, verify=(not insecure))
self.client = keystone_client.Client(session=session)
self.admin_role_id = None
self.tenant_info = None
self.admin_role_name = kwargs['admin_role_name']
self.user_id = self.auth.auth_ref.user_id
@property
def client_projects(self):
if self.client.version == "v2.0":
return self.client.tenants
return self.client.projects
def get_project_id(self, project_name_or_id=None):
"""Get a project by its id
@ -558,35 +578,34 @@ class KeystoneManager(object):
* ID of current project if called without parameter,
* ID of project given as parameter if one is given.
"""
if project_name_or_id is None:
return self.client.tenant_id
return self.auth.auth_ref.project_id
try:
self.tenant_info = self.client.tenants.get(project_name_or_id)
self.tenant_info = self.client_projects.get(project_name_or_id)
# If it doesn't raise an 404, project_name_or_id is
# already the project's id
project_id = project_name_or_id
except api_exceptions.NotFound:
try:
# Can raise api_exceptions.Forbidden:
tenants = self.client.tenants.list()
tenants = self.client_projects.list()
project_id = filter(
lambda x: x.name == project_name_or_id, tenants)[0].id
except IndexError:
raise exceptions.NoSuchProject(project_name_or_id)
if not self.tenant_info:
self.tenant_info = self.client.tenants.get(project_id)
self.tenant_info = self.client_projects.get(project_id)
return project_id
def enable_project(self, project_id):
logging.info("* Enabling project {}.".format(project_id))
self.tenant_info = self.client.tenants.update(project_id, enabled=True)
self.tenant_info = self.client_projects.update(project_id, enabled=True)
def disable_project(self, project_id):
logging.info("* Disabling project {}.".format(project_id))
self.tenant_info = self.client.tenants.update(project_id, enabled=False)
self.tenant_info = self.client_projects.update(project_id, enabled=False)
def get_admin_role_id(self):
if not self.admin_role_id:
@ -595,35 +614,45 @@ class KeystoneManager(object):
return self.admin_role_id
def become_project_admin(self, project_id):
user_id = self.client.user_id
user_id = self.user_id
admin_role_id = self.get_admin_role_id()
logging.info("* Granting role admin to user {} on project {}.".format(
user_id, project_id))
return self.client.roles.add_user_role(user_id, admin_role_id, project_id)
if self.client.version == "v2.0":
return self.client.roles.add_user_role(user_id, admin_role_id,
project_id)
else:
return self.client.roles.grant(role=admin_role_id, user=user_id,
project=project_id)
def undo_become_project_admin(self, project_id):
user_id = self.client.user_id
user_id = self.user_id
admin_role_id = self.get_admin_role_id()
logging.info("* Removing role admin to user {} on project {}.".format(
user_id, project_id))
return self.client.roles.remove_user_role(user_id, admin_role_id, project_id)
if self.client.version == "v2.0":
return self.client.roles.remove_user_role(user_id,
admin_role_id,
project_id)
else:
return self.client.roles.revoke(role=admin_role_id,
user=user_id,
project=project_id)
def delete_project(self, project_id):
logging.info("* Deleting project {}.".format(project_id))
self.client.tenants.delete(project_id)
self.client_projects.delete(project_id)
def perform_on_project(admin_name, password, project, auth_url,
endpoint_type='publicURL', region_name=None,
action='dump', insecure=False):
endpoint_type='publicURL', action='dump',
insecure=False, **kwargs):
"""Perform provided action on all resources of project.
action can be: 'purge' or 'dump'
"""
session = base.Session(admin_name, password, project, auth_url,
endpoint_type, region_name, insecure)
endpoint_type, insecure, **kwargs)
error = None
for rc in constants.RESOURCES_CLASSES:
try:
@ -701,7 +730,7 @@ def parse_args():
help="The user's password. Defaults "
"to env[OS_PASSWORD].")
parser.add_argument("--admin-project", action=EnvDefault,
envvar='OS_TENANT_NAME', required=True,
envvar='OS_TENANT_NAME', required=False,
help="Project name used for authentication. This project "
"will be purged if --own-project is set. "
"Defaults to env[OS_TENANT_NAME].")
@ -711,6 +740,26 @@ def parse_args():
envvar='OS_AUTH_URL', required=True,
help="Authentication URL. Defaults to "
"env[OS_AUTH_URL].")
parser.add_argument("--user-domain-id", action=EnvDefault,
envvar='OS_USER_DOMAIN_ID', required=False,
help="User Domain ID. Defaults to "
"env[OS_USER_DOMAIN_ID].")
parser.add_argument("--user-domain-name", action=EnvDefault,
envvar='OS_USER_DOMAIN_NAME', required=False,
help="User Domain ID. Defaults to "
"env[OS_USER_DOMAIN_NAME].")
parser.add_argument("--project-name", action=EnvDefault,
envvar='OS_PROJECT_NAME', required=False,
help="Project Name. Defaults to "
"env[OS_PROJECT_NAME].")
parser.add_argument("--project-domain-id", action=EnvDefault,
envvar='OS_PROJECT_DOMAIN_ID', required=False,
help="Project Domain ID. Defaults to "
"env[OS_PROJECT_DOMAIN_ID].")
parser.add_argument("--project-domain-name", action=EnvDefault,
envvar='OS_PROJECT_DOMAIN_NAME', required=False,
help="Project Domain NAME. Defaults to "
"env[OS_PROJECT_DOMAIN_NAME].")
parser.add_argument("--cleanup-project", required=False, default=None,
help="ID or Name of project to purge. Not required "
"if --own-project has been set. Using --cleanup-project "
@ -733,6 +782,8 @@ def parse_args():
if args.cleanup_project and args.own_project:
parser.error('Both --cleanup-project '
'and --own-project can not be set')
if not (args.admin_project or args.project_name):
parser.error('--admin-project or --project-name is required')
return args
@ -745,11 +796,20 @@ def main():
# Set default log level to Warning
logging.basicConfig(level=logging.WARNING)
data = {
'region_name': args.region_name,
'user_domain_id': args.user_domain_id,
'project_domain_id': args.project_domain_id,
'project_domain_name': args.project_domain_name,
'user_domain_name': args.user_domain_name,
'admin_role_name': args.admin_role_name
}
project = args.admin_project if args.admin_project else args.project_name
try:
keystone_manager = KeystoneManager(args.username, args.password,
args.admin_project, args.auth_url,
args.insecure, region_name=args.region_name,
admin_role_name=args.admin_role_name)
project, args.auth_url,
args.insecure, **data)
except api_exceptions.Unauthorized as exc:
print("Authentication failed: {}".format(str(exc)))
sys.exit(constants.AUTHENTICATION_FAILED_ERROR_CODE)
@ -786,8 +846,8 @@ def main():
try:
action = "dump" if args.dry_run else "purge"
perform_on_project(args.username, args.password, cleanup_project_id,
args.auth_url, args.endpoint_type, args.region_name,
action, args.insecure)
args.auth_url, args.endpoint_type, action,
args.insecure, **data)
except requests.exceptions.ConnectionError as exc:
print("Connection error: {}".format(str(exc)))
sys.exit(constants.CONNECTION_ERROR_CODE)

View File

@ -62,6 +62,8 @@ class HttpTest(testtools.TestCase):
httpretty.register_uri(method, url, **kwargs)
def stub_auth(self):
self.stub_url('GET', base_url=AUTH_URL,
json=client_fixtures.AUTH_URL_RESPONSE)
self.stub_url('POST', parts=['tokens'], base_url=AUTH_URL,
json=client_fixtures.PROJECT_SCOPED_TOKEN)
self.stub_url('GET', parts=['roles'],
@ -75,7 +77,8 @@ class SessionTest(HttpTest):
def test_init(self):
self.stub_auth()
session = base.Session(USERNAME, PASSWORD,
client_fixtures.PROJECT_ID, AUTH_URL)
client_fixtures.PROJECT_ID, AUTH_URL,
region_name="RegionOne")
self.assertEqual(session.token, client_fixtures.TOKEN_ID)
self.assertEqual(session.user_id, client_fixtures.USER_ID)
self.assertEqual(session.project_id, client_fixtures.PROJECT_ID)
@ -85,7 +88,8 @@ class SessionTest(HttpTest):
def test_get_public_endpoint(self):
self.stub_auth()
session = base.Session(USERNAME, PASSWORD,
client_fixtures.PROJECT_ID, AUTH_URL)
client_fixtures.PROJECT_ID, AUTH_URL,
region_name="RegionOne")
endpoint = session.get_endpoint('volume')
self.assertEqual(endpoint, client_fixtures.VOLUME_PUBLIC_ENDPOINT)
endpoint = session.get_endpoint('image')
@ -94,8 +98,10 @@ class SessionTest(HttpTest):
@httpretty.activate
def test_get_internal_endpoint(self):
self.stub_auth()
session = base.Session(USERNAME, PASSWORD, client_fixtures.PROJECT_ID,
AUTH_URL, endpoint_type='internalURL')
session = base.Session(USERNAME, PASSWORD,
client_fixtures.PROJECT_ID, AUTH_URL,
region_name="RegionOne",
endpoint_type='internalURL')
endpoint = session.get_endpoint('volume')
self.assertEqual(endpoint, client_fixtures.VOLUME_INTERNAL_ENDPOINT)
endpoint = session.get_endpoint('image')
@ -112,7 +118,8 @@ class TestResourcesBase(HttpTest):
super(TestResourcesBase, self).setUp()
self.stub_auth()
self.session = base.Session(USERNAME, PASSWORD,
client_fixtures.PROJECT_ID, AUTH_URL)
client_fixtures.PROJECT_ID, AUTH_URL,
region_name="RegionOne")
# We can't add other stubs in subclasses setUp because
# httpretty.dactivate() is called after this set_up (so during the
# super call to this method in subclasses). and extra stubs will not