Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test turbo server #78

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions .github/workflows/eco-vcenter-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,9 @@ jobs:
working-directory: ansible_collections/vmware/vmware
env:
ANSIBLE_COLLECTIONS_PATH: "${{ github.workspace }}"

- name: kill turbo server
if: ${{ always() }}
run: |
# kill turbo server
ps -ef | grep turbo | grep -v 'grep' |awk '{print $2}' | xargs kill -9
46 changes: 46 additions & 0 deletions docs/module_profiling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Module Profiling and Performance

It might be useful to check module performance when developing.

## Timing Tasks

Ansible comes with a basic profiler that outputs how long each task takes. You can enable it by setting an environment variable:
```bash
export ANSIBLE_CALLBACKS_ENABLED=profile_tasks
```

## Profiling Modules

Python comes with a profiler that will show how long each method takes. Heres a very basic example:
```python
def main():
# import the profiler and setup the context manager
import cProfile
import pstats
with cProfile.Profile() as pr:
argument_spec = VmwareRestClient.vmware_client_argument_spec()
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)

# Execute your module as usual
my_class = Foo(module)
out = my_class.do()

# save the profile results to a variable
stats = pstats.Stats(pr)

# sort by time and write the output to a file so you can view the results
stats.sort_stats(pstats.SortKey.TIME)
stats.dump_stats("/some/local/path/profile.prof")

# exit module as usual
module.exit_json(changed=False, out=out)
```

You can use a tool like `snakeviz` to view the profile results.
```bash
pip install snakeviz
snakviz /some/local/path/profile.prof
```
95 changes: 95 additions & 0 deletions docs/turbo_server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Using the Turbo Server and Cache

The VMware modules make a lot of API calls that can take a (relatively) long time to finish. To help speed things up, you can enable some caching functionality.

The cache relies on the turbo server provided by `cloud.common`. (ADD LINK HERE)

## Enabling and Configuring

To enable the cache, set the environment variable:
```bash
export ENABLE_TURBO_SERVER=1
```

The cache expires every 15 seconds by default. To change the length of time before the cache expires, set the environment variable:
```bash
export ANSIBLE_TURBO_LOOKUP_TTL=120
```

You can also set these variables in your playbook, if that's more convenient:
```yaml
- name: Example
hosts: localhost
environment:
ENABLE_TURBO_SERVER: 1
ANSIBLE_TURBO_LOOKUP_TTL: 120
tasks:
...
```

### Clearing The Cache

You may find the need to clear the cache manually. This will make sure that all cached method return values are invalidated. You can do so with the `clear_cache` module:
```yaml
- name: Clear the cache
vmware.vmware.clear_cache: {}
```

### Killing the turbo server

You may want to kill the turbo server before its expriation time. This will clear the cache and also delete any cached module files. You can do so by terminating the process running on the remote host (the host that the vmware.vmware task was run on):
```bash
ps -ef | grep turbo | grep -v grep | awk '{print $2}' | xargs kill
```

## Development

To use the turbo server in your module, you need to replace the AnsibleModule import with the custom class from this repo.
```python
from ansible_collections.vmware.vmware.plugins.module_utils._vmware_ansible_module import (
AnsibleModule
)
```

You can leverage the cache from `functools` to save the results from a method. One use case would be caching an authentication session or the results from looking up VM information. To use the cache, import `functools` and then add the cache decorator to the method:
```python
import functools

@functools.cache
def my_method():
....
```

When attaching the cache decorator to methods, the argument inputs are hashed and compared to previous calls to determine if a cached result can be used. This means if your method uses a class, the class must have a reasonable hash and equals method defined. For example:
```python
class PyVmomi(object):
def __init__(self, module):
"""
Constructor
"""
self.module = module
self.params = module.params
....

def __eq__(self, value):
if not isinstance(value, self.__class__):
return False
return bool(all([
(self.params['hostname'] == value.params['hostname']),
(self.params['username'] == value.params['username'])
]))

def __hash__(self):
return hash(self.params['hostname'] + self.params['username'])
```

To clear the cache from within a module, you can call the builtin `clear_vmware_cache` method. If the user is not using the turbo server, the module does nothing so you can call this method safely without checking.
```python
def main():
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
....
module.clear_vmware_cache()
```
2 changes: 2 additions & 0 deletions galaxy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ build_ignore:
- changelogs/.plugin-cache.yaml
- .github
- .vscode
dependencies:
cloud.common: "*"
22 changes: 21 additions & 1 deletion plugins/module_utils/_vmware.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
HAS_PYVMOMI = False

from ansible.module_utils.basic import env_fallback, missing_required_lib
from ansible_collections.vmware.vmware.plugins.module_utils._vmware_ansible_module import (
cache,
)


class ApiAccessError(Exception):
Expand Down Expand Up @@ -80,6 +83,7 @@
)


@cache
def connect_to_api(module, disconnect_atexit=True, return_si=False, hostname=None, username=None, password=None,
port=None, validate_certs=None,
httpProxyHost=None, httpProxyPort=None):
Expand Down Expand Up @@ -203,11 +207,26 @@
self.module = module
self.params = module.params
self.current_vm_obj = None
self.si, self.content = connect_to_api(self.module, return_si=True)
self.si, self.content = self._connect_to_vcenter()

Check warning on line 210 in plugins/module_utils/_vmware.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware.py#L210

Added line #L210 was not covered by tests
self.custom_field_mgr = []
if self.content.customFieldsManager: # not an ESXi
self.custom_field_mgr = self.content.customFieldsManager.field

def __eq__(self, value):
if not isinstance(value, self.__class__):
return False
return bool(all([

Check warning on line 218 in plugins/module_utils/_vmware.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware.py#L217-L218

Added lines #L217 - L218 were not covered by tests
(self.params['hostname'] == value.params['hostname']),
(self.params['username'] == value.params['username'])
]))

def __hash__(self):
return hash(self.params['hostname'] + self.params['username'])

Check warning on line 224 in plugins/module_utils/_vmware.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware.py#L224

Added line #L224 was not covered by tests

@cache
def _connect_to_vcenter(self):
return connect_to_api(self.module, return_si=True)

Check warning on line 228 in plugins/module_utils/_vmware.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware.py#L228

Added line #L228 was not covered by tests

def is_vcenter(self):
"""
Check if given hostname is vCenter or ESXi host
Expand Down Expand Up @@ -282,6 +301,7 @@
"""
return self.get_objs_by_name_or_moid([vim.dvs.DistributedVirtualPortgroup], portgroup)

@cache
def get_vm_using_params(
self, name_param='name', uuid_param='uuid', moid_param='moid', fail_on_missing=False,
name_match_param='name_match', use_instance_uuid_param='use_instance_uuid'):
Expand Down
82 changes: 82 additions & 0 deletions plugins/module_utils/_vmware_ansible_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from __future__ import absolute_import, division, print_function

__metaclass__ = type


import os
import functools

from ansible.module_utils.common.validation import check_type_bool
from ansible.module_utils.common.text.converters import to_native


enable_turbo_mode = check_type_bool(os.environ.get("ENABLE_TURBO_MODE", False))

if enable_turbo_mode:
try:
from ansible_collections.cloud.common.plugins.module_utils.turbo.module import (

Check warning on line 17 in plugins/module_utils/_vmware_ansible_module.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_ansible_module.py#L16-L17

Added lines #L16 - L17 were not covered by tests
AnsibleTurboModule as BaseAnsibleModule,
)

BaseAnsibleModule.collection_name = "vmware.vmware"
except ImportError:
from ansible.module_utils.basic import AnsibleModule as BaseAnsibleModule

Check warning on line 23 in plugins/module_utils/_vmware_ansible_module.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_ansible_module.py#L21-L23

Added lines #L21 - L23 were not covered by tests
else:
from ansible.module_utils.basic import AnsibleModule as BaseAnsibleModule


CACHED_FUNCTION_REGISTRY = set()


class AnsibleModule(BaseAnsibleModule):
"""
The exit_json and __format_value_for_turbo_server should really be added to the upstream
cloud.common repo, but until then we need it here.
The outputs from a module need to be passed through the turbo server using pickle. If the output
contains something that pickle cannot encode/decode, we need to convert it first.
For most APIs that return content as JSON, this isn't an issue. But for the SDKs VMware uses,
it can be a problem.
"""
def exit_json(self, **kwargs):
if enable_turbo_mode:
kwargs = self.__format_value_for_turbo_server(kwargs)
super().exit_json(**kwargs)

Check warning on line 43 in plugins/module_utils/_vmware_ansible_module.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_ansible_module.py#L42-L43

Added lines #L42 - L43 were not covered by tests

def __format_value_for_turbo_server(self, value):
if isinstance(value, (str, bool, int)):
return value

Check warning on line 47 in plugins/module_utils/_vmware_ansible_module.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_ansible_module.py#L47

Added line #L47 was not covered by tests
if isinstance(value, set):
return self.__format_value_for_turbo_server(list(value))

Check warning on line 49 in plugins/module_utils/_vmware_ansible_module.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_ansible_module.py#L49

Added line #L49 was not covered by tests
if isinstance(value, list):
return [self.__format_value_for_turbo_server(v) for v in value]
if isinstance(value, dict):
for k, v in value.items():
value[k] = self.__format_value_for_turbo_server(v)
return value

Check warning on line 55 in plugins/module_utils/_vmware_ansible_module.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_ansible_module.py#L54-L55

Added lines #L54 - L55 were not covered by tests

return to_native(value)

Check warning on line 57 in plugins/module_utils/_vmware_ansible_module.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_ansible_module.py#L57

Added line #L57 was not covered by tests

def clear_vmware_cache(self, funcs=None):
cleared = set()
no_cache = set()

Check warning on line 61 in plugins/module_utils/_vmware_ansible_module.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_ansible_module.py#L60-L61

Added lines #L60 - L61 were not covered by tests
if not funcs:
funcs = CACHED_FUNCTION_REGISTRY

Check warning on line 63 in plugins/module_utils/_vmware_ansible_module.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_ansible_module.py#L63

Added line #L63 was not covered by tests

for f in funcs:
try:
f.cache_clear()
cleared.add(f.__name__)
except AttributeError:
no_cache.add(f.__name__)

Check warning on line 70 in plugins/module_utils/_vmware_ansible_module.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_ansible_module.py#L66-L70

Added lines #L66 - L70 were not covered by tests

return cleared, no_cache

Check warning on line 72 in plugins/module_utils/_vmware_ansible_module.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_ansible_module.py#L72

Added line #L72 was not covered by tests


def cache(func):
@functools.wraps(func)
@functools.lru_cache(maxsize=128)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)

Check warning on line 79 in plugins/module_utils/_vmware_ansible_module.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_ansible_module.py#L79

Added line #L79 was not covered by tests

CACHED_FUNCTION_REGISTRY.add(wrapper)
return wrapper
28 changes: 21 additions & 7 deletions plugins/module_utils/_vmware_facts.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,29 @@
except ImportError:
pass

from ansible.module_utils._text import to_text
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.six import integer_types, string_types, iteritems
import ansible.module_utils.common._collections_compat as collections_compat
import ansible.module_utils.six.moves.collections_abc as collections_compat
from ansible_collections.vmware.vmware.plugins.module_utils._vmware_folder_paths import get_folder_path_of_vm
from ansible_collections.vmware.vmware.plugins.module_utils._vmware_ansible_module import (
cache,
)


class VmFacts():
def __init__(self, vm):
self.vm = vm

def __eq__(self, value):
if not isinstance(value, self.__class__):
return False
return bool(all([

Check warning on line 38 in plugins/module_utils/_vmware_facts.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_facts.py#L37-L38

Added lines #L37 - L38 were not covered by tests
(self.vm._GetMoId() == value.vm._GetMoId())
]))

def __hash__(self):
return hash(self.vm._GetMoId())

Check warning on line 43 in plugins/module_utils/_vmware_facts.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_facts.py#L43

Added line #L43 was not covered by tests

def hw_all_facts(self):
'''
Returns a combined set of all 'hw_' facts
Expand All @@ -42,6 +55,7 @@
**self.hw_network_device_facts()
}

@cache
def all_facts(self, content):
return {
**self.hw_all_facts(),
Expand Down Expand Up @@ -355,7 +369,7 @@
elif isinstance(xo, vim.vm.device.VirtualDisk):
data[x] = serialize_spec(xo)
elif isinstance(xo, vim.vm.device.VirtualDeviceSpec.FileOperation):
data[x] = to_text(xo)
data[x] = to_native(xo)

Check warning on line 372 in plugins/module_utils/_vmware_facts.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_facts.py#L372

Added line #L372 was not covered by tests
elif isinstance(xo, vim.Description):
data[x] = {
'dynamicProperty': serialize_spec(xo.dynamicProperty),
Expand All @@ -364,7 +378,7 @@
'summary': serialize_spec(xo.summary),
}
elif hasattr(xo, 'name'):
data[x] = to_text(xo) + ':' + to_text(xo.name)
data[x] = to_native(xo) + ':' + to_native(xo.name)

Check warning on line 381 in plugins/module_utils/_vmware_facts.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_facts.py#L381

Added line #L381 was not covered by tests
elif isinstance(xo, vim.vm.ProfileSpec):
pass
elif issubclass(xt, list):
Expand All @@ -375,13 +389,13 @@
if issubclass(xt, integer_types):
data[x] = int(xo)
else:
data[x] = to_text(xo)
data[x] = to_native(xo)

Check warning on line 392 in plugins/module_utils/_vmware_facts.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_facts.py#L392

Added line #L392 was not covered by tests
elif issubclass(xt, bool):
data[x] = xo
elif issubclass(xt, dict):
data[to_text(x)] = {}
data[to_native(x)] = {}

Check warning on line 396 in plugins/module_utils/_vmware_facts.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_facts.py#L396

Added line #L396 was not covered by tests
for k, v in xo.items():
k = to_text(k)
k = to_native(k)

Check warning on line 398 in plugins/module_utils/_vmware_facts.py

View check run for this annotation

Codecov / codecov/patch

plugins/module_utils/_vmware_facts.py#L398

Added line #L398 was not covered by tests
data[x][k] = serialize_spec(v)
else:
data[x] = str(xt)
Expand Down
Loading
Loading