Skip to content
This repository has been archived by the owner on Nov 9, 2020. It is now read-only.

code refactoring for tenant management API #729

Merged
merged 20 commits into from
Nov 22, 2016
Merged

Conversation

lipingxue
Copy link
Contributor

This PR includes:

  1. Refactor code to add an abstract layer provide APIs for tenant management
  2. add vmdk_ops unit test for multi-tenency
  3. make "name" unique in "tenants" table

@lipingxue
Copy link
Contributor Author

Please review it @msterin @shaominchen

@pdhamdhere
Copy link
Contributor

@lipingxue CI failing. Please fix.

return operation_fail(error_info)

name = args.name
name = args.name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently we do not validate any parameters. We should. Separate PR is fine with me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue #734 is filed to track this.

Copy link
Contributor

@pdhamdhere pdhamdhere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a quick first pass. See comments below.

@@ -324,6 +326,25 @@ def add_volume_to_volumes_table(tenant_uuid, datastore, vol_name, vol_size_in_MB

return None

def remove_volume_from_volumes_table(tenant_uuid, datastore, vol_name):
""" Remove volume from volumes table. """
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a line about return code e.g. "Returns None on success or error string" to all functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

vm_uuid = vmdk_utils.get_vm_uuid_by_name(vm_name)
if not vm_uuid:
error_info = "Cannot find vm_uuid for vm {0}, tenant_create failed".format(vm_name)
return error_info, None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I had commented on this last PR. We should not terminate loop on first bad VM name. Rather iterate through entire list and return VMs that are successfully assigned and VMs that couldn't be found.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

'global_visibility': 0,
'create_volume': 0,
'delete_volume': 0,
'mount_volume': 0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Looks off by a space.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

def default_privileges():
privileges = [{'datastore': 'datastore1',
'global_visibility': 0,
'create_volume': 0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we going to group privileges as discussed in #709

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a refactoring of existing code, changing/ grouping provileges should not be in this PR IMO.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Fine with separate PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Existing issue #709 tracked this.

@@ -28,6 +28,7 @@
CMD_REMOVE = 'remove'
CMD_ATTACH = 'attach'
CMD_DETACH = 'detach'
CMD_LIST = 'list'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had removed this in my last PR. Any reason we are re-introducing?
See executeRequest. Auth checks are bypassed for 'list' command.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my unit test code, I need to test 'list' command. It seems that your change assume we only put commands which need authorize in auth.py. I will remove 'list' command from this file and update the comment.

# mount_volume, max_volume_size, usage_quota)
privileges_dict = {}
privileges_dict[auth_data_const.COL_DATASTORE] = privileges[1]
privileges_dict[auth_data_const.COL_GLOBAL_VISIBILITY] = privileges[2]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't we get rid of GLOBAL_VISIBILITY?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on this one - there are too many open issues on this one , we need to drop it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Remove "global_visibility" from the code.

@@ -359,15 +359,16 @@ def create_tables(self):
'''
CREATE TABLE tenants(
-- uuid for the tenant, which is generated by create_tenant() API
id TEXT PRIMARY KEY NOT NULL,
id TEXT NOT NULL,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If name is unique then why do we need ID?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so we can rename without moving volumes, and have something invariant no matter what the ever-changing name is :-)
Still ma consider using it as a key, just need to be careful on rename

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR ##711 tracks "tenant rename".

vol_size_in_MB = convert.convert_to_MB(auth.get_vol_size(opts))
auth.add_volume_to_volumes_table(tenant_uuid, datastore, vol_name, vol_size_in_MB)
else:
logging.warning(" VM %s does not belong to any tenant", vm_name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warnings can flood logs. How about changing it to debug?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's not a big deal now - we should not have this warning once "_fallback" is implemented - in which case "not belonging to any tenant" is a warning and the access for VM is denied

However given the costs, it's easier to change to debug than discuss :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the log to "DEBUG".

return error_info
else:
if not vm_name:
logging.warning("VM %s does not belong to any tenant", vm_name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. These warnings can flood logs. Change it to debug?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please try to avoid copy-n-paste of messages. It is usually an indication that you need to either introduce a global for format string, or a simple function to return formatted string

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this PR, I just define a global for format string. But in the long run, I think we need to have a file which defines all the error code and corresponding error string for all the error message we used in our code.

Copy link
Contributor

@msterin msterin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments inline. All in all good change , the majority of comments are format / readability issues (e.g. docstrings or copy-n-paste of messages).

privileges = privileges)

error_info, tenant = auth_api._tenant_create(
name = name,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applies to multiple places:

PEP8 https://www.python.org/dev/peps/pep-0008/#whitespace-in-expressions-and-statements:

Don't use spaces around the = sign when used to indicate a keyword argument or a default parameter value.

Yes:

def complex(real, imag=0.0):
    return magic(r=real, i=imag)

No:

def complex(real, imag = 0.0):
    return magic(r = real, i = imag)

(note that spaces around assignment in other cases are still needed)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

datastore = args.datastore
rights = args.rights
volume_maxsize = args.volume_maxsize
volume_totalsize = args.volume_totalsize
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are those assignment really needed ? What prevents us from simply callng

 error_info = auth_api._tenant_access_add(
           name=args.name,
           datastore=args.datastore,
           privileges=generate_privileges(args),
...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

privileges_dict = modify_privileges(privileges_dict, args)

error_info = tenant.set_datastore_access_privileges(_auth_mgr.conn, [privileges_dict])
error_info = auth_api._tenant_access_set(name, datastore, add_rights, rm_rights, volume_maxsize, volume_totalsize)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question - assignment above do not not seem to be needed, you can use param=args.field in the call to _tenant_access_set

the comment applies to the rest of the wrappers too

import auth_data
import vmdk_utils

def get_auth_mgr():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dosctrings are missing, please add - that applies to all new methods/functions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

def get_auth_mgr():
try:
auth_mgr = auth.get_auth_mgr()
except auth_data.DbConnectionError, e:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's new code, please use 'except as . If this is just code move, its OK to leave it as is

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if (not self.datastore_name):
datastores = vmdk_utils.get_datastores()
datastore = datastores[0]
if (not datastore):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks suspicious. Is there a case where non-empty array with None as the first element is returned by get_datastores() ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


def tearDown(self):
""" Cleanup after each test """
logging.debug("VMDKTenantTest tearDown path")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logging.info ? we are in tests, it's important to have traces

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

vmdk_ops.removeVMDK(vmdk_path)

# cleanup existing tenant
error_info = auth_api._tenant_rm(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: PEP8 (just a reminder). no spaces around = in param assignment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

self.assertEqual("tenant2_vol2", result[1]['Name'])
self.assertEqual("tenant2_vol3", result[2]['Name'])


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A general comment on tests: it is better to have them data drivel . I.e. instead of using tenantx_volx ad opts and other stuff in the code, ALL that should be in data structures and the test should just taverse the structures. See example (testInfo traversing) in vmdk_ops_test.py.

It's OK to leave it as is for this PR but please do open an issue for test rearrangement and making them data driven - that allows to understand tests better and to add to the test set easily

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue #740 is filed to track this

@@ -62,6 +62,7 @@ def test_create_delete(self):

def test_delete_nonexistent_policy_fails(self):
self.assertNotEqual(None, vsan_policy.delete(self.name))
print ("The test itself is expected to fail, and please ignore the errors printed above.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does not it have to be stderr ? Or log ? (just asking since I do not see this message in the log

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

put it in logging.info.

Copy link
Contributor

@msterin msterin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, almost there. A couple of files are missing standard headers ,and I am not sure why ' name' is a part of keys in Tenant. When this is resolved, ready to merge

@@ -0,0 +1,336 @@
import auth
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add standard file header

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was auto-generated? CC/ @kerneltime

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was auto-generated when we did a pass of adding headers :-). Now it is copy-n-paste before push.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

-- brief description of the tenant, which is specified by user when creating the tenant
-- this field can be changed laster by using set_description API
description TEXT,
-- not use currently
default_datastore TEXT
default_datastore TEXT,
PRIMARY KEY(id, name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need a key with both id, name ? does it mean the key changes when name changes ?
I thought ID would be the key and name would be just a text field (unique is good, but just text, not key) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Make "name" to be UNIQUE, but not the key.

@@ -446,6 +442,12 @@ def create_tenant(self, name, description, default_datastore, default_privileges

"""
logging.debug ("create_tenant name=%s", name)
error_info, exist_tenant = self.get_tenant(name)
if exist_tenant:
error_info = error_code.TENANT_ALREADY_EXIST.format(name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice !

@@ -0,0 +1,7 @@
# Tenant related error message
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add standard file header

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -214,4 +214,38 @@ def get_vm_uuid_by_name(vm_name):
return vm[0].config.uuid
except:
return None
def get_datastore_path(ds_name):
for (datastore, url_name, path) in get_datastores():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dosctring (or comment) is missing. Also, an empty line before def is needed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor

@pdhamdhere pdhamdhere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took quick look. It's huge change so couldn't go through logic again. Overall LGTM. Please address Mark's review comments and I have some minor comments below. Once those are addressed, it's good to go from my side.

privileges=privileges)
if error_info:
return error_info, None

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Print/ Log not_found_vms informing user?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

vol_name = vmdk_utils.strip_vmdk_extension(vmdk['filename']),
vm_name = None,
tenant_uuid = tenant_id,
datastore = vmdk['datastore'])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces still exist. Forgot to push?

VM_NOT_BELONG_TO_TENANT = "VM {0} does not belong to any tenant"
TENANT_NOT_EXIST = "Tenant {0} does not exist"
TENANT_ALREADY_EXIST = "Tenant {0} already exists"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return err(msg)

error_info = err(msg)
remove_err = removeVMDK(vmdk_path = vmdk_path,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove space?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

logging.warning(" VM %s does not belong to any tenant", vm_name)

elif cmd == "create":
response = createVMDK(vmdk_path = vmdk_path,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove spaces?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

elif cmd == "remove":
response = removeVMDK(vmdk_path)
response = removeVMDK(vmdk_path = vmdk_path,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove spaces?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@msterin
Copy link
Contributor

msterin commented Nov 18, 2016

@lipingxue - it looks like there are some conflicts to be resolved. and the latest changes (marked as "done" are still not in. Looking forward...

@lipingxue
Copy link
Contributor Author

Yes. After rebase with the master, there are some conflict with latest change in master(Bruno's change). I am addressing those conflict and rerun the test. Will do the push when it is done.

@lipingxue
Copy link
Contributor Author

@msterin I have resolved conflict and push the change. Please take another look.

@@ -438,52 +438,6 @@ def remove_vm(si, vm_name):
task = vm.Destroy_Task()
vmdk_ops.wait_for_tasks(si, [task])

class ValidationTestCase(unittest.TestCase):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this test is removed?

@lipingxue lipingxue force-pushed the auth_refactor.liping branch 3 times, most recently from 910a9db to 3f8b606 Compare November 21, 2016 17:39
@lipingxue
Copy link
Contributor Author

@msterin
As we discussed last Friday, CI test failed due to DB schema existed in the testbed.
In order to make the test always start from a clean state, we decided to add code to remove auth_DB at each test. I added code for that, but it does not work.
The CI test failed with "disk I/O error" when accessing auth DB

I checked the log and also looked at our code, and I figured out this is reason why I see the above error. We call function "get_auth_mgr()" to get a connection to the DB. This function will call "thread_local._auth_mgr.connect()" ONLY if it does not have a cached value. In function"AuthorizationDataManager.connect()", we have the logic to decide whether we need to create table or not (check whether DB file exists or not).

Assume we start from the state that we have pre-exist DB in the testbed.

  1. first time call "get_auth_mgr" to get a DB connection. Since this is the first time, it will further call AuthorizationDataManager.connect() to check whether create table is needed or not. Here
    we don't need create the table since we already has the DB file in the test bed. The function
    return a connection for the DB.

  2. remove the auth DB file in the test code

  3. second time call "get_auth_mgr" to get a DB connection. This time, it will not call AuthorizationDataManager.connect() since it already has the cached value. Therefore, it returned
    a connection to staled DB file.

I think we only need to remove the old DB one time. This logic also apply to the possible DB migration.
Now I change the logic as follows:

  1. At the beginning of AuthorizationDataManager.connect(), check if we have DB file, it we have, and we haven't removed it (upgraded), do remove the DB.
  2. After that, existing code will check whether we have the DB file (we don't have now since we just remove it), and if we don't have, it will create the DB file and return the connection to the DB.

Please review this newly added code.

Copy link
Contributor

@msterin msterin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

file removal should be a part of the esx_service/Makefile not .py code

logging.debug("auth DB %s exist, try to remove the file", AUTH_DB_PATH)
os.remove(AUTH_DB_PATH)
# insert some delay
time.sleep(60)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the delay ?


if os.path.isfile(self.db_path):
# already exist
if not has_migrate_auth_db and self.need_migrate_auth_db():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.need_migrate_auth_db() need to check for 'has already migrated' (what you cann has_migrate_auth_db ) for better encapsulation

# already exist
if not has_migrate_auth_db and self.need_migrate_auth_db():
self.migrate_auth_db()
has_migrate_auth_db = True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, since it's global it's "test and set" should be atomic , so I suggest a lock here

@@ -117,9 +117,11 @@ function deployesx {
do
TARGET=root@$ip
log "Deploying to ESX $TARGET"
$SSH $TARGET rm -f /etc/vmware/vmdkops/log_config.json
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one should be a different commit - it's a useful fix

@lipingxue
Copy link
Contributor Author

@msterin I have addressed your comments, please review.

Copy link
Contributor

@msterin msterin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contingent on passing CI

@lipingxue lipingxue merged commit 8717d75 into master Nov 22, 2016
ashahi1 pushed a commit to ashahi1/docker-volume-vsphere that referenced this pull request Nov 22, 2016
* Refactor code to add an abstract layer to provide APIs for tenant management.
Those APIs can be called by both AdminCLI and VMODL code.
* Add unit test for multi-tenancy in "vmdk_ops_test.py"
* remove volume from "volumes" table when removing the volume successfully.
* make change to make "tenant name" unique in DB schema.
* Force remove file "/etc/vmware/vmdkops/log_config.json" in function "deployesx".
* Add code to force removing auth DB file before running unit test.
@msterin msterin deleted the auth_refactor.liping branch November 23, 2016 22:54
@lipingxue
Copy link
Contributor Author

Fixed #710

# for free to subscribe to this conversation on GitHub. Already have an account? #.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants