From 86c2e5b2f036ef4236744b1da30cb554c7ee60ac Mon Sep 17 00:00:00 2001 From: Kamil Sambor Date: Mon, 30 Jun 2014 14:52:02 +0200 Subject: [PATCH] Add authentication in fuel-cli * modified APIClient to get token * modified base test class for fuel-cli to mock keystone authentication * modified fuel-cli to use user and password Change-Id: I4b0a2dc485c086949b103c11689ea892069f02e5 Implements: blueprint access-control-master-node --- fuelclient/cli/actions/base.py | 5 ++++ fuelclient/cli/error.py | 3 ++ fuelclient/cli/parser.py | 21 ++++++++++++++ fuelclient/client.py | 52 ++++++++++++++++++++++++++++++---- requirements.txt | 1 + tests/base.py | 1 + 6 files changed, 77 insertions(+), 6 deletions(-) diff --git a/fuelclient/cli/actions/base.py b/fuelclient/cli/actions/base.py index 8d9b7c71..70e24b88 100644 --- a/fuelclient/cli/actions/base.py +++ b/fuelclient/cli/actions/base.py @@ -46,6 +46,11 @@ class Action(object): """Entry point for all actions subclasses """ APIClient.debug_mode(debug=params.debug) + if getattr(params, 'user') and getattr(params, 'password'): + APIClient.user = params.user + APIClient.password = params.password + APIClient.initialize_keystone_client() + self.serializer = Serializer.from_params(params) if self.flag_func_map is not None: for flag, method in self.flag_func_map: diff --git a/fuelclient/cli/error.py b/fuelclient/cli/error.py index 6bbb6356..eb9acca8 100644 --- a/fuelclient/cli/error.py +++ b/fuelclient/cli/error.py @@ -13,6 +13,7 @@ # under the License. from functools import wraps +from keystoneclient.exceptions import Unauthorized import sys import urllib2 @@ -77,6 +78,8 @@ def handle_exceptions(exc): )) elif isinstance(exc, urllib2.URLError): exit_with_error("Can't connect to Nailgun server!") + elif isinstance(exc, Unauthorized): + exit_with_error("Unauthorized: need authentication!") elif isinstance(exc, FuelClientException): exit_with_error(exc.message) else: diff --git a/fuelclient/cli/parser.py b/fuelclient/cli/parser.py index 8dc277ab..dc883932 100644 --- a/fuelclient/cli/parser.py +++ b/fuelclient/cli/parser.py @@ -44,6 +44,7 @@ class Parser: self.generate_actions() self.add_version_args() self.add_debug_arg() + self.add_keystone_credentials_args() self.add_serializers_args() def generate_actions(self): @@ -101,6 +102,26 @@ class Parser: default=False ) + def add_keystone_credentials_args(self): + self.universal_flags.append("--os-username") + self.parser.add_argument( + "--os-username", + dest="user", + action="store", + type=str, + help="credentials for keystone authentication user", + default=None + ) + self.universal_flags.append("--os-password") + self.parser.add_argument( + "--os-password", + dest="password", + action="store", + type=str, + help="credentials for keystone authentication password", + default=None + ) + def add_version_args(self): for args in (get_version_arg(), get_fuel_version_arg()): self.parser.add_argument(*args["args"], **args["params"]) diff --git a/fuelclient/client.py b/fuelclient/client.py index cbc2127e..3f43c6e2 100644 --- a/fuelclient/client.py +++ b/fuelclient/client.py @@ -18,6 +18,8 @@ import urllib2 import yaml +from keystoneclient import client as auth_client + from fuelclient.cli.error import exceptions_decorator @@ -27,10 +29,14 @@ class Client(object): def __init__(self): self.debug = False - path_to_config = "/etc/fuel-client.yaml" + self.test_mod = bool(os.environ.get('TEST_MODE', '')) + path_to_config = "/etc/fuel/client/config.yaml" defaults = { - "LISTEN_ADDRESS": "127.0.0.1", - "LISTEN_PORT": "8000" + "SERVER_ADDRESS": "127.0.0.1", + "LISTEN_PORT": "8000", + "KEYSTONE_USER": "admin", + "KEYSTONE_PASS": "admin", + "KEYSTONE_PORT": "5000", } if os.path.exists(path_to_config): with open(path_to_config, "r") as fh: @@ -38,9 +44,34 @@ class Client(object): defaults.update(config) else: defaults.update(os.environ) - self.root = "http://{LISTEN_ADDRESS}:{LISTEN_PORT}".format(**defaults) + self.root = "http://{SERVER_ADDRESS}:{LISTEN_PORT}".format(**defaults) + self.keystone_base = ( + "http://{SERVER_ADDRESS}:{LISTEN_PORT}/keystone/v2.0" + .format(**defaults) + ) self.api_root = self.root + "/api/v1/" self.ostf_root = self.root + "/ostf/" + self.auth_status() + self.user = defaults["KEYSTONE_USER"] + self.password = defaults["KEYSTONE_PASS"] + self.keystone_client = None + self.initialize_keystone_client() + + def auth_status(self): + self.auth_required = False + if not self.test_mod: + request = urllib2.urlopen(''.join([self.api_root, 'version'])) + self.auth_required = json.loads( + request.read()).get('auth_required', False) + + def initialize_keystone_client(self): + if not self.test_mod and self.auth_required: + self.keystone_client = auth_client.Client( + username=self.user, + password=self.password, + auth_url=self.keystone_base, + tenant_name="admin") + self.keystone_client.authenticate() def debug_mode(self, debug=False): self.debug = debug @@ -53,12 +84,14 @@ class Client(object): def delete_request(self, api): """Make DELETE request to specific API with some data """ + token = self.keystone_client.auth_token if self.keystone_client else '' self.print_debug( "DELETE {0}".format(self.api_root + api) ) opener = urllib2.build_opener(urllib2.HTTPHandler) request = urllib2.Request(self.api_root + api) request.add_header('Content-Type', 'application/json') + request.add_header('X-Auth-Token', token) request.get_method = lambda: 'DELETE' opener.open(request) return {} @@ -66,6 +99,7 @@ class Client(object): def put_request(self, api, data): """Make PUT request to specific API with some data """ + token = self.keystone_client.auth_token if self.keystone_client else '' data_json = json.dumps(data) self.print_debug( "PUT {0} data={1}" @@ -74,6 +108,7 @@ class Client(object): opener = urllib2.build_opener(urllib2.HTTPHandler) request = urllib2.Request(self.api_root + api, data=data_json) request.add_header('Content-Type', 'application/json') + request.add_header('X-Auth-Token', token) request.get_method = lambda: 'PUT' return json.loads( opener.open(request).read() @@ -82,19 +117,23 @@ class Client(object): def get_request(self, api, ostf=False): """Make GET request to specific API """ + token = self.keystone_client.auth_token if self.keystone_client else '' url = (self.ostf_root if ostf else self.api_root) + api self.print_debug( "GET {0}" .format(url) ) - request = urllib2.urlopen(url) + opener = urllib2.build_opener(urllib2.HTTPHandler) + request = urllib2.Request(url) + request.add_header('X-Auth-Token', token) return json.loads( - request.read() + opener.open(request).read() ) def post_request(self, api, data, ostf=False): """Make POST request to specific API with some data """ + token = self.keystone_client.auth_token if self.keystone_client else '' url = (self.ostf_root if ostf else self.api_root) + api data_json = json.dumps(data) self.print_debug( @@ -108,6 +147,7 @@ class Client(object): 'Content-Type': 'application/json' } ) + request.add_header('X-Auth-Token', token) try: response = json.loads( urllib2.urlopen(request) diff --git a/requirements.txt b/requirements.txt index ed99ca8f..f6fcc3d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,3 +23,4 @@ simplejson==3.3.0 web.py==0.37 wsgilog==0.3 wsgiref==0.1.2 +python-keystoneclient>=0.7 diff --git a/tests/base.py b/tests/base.py index fb849709..08ca7f93 100644 --- a/tests/base.py +++ b/tests/base.py @@ -102,6 +102,7 @@ class BaseTestCase(TestCase): def run_cli_command(self, command_line, check_errors=False): modified_env = os.environ.copy() + modified_env['TEST_MODE'] = 'True' command_args = [" ".join((self.fuel_path, command_line))] process_handle = subprocess.Popen( command_args,