mirror of
				https://github.com/ansible-collections/community.general.git
				synced 2025-10-25 05:23:58 -07:00 
			
		
		
		
	Adds bigip_vcmp_guest module (#33024)
This module can be used to manage guests on a vCMP provisioned BIG-IP. vCMP is a hardware-only feature, therefore this module cannot be used on the VE editions of BIG-IP.
This commit is contained in:
		
					parent
					
						
							
								67d5e1d3e7
							
						
					
				
			
			
				commit
				
					
						c94d57311c
					
				
			
		
					 2 changed files with 899 additions and 0 deletions
				
			
		
							
								
								
									
										714
									
								
								lib/ansible/modules/network/f5/bigip_vcmp_guest.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										714
									
								
								lib/ansible/modules/network/f5/bigip_vcmp_guest.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,714 @@ | ||||||
|  | #!/usr/bin/python | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # | ||||||
|  | # Copyright (c) 2017 F5 Networks Inc. | ||||||
|  | # GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) | ||||||
|  | 
 | ||||||
|  | from __future__ import absolute_import, division, print_function | ||||||
|  | __metaclass__ = type | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ANSIBLE_METADATA = {'metadata_version': '1.1', | ||||||
|  |                     'status': ['preview'], | ||||||
|  |                     'supported_by': 'community'} | ||||||
|  | 
 | ||||||
|  | DOCUMENTATION = r''' | ||||||
|  | --- | ||||||
|  | module: bigip_vcmp_guest | ||||||
|  | short_description: Manages vCMP guests on a BIG-IP | ||||||
|  | description: | ||||||
|  |   - Manages vCMP guests on a BIG-IP. This functionality only exists on | ||||||
|  |     actual hardware and must be enabled by provisioning C(vcmp) with the | ||||||
|  |     C(bigip_provision) module. | ||||||
|  | version_added: "2.5" | ||||||
|  | options: | ||||||
|  |   name: | ||||||
|  |     description: | ||||||
|  |       - The name of the vCMP guest to manage. | ||||||
|  |     required: True | ||||||
|  |   vlans: | ||||||
|  |     description: | ||||||
|  |       - VLANs that the guest uses to communicate with other guests, the host, and with | ||||||
|  |         the external network. The available VLANs in the list are those that are | ||||||
|  |         currently configured on the vCMP host. | ||||||
|  |       - The order of these VLANs is not important; in fact, it's ignored. This module will | ||||||
|  |         order the VLANs for you automatically. Therefore, if you deliberately re-order them | ||||||
|  |         in subsequent tasks, you will find that this module will B(not) register a change. | ||||||
|  |   initial_image: | ||||||
|  |     description: | ||||||
|  |       - Specifies the base software release ISO image file for installing the TMOS | ||||||
|  |         hypervisor instance and any licensed BIG-IP modules onto the guest's virtual | ||||||
|  |         disk. When creating a new guest, this parameter is required. | ||||||
|  |   mgmt_network: | ||||||
|  |     description: | ||||||
|  |       - Specifies the method by which the management address is used in the vCMP guest. | ||||||
|  |       - When C(bridged), specifies that the guest can communicate with the vCMP host's | ||||||
|  |         management network. | ||||||
|  |       - When C(isolated), specifies that the guest is isolated from the vCMP host's | ||||||
|  |         management network. In this case, the only way that a guest can communicate | ||||||
|  |         with the vCMP host is through the console port or through a self IP address | ||||||
|  |         on the guest that allows traffic through port 22. | ||||||
|  |       - When C(host only), prevents the guest from installing images and hotfixes other | ||||||
|  |         than those provided by the hypervisor. | ||||||
|  |       - If the guest setting is C(isolated) or C(host only), the C(mgmt_address) does | ||||||
|  |         not apply. | ||||||
|  |       - Concerning mode changing, changing C(bridged) to C(isolated) causes the vCMP | ||||||
|  |         host to remove all of the guest's management interfaces from its bridged | ||||||
|  |         management network. This immediately disconnects the guest's VMs from the | ||||||
|  |         physical management network. Changing C(isolated) to C(bridged) causes the | ||||||
|  |         vCMP host to dynamically add the guest's management interfaces to the bridged | ||||||
|  |         management network. This immediately connects all of the guest's VMs to the | ||||||
|  |         physical management network. Changing this property while the guest is in the | ||||||
|  |         C(configured) or C(provisioned) state has no immediate effect. | ||||||
|  |     choices: | ||||||
|  |       - bridged | ||||||
|  |       - isolated | ||||||
|  |       - host only | ||||||
|  |   delete_virtual_disk: | ||||||
|  |     description: | ||||||
|  |       - When C(state) is C(absent), will additionally delete the virtual disk associated | ||||||
|  |         with the vCMP guest. By default, this value is C(no). | ||||||
|  |     default: no | ||||||
|  |   mgmt_address: | ||||||
|  |     description: | ||||||
|  |       - Specifies the IP address, and subnet or subnet mask that you use to access | ||||||
|  |         the guest when you want to manage a module running within the guest. This | ||||||
|  |         parameter is required if the C(mgmt_network) parameter is C(bridged). | ||||||
|  |       - When creating a new guest, if you do not specify a network or network mask, | ||||||
|  |         a default of C(/24) (C(255.255.255.0)) will be assumed. | ||||||
|  |   mgmt_route: | ||||||
|  |     description: | ||||||
|  |       - Specifies the gateway address for the C(mgmt_address). | ||||||
|  |       - If this value is not specified when creating a new guest, it is set to C(none). | ||||||
|  |       - The value C(none) can be used during an update to remove this value. | ||||||
|  |   state: | ||||||
|  |     description: | ||||||
|  |       - The state of the vCMP guest on the system. Each state implies the actions of | ||||||
|  |         all states before it. | ||||||
|  |       - When C(configured), guarantees that the vCMP guest exists with the provided | ||||||
|  |         attributes. Additionally, ensures that the vCMP guest is turned off. | ||||||
|  |       - When C(disabled), behaves the same as C(configured) the name of this state | ||||||
|  |         is just a convenience for the user that is more understandable. | ||||||
|  |       - When C(provisioned), will ensure that the guest is created and installed. | ||||||
|  |         This state will not start the guest; use C(deployed) for that. This state | ||||||
|  |         is one step beyond C(present) as C(present) will not install the guest; | ||||||
|  |         only setup the configuration for it to be installed. | ||||||
|  |       - When C(present), ensures the guest is properly provisioned and starts | ||||||
|  |         the guest so that it is in a running state. | ||||||
|  |       - When C(absent), removes the vCMP from the system. | ||||||
|  |     default: "present" | ||||||
|  |     choices: | ||||||
|  |       - configured | ||||||
|  |       - disabled | ||||||
|  |       - provisioned | ||||||
|  |       - present | ||||||
|  |       - absent | ||||||
|  |   cores_per_slot: | ||||||
|  |     description: | ||||||
|  |       - Specifies the number of cores that the system allocates to the guest. | ||||||
|  |       - Each core represents a portion of CPU and memory. Therefore, the amount of | ||||||
|  |         memory allocated per core is directly tied to the amount of CPU. This amount | ||||||
|  |         of memory varies per hardware platform type. | ||||||
|  |       - The number you can specify depends on the type of hardware you have. | ||||||
|  |       - In the event of a reboot, the system persists the guest to the same slot on | ||||||
|  |         which it ran prior to the reboot. | ||||||
|  | notes: | ||||||
|  |   - Requires the f5-sdk Python package on the host. This is as easy as pip | ||||||
|  |     install f5-sdk. | ||||||
|  |   - This module can take a lot of time to deploy vCMP guests. This is an intrinsic | ||||||
|  |     limitation of the vCMP system because it is booting real VMs on the BIG-IP | ||||||
|  |     device. This boot time is very similar in length to the time it takes to | ||||||
|  |     boot VMs on any other virtualization platform; public or private. | ||||||
|  |   - When BIG-IP starts, the VMs are booted sequentially; not in parallel. This | ||||||
|  |     means that it is not unusual for a vCMP host with many guests to take a | ||||||
|  |     long time (60+ minutes) to reboot and bring all the guests online. The | ||||||
|  |     BIG-IP chassis will be available before all vCMP guests are online. | ||||||
|  | requirements: | ||||||
|  |   - f5-sdk >= 3.0.3 | ||||||
|  |   - netaddr | ||||||
|  | extends_documentation_fragment: f5 | ||||||
|  | author: | ||||||
|  |   - Tim Rupp (@caphrim007) | ||||||
|  | ''' | ||||||
|  | 
 | ||||||
|  | EXAMPLES = r''' | ||||||
|  | - name: Create a vCMP guest | ||||||
|  |   bigip_vcmp_guest: | ||||||
|  |     name: foo | ||||||
|  |     password: secret | ||||||
|  |     server: lb.mydomain.com | ||||||
|  |     state: present | ||||||
|  |     user: admin | ||||||
|  |     mgmt_network: bridge | ||||||
|  |     mgmt_address: 10.20.30.40/24 | ||||||
|  |   delegate_to: localhost | ||||||
|  | 
 | ||||||
|  | - name: Create a vCMP guest with specific VLANs | ||||||
|  |   bigip_vcmp_guest: | ||||||
|  |     name: foo | ||||||
|  |     password: secret | ||||||
|  |     server: lb.mydomain.com | ||||||
|  |     state: present | ||||||
|  |     user: admin | ||||||
|  |     mgmt_network: bridge | ||||||
|  |     mgmt_address: 10.20.30.40/24 | ||||||
|  |     vlans: | ||||||
|  |       - vlan1 | ||||||
|  |       - vlan2 | ||||||
|  |   delegate_to: localhost | ||||||
|  | 
 | ||||||
|  | - name: Remove vCMP guest and disk | ||||||
|  |   bigip_vcmp_guest: | ||||||
|  |     name: guest1 | ||||||
|  |     state: absent | ||||||
|  |     delete_virtual_disk: yes | ||||||
|  |   register: result | ||||||
|  | ''' | ||||||
|  | 
 | ||||||
|  | RETURN = r''' | ||||||
|  | vlans: | ||||||
|  |   description: The VLANs assigned to the vCMP guest, in their full path format. | ||||||
|  |   returned: changed | ||||||
|  |   type: list | ||||||
|  |   sample: ['/Common/vlan1', '/Common/vlan2'] | ||||||
|  | ''' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | from ansible.module_utils.f5_utils import AnsibleF5Client | ||||||
|  | from ansible.module_utils.f5_utils import AnsibleF5Parameters | ||||||
|  | from ansible.module_utils.f5_utils import HAS_F5SDK | ||||||
|  | from ansible.module_utils.f5_utils import F5ModuleError | ||||||
|  | from ansible.module_utils.six import iteritems | ||||||
|  | from collections import defaultdict | ||||||
|  | from collections import namedtuple | ||||||
|  | 
 | ||||||
|  | import time | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     from netaddr import IPAddress, AddrFormatError, IPNetwork | ||||||
|  |     HAS_NETADDR = True | ||||||
|  | except ImportError: | ||||||
|  |     HAS_NETADDR = False | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     from f5.utils.responses.handlers import Stats | ||||||
|  |     from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError | ||||||
|  | except ImportError: | ||||||
|  |     HAS_F5SDK = False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Parameters(AnsibleF5Parameters): | ||||||
|  |     api_map = { | ||||||
|  |         'managementGw': 'mgmt_route', | ||||||
|  |         'managementNetwork': 'mgmt_network', | ||||||
|  |         'managementIp': 'mgmt_address', | ||||||
|  |         'initialImage': 'initial_image', | ||||||
|  |         'virtualDisk': 'virtual_disk', | ||||||
|  |         'coresPerSlot': 'cores_per_slot' | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     api_attributes = [ | ||||||
|  |         'vlans', 'managementNetwork', 'managementIp', 'initialImage', 'managementGw', | ||||||
|  |         'state' | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     returnables = [ | ||||||
|  |         'vlans', 'mgmt_network', 'mgmt_address', 'initial_image', 'mgmt_route', | ||||||
|  |         'name' | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     updatables = [ | ||||||
|  |         'vlans', 'mgmt_network', 'mgmt_address', 'initial_image', 'mgmt_route', | ||||||
|  |         'state' | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     def __init__(self, params=None, client=None): | ||||||
|  |         self._values = defaultdict(lambda: None) | ||||||
|  |         self._values['__warnings'] = [] | ||||||
|  |         if params: | ||||||
|  |             self.update(params=params) | ||||||
|  |         self.client = client | ||||||
|  | 
 | ||||||
|  |     def update(self, params=None): | ||||||
|  |         if params: | ||||||
|  |             for k, v in iteritems(params): | ||||||
|  |                 if self.api_map is not None and k in self.api_map: | ||||||
|  |                     map_key = self.api_map[k] | ||||||
|  |                 else: | ||||||
|  |                     map_key = k | ||||||
|  | 
 | ||||||
|  |                 # Handle weird API parameters like `dns.proxy.__iter__` by | ||||||
|  |                 # using a map provided by the module developer | ||||||
|  |                 class_attr = getattr(type(self), map_key, None) | ||||||
|  |                 if isinstance(class_attr, property): | ||||||
|  |                     # There is a mapped value for the api_map key | ||||||
|  |                     if class_attr.fset is None: | ||||||
|  |                         # If the mapped value does not have | ||||||
|  |                         # an associated setter | ||||||
|  |                         self._values[map_key] = v | ||||||
|  |                     else: | ||||||
|  |                         # The mapped value has a setter | ||||||
|  |                         setattr(self, map_key, v) | ||||||
|  |                 else: | ||||||
|  |                     # If the mapped value is not a @property | ||||||
|  |                     self._values[map_key] = v | ||||||
|  | 
 | ||||||
|  |     def _fqdn_name(self, value): | ||||||
|  |         if value is not None and not value.startswith('/'): | ||||||
|  |             return '/{0}/{1}'.format(self.partition, value) | ||||||
|  |         return value | ||||||
|  | 
 | ||||||
|  |     def to_return(self): | ||||||
|  |         result = {} | ||||||
|  |         try: | ||||||
|  |             for returnable in self.returnables: | ||||||
|  |                 result[returnable] = getattr(self, returnable) | ||||||
|  |             result = self._filter_params(result) | ||||||
|  |         except Exception: | ||||||
|  |             pass | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     def api_params(self): | ||||||
|  |         result = {} | ||||||
|  |         for api_attribute in self.api_attributes: | ||||||
|  |             if self.api_map is not None and api_attribute in self.api_map: | ||||||
|  |                 result[api_attribute] = getattr(self, self.api_map[api_attribute]) | ||||||
|  |             else: | ||||||
|  |                 result[api_attribute] = getattr(self, api_attribute) | ||||||
|  |         result = self._filter_params(result) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def mgmt_route(self): | ||||||
|  |         if self._values['mgmt_route'] is None: | ||||||
|  |             return None | ||||||
|  |         elif self._values['mgmt_route'] == 'none': | ||||||
|  |             return 'none' | ||||||
|  |         try: | ||||||
|  |             result = IPAddress(self._values['mgmt_route']) | ||||||
|  |             return str(result) | ||||||
|  |         except AddrFormatError: | ||||||
|  |             raise F5ModuleError( | ||||||
|  |                 "The specified 'mgmt_route' is not a valid IP address" | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def mgmt_address(self): | ||||||
|  |         if self._values['mgmt_address'] is None: | ||||||
|  |             return None | ||||||
|  |         try: | ||||||
|  |             addr = IPNetwork(self._values['mgmt_address']) | ||||||
|  |             result = '{0}/{1}'.format(addr.ip, addr.prefixlen) | ||||||
|  |             return result | ||||||
|  |         except AddrFormatError: | ||||||
|  |             raise F5ModuleError( | ||||||
|  |                 "The specified 'mgmt_address' is not a valid IP address" | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def mgmt_tuple(self): | ||||||
|  |         result = None | ||||||
|  |         Destination = namedtuple('Destination', ['ip', 'subnet']) | ||||||
|  |         try: | ||||||
|  |             parts = self._values['mgmt_address'].split('/') | ||||||
|  |             if len(parts) == 2: | ||||||
|  |                 result = Destination(ip=parts[0], subnet=parts[1]) | ||||||
|  |             elif len(parts) < 2: | ||||||
|  |                 result = Destination(ip=parts[0], subnet=None) | ||||||
|  |             else: | ||||||
|  |                 F5ModuleError( | ||||||
|  |                     "The provided mgmt_address is malformed." | ||||||
|  |                 ) | ||||||
|  |         except ValueError: | ||||||
|  |             result = Destination(ip=None, subnet=None) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def state(self): | ||||||
|  |         if self._values['state'] == 'present': | ||||||
|  |             return 'deployed' | ||||||
|  |         elif self._values['state'] in ['configured', 'disabled']: | ||||||
|  |             return 'configured' | ||||||
|  |         return self._values['state'] | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def vlans(self): | ||||||
|  |         if self._values['vlans'] is None: | ||||||
|  |             return None | ||||||
|  |         result = [self._fqdn_name(x) for x in self._values['vlans']] | ||||||
|  |         result.sort() | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def initial_image(self): | ||||||
|  |         if self._values['initial_image'] is None: | ||||||
|  |             return None | ||||||
|  |         if self.initial_image_exists(self._values['initial_image']): | ||||||
|  |             return self._values['initial_image'] | ||||||
|  |         raise F5ModuleError( | ||||||
|  |             "The specified 'initial_image' does not exist on the remote device" | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def initial_image_exists(self, image): | ||||||
|  |         collection = self.client.api.tm.sys.software.images.get_collection() | ||||||
|  |         for resource in collection: | ||||||
|  |             if resource.name.startswith(image): | ||||||
|  |                 return True | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Changes(Parameters): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Difference(object): | ||||||
|  |     def __init__(self, want, have=None): | ||||||
|  |         self.want = want | ||||||
|  |         self.have = have | ||||||
|  | 
 | ||||||
|  |     def compare(self, param): | ||||||
|  |         try: | ||||||
|  |             result = getattr(self, param) | ||||||
|  |             return result | ||||||
|  |         except AttributeError: | ||||||
|  |             return self.__default(param) | ||||||
|  | 
 | ||||||
|  |     def __default(self, param): | ||||||
|  |         attr1 = getattr(self.want, param) | ||||||
|  |         try: | ||||||
|  |             attr2 = getattr(self.have, param) | ||||||
|  |             if attr1 != attr2: | ||||||
|  |                 return attr1 | ||||||
|  |         except AttributeError: | ||||||
|  |             return attr1 | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def mgmt_address(self): | ||||||
|  |         want = self.want.mgmt_tuple | ||||||
|  |         if want.subnet is None: | ||||||
|  |             raise F5ModuleError( | ||||||
|  |                 "A subnet must be specified when changing the mgmt_address" | ||||||
|  |             ) | ||||||
|  |         if self.want.mgmt_address != self.have.mgmt_address: | ||||||
|  |             return self.want.mgmt_address | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ModuleManager(object): | ||||||
|  |     def __init__(self, client): | ||||||
|  |         self.client = client | ||||||
|  |         self.want = Parameters(client=client, params=self.client.module.params) | ||||||
|  |         self.changes = Changes() | ||||||
|  | 
 | ||||||
|  |     def _set_changed_options(self): | ||||||
|  |         changed = {} | ||||||
|  |         for key in Parameters.returnables: | ||||||
|  |             if getattr(self.want, key) is not None: | ||||||
|  |                 changed[key] = getattr(self.want, key) | ||||||
|  |         if changed: | ||||||
|  |             self.changes = Changes(changed) | ||||||
|  | 
 | ||||||
|  |     def _update_changed_options(self): | ||||||
|  |         diff = Difference(self.want, self.have) | ||||||
|  |         updatables = Parameters.updatables | ||||||
|  |         changed = dict() | ||||||
|  |         for k in updatables: | ||||||
|  |             change = diff.compare(k) | ||||||
|  |             if change is None: | ||||||
|  |                 continue | ||||||
|  |             else: | ||||||
|  |                 changed[k] = change | ||||||
|  |         if changed: | ||||||
|  |             self.changes = Parameters(changed) | ||||||
|  |             return True | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def should_update(self): | ||||||
|  |         result = self._update_changed_options() | ||||||
|  |         if result: | ||||||
|  |             return True | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def exec_module(self): | ||||||
|  |         changed = False | ||||||
|  |         result = dict() | ||||||
|  |         state = self.want.state | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             if state in ['configured', 'provisioned', 'deployed']: | ||||||
|  |                 changed = self.present() | ||||||
|  |             elif state == "absent": | ||||||
|  |                 changed = self.absent() | ||||||
|  |         except iControlUnexpectedHTTPError as e: | ||||||
|  |             raise F5ModuleError(str(e)) | ||||||
|  | 
 | ||||||
|  |         changes = self.changes.to_return() | ||||||
|  |         result.update(**changes) | ||||||
|  |         result.update(dict(changed=changed)) | ||||||
|  |         self._announce_deprecations(result) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     def _announce_deprecations(self, result): | ||||||
|  |         warnings = result.pop('__warnings', []) | ||||||
|  |         for warning in warnings: | ||||||
|  |             self.client.module.deprecate( | ||||||
|  |                 msg=warning['msg'], | ||||||
|  |                 version=warning['version'] | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |     def _fqdn_name(self, value): | ||||||
|  |         if value is not None and not value.startswith('/'): | ||||||
|  |             return '/{0}/{1}'.format(self.partition, value) | ||||||
|  |         return value | ||||||
|  | 
 | ||||||
|  |     def present(self): | ||||||
|  |         if self.exists(): | ||||||
|  |             return self.update() | ||||||
|  |         else: | ||||||
|  |             return self.create() | ||||||
|  | 
 | ||||||
|  |     def exists(self): | ||||||
|  |         result = self.client.api.tm.vcmp.guests.guest.exists( | ||||||
|  |             name=self.want.name | ||||||
|  |         ) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     def update(self): | ||||||
|  |         self.have = self.read_current_from_device() | ||||||
|  |         if not self.should_update(): | ||||||
|  |             return False | ||||||
|  |         if self.client.check_mode: | ||||||
|  |             return True | ||||||
|  |         self.update_on_device() | ||||||
|  |         if self.want.state == 'provisioned': | ||||||
|  |             self.provision() | ||||||
|  |         elif self.want.state == 'deployed': | ||||||
|  |             self.deploy() | ||||||
|  |         elif self.want.state == 'configured': | ||||||
|  |             self.configure() | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|  |     def remove(self): | ||||||
|  |         if self.client.check_mode: | ||||||
|  |             return True | ||||||
|  |         if self.want.delete_virtual_disk: | ||||||
|  |             self.have = self.read_current_from_device() | ||||||
|  |         self.remove_from_device() | ||||||
|  |         if self.exists(): | ||||||
|  |             raise F5ModuleError("Failed to delete the resource.") | ||||||
|  |         if self.want.delete_virtual_disk: | ||||||
|  |             self.remove_virtual_disk() | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|  |     def create(self): | ||||||
|  |         self._set_changed_options() | ||||||
|  |         if self.client.check_mode: | ||||||
|  |             return True | ||||||
|  |         if self.want.mgmt_tuple.subnet is None: | ||||||
|  |             self.want.update(dict( | ||||||
|  |                 mgmt_address='{0}/255.255.255.0'.format(self.want.mgmt_tuple.ip) | ||||||
|  |             )) | ||||||
|  |         self.create_on_device() | ||||||
|  |         if self.want.state == 'provisioned': | ||||||
|  |             self.provision() | ||||||
|  |         elif self.want.state == 'deployed': | ||||||
|  |             self.deploy() | ||||||
|  |         elif self.want.state == 'configured': | ||||||
|  |             self.configure() | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|  |     def create_on_device(self): | ||||||
|  |         params = self.want.api_params() | ||||||
|  |         self.client.api.tm.vcmp.guests.guest.create( | ||||||
|  |             name=self.want.name, | ||||||
|  |             **params | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def update_on_device(self): | ||||||
|  |         params = self.changes.api_params() | ||||||
|  |         resource = self.client.api.tm.vcmp.guests.guest.load( | ||||||
|  |             name=self.want.name | ||||||
|  |         ) | ||||||
|  |         resource.modify(**params) | ||||||
|  | 
 | ||||||
|  |     def absent(self): | ||||||
|  |         if self.exists(): | ||||||
|  |             return self.remove() | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def remove_from_device(self): | ||||||
|  |         resource = self.client.api.tm.vcmp.guests.guest.load( | ||||||
|  |             name=self.want.name | ||||||
|  |         ) | ||||||
|  |         if resource: | ||||||
|  |             resource.delete() | ||||||
|  | 
 | ||||||
|  |     def read_current_from_device(self): | ||||||
|  |         resource = self.client.api.tm.vcmp.guests.guest.load( | ||||||
|  |             name=self.want.name | ||||||
|  |         ) | ||||||
|  |         result = resource.attrs | ||||||
|  |         return Parameters(result) | ||||||
|  | 
 | ||||||
|  |     def remove_virtual_disk(self): | ||||||
|  |         if self.virtual_disk_exists(): | ||||||
|  |             return self.remove_virtual_disk_from_device() | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def virtual_disk_exists(self): | ||||||
|  |         collection = self.client.api.tm.vcmp.virtual_disks.get_collection() | ||||||
|  |         for resource in collection: | ||||||
|  |             check = '{0}/'.format(self.have.virtual_disk) | ||||||
|  |             if resource.name.startswith(check): | ||||||
|  |                 return True | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def remove_virtual_disk_from_device(self): | ||||||
|  |         collection = self.client.api.tm.vcmp.virtual_disks.get_collection() | ||||||
|  |         for resource in collection: | ||||||
|  |             check = '{0}/'.format(self.have.virtual_disk) | ||||||
|  |             if resource.name.startswith(check): | ||||||
|  |                 resource.delete() | ||||||
|  |                 return True | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def is_configured(self): | ||||||
|  |         """Checks to see if guest is disabled | ||||||
|  | 
 | ||||||
|  |         A disabled guest is fully disabled once their Stats go offline. | ||||||
|  |         Until that point they are still in the process of disabling. | ||||||
|  | 
 | ||||||
|  |         :return: | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             res = self.client.api.tm.vcmp.guests.guest.load(name=self.want.name) | ||||||
|  |             Stats(res.stats.load()) | ||||||
|  |             return False | ||||||
|  |         except iControlUnexpectedHTTPError as ex: | ||||||
|  |             if 'Object not found - ' in str(ex): | ||||||
|  |                 return True | ||||||
|  |             raise | ||||||
|  | 
 | ||||||
|  |     def is_provisioned(self): | ||||||
|  |         try: | ||||||
|  |             res = self.client.api.tm.vcmp.guests.guest.load(name=self.want.name) | ||||||
|  |             stats = Stats(res.stats.load()) | ||||||
|  |             if stats.stat['requestedState']['description'] == 'provisioned': | ||||||
|  |                 if stats.stat['vmStatus']['description'] == 'stopped': | ||||||
|  |                     return True | ||||||
|  |         except iControlUnexpectedHTTPError: | ||||||
|  |             pass | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def is_deployed(self): | ||||||
|  |         try: | ||||||
|  |             res = self.client.api.tm.vcmp.guests.guest.load(name=self.want.name) | ||||||
|  |             stats = Stats(res.stats.load()) | ||||||
|  |             if stats.stat['requestedState']['description'] == 'deployed': | ||||||
|  |                 if stats.stat['vmStatus']['description'] == 'running': | ||||||
|  |                     return True | ||||||
|  |         except iControlUnexpectedHTTPError: | ||||||
|  |             pass | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def configure(self): | ||||||
|  |         if self.is_configured(): | ||||||
|  |             return False | ||||||
|  |         self.configure_on_device() | ||||||
|  |         self.wait_for_configured() | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|  |     def configure_on_device(self): | ||||||
|  |         resource = self.client.api.tm.vcmp.guests.guest.load(name=self.want.name) | ||||||
|  |         resource.modify(state='configured') | ||||||
|  | 
 | ||||||
|  |     def wait_for_configured(self): | ||||||
|  |         nops = 0 | ||||||
|  |         while nops < 3: | ||||||
|  |             if self.is_configured(): | ||||||
|  |                 nops += 1 | ||||||
|  |             time.sleep(1) | ||||||
|  | 
 | ||||||
|  |     def provision(self): | ||||||
|  |         if self.is_provisioned(): | ||||||
|  |             return False | ||||||
|  |         self.provision_on_device() | ||||||
|  |         self.wait_for_provisioned() | ||||||
|  | 
 | ||||||
|  |     def provision_on_device(self): | ||||||
|  |         resource = self.client.api.tm.vcmp.guests.guest.load(name=self.want.name) | ||||||
|  |         resource.modify(state='provisioned') | ||||||
|  | 
 | ||||||
|  |     def wait_for_provisioned(self): | ||||||
|  |         nops = 0 | ||||||
|  |         while nops < 3: | ||||||
|  |             if self.is_provisioned(): | ||||||
|  |                 nops += 1 | ||||||
|  |             time.sleep(1) | ||||||
|  | 
 | ||||||
|  |     def deploy(self): | ||||||
|  |         if self.is_deployed(): | ||||||
|  |             return False | ||||||
|  |         self.deploy_on_device() | ||||||
|  |         self.wait_for_deployed() | ||||||
|  | 
 | ||||||
|  |     def deploy_on_device(self): | ||||||
|  |         resource = self.client.api.tm.vcmp.guests.guest.load(name=self.want.name) | ||||||
|  |         resource.modify(state='deployed') | ||||||
|  | 
 | ||||||
|  |     def wait_for_deployed(self): | ||||||
|  |         nops = 0 | ||||||
|  |         while nops < 3: | ||||||
|  |             if self.is_deployed(): | ||||||
|  |                 nops += 1 | ||||||
|  |             time.sleep(1) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ArgumentSpec(object): | ||||||
|  |     def __init__(self): | ||||||
|  |         self.supports_check_mode = True | ||||||
|  |         self.argument_spec = dict( | ||||||
|  |             name=dict(required=True), | ||||||
|  |             vlans=dict(type='list'), | ||||||
|  |             mgmt_network=dict(choices=['bridged', 'isolated', 'host only']), | ||||||
|  |             mgmt_address=dict(), | ||||||
|  |             mgmt_route=dict(), | ||||||
|  |             initial_image=dict(), | ||||||
|  |             state=dict( | ||||||
|  |                 default='present', | ||||||
|  |                 choices=['configured', 'disabled', 'provisioned', 'absent', 'present'] | ||||||
|  |             ), | ||||||
|  |             delete_virtual_disk=dict( | ||||||
|  |                 type='bool', default='no' | ||||||
|  |             ), | ||||||
|  |             cores_per_slot=dict(type='int') | ||||||
|  |         ) | ||||||
|  |         self.f5_product_name = 'bigip' | ||||||
|  |         self.required_if = [ | ||||||
|  |             ['mgmt_network', 'bridged', ['mgmt_address']] | ||||||
|  |         ] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def main(): | ||||||
|  |     if not HAS_F5SDK: | ||||||
|  |         raise F5ModuleError("The python f5-sdk module is required") | ||||||
|  | 
 | ||||||
|  |     if not HAS_NETADDR: | ||||||
|  |         raise F5ModuleError("The python netaddr module is required") | ||||||
|  | 
 | ||||||
|  |     spec = ArgumentSpec() | ||||||
|  | 
 | ||||||
|  |     client = AnsibleF5Client( | ||||||
|  |         argument_spec=spec.argument_spec, | ||||||
|  |         supports_check_mode=spec.supports_check_mode, | ||||||
|  |         f5_product_name=spec.f5_product_name | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         mm = ModuleManager(client) | ||||||
|  |         results = mm.exec_module() | ||||||
|  |         client.module.exit_json(**results) | ||||||
|  |     except F5ModuleError as e: | ||||||
|  |         client.module.fail_json(msg=str(e)) | ||||||
|  | 
 | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     main() | ||||||
							
								
								
									
										185
									
								
								test/units/modules/network/f5/test_bigip_vcmp_guest.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								test/units/modules/network/f5/test_bigip_vcmp_guest.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,185 @@ | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # | ||||||
|  | # Copyright (c) 2017 F5 Networks Inc. | ||||||
|  | # GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) | ||||||
|  | 
 | ||||||
|  | from __future__ import (absolute_import, division, print_function) | ||||||
|  | __metaclass__ = type | ||||||
|  | 
 | ||||||
|  | import os | ||||||
|  | import json | ||||||
|  | import pytest | ||||||
|  | import sys | ||||||
|  | 
 | ||||||
|  | from nose.plugins.skip import SkipTest | ||||||
|  | if sys.version_info < (2, 7): | ||||||
|  |     raise SkipTest("F5 Ansible modules require Python >= 2.7") | ||||||
|  | 
 | ||||||
|  | from ansible.compat.tests import unittest | ||||||
|  | from ansible.compat.tests.mock import patch, Mock | ||||||
|  | from ansible.module_utils import basic | ||||||
|  | from ansible.module_utils._text import to_bytes | ||||||
|  | from ansible.module_utils.f5_utils import AnsibleF5Client | ||||||
|  | from ansible.module_utils.f5_utils import F5ModuleError | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     from library.bigip_vcmp_guest import Parameters | ||||||
|  |     from library.bigip_vcmp_guest import ModuleManager | ||||||
|  |     from library.bigip_vcmp_guest import ArgumentSpec | ||||||
|  |     from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError | ||||||
|  | except ImportError: | ||||||
|  |     try: | ||||||
|  |         from ansible.modules.network.f5.bigip_vcmp_guest import Parameters | ||||||
|  |         from ansible.modules.network.f5.bigip_vcmp_guest import ModuleManager | ||||||
|  |         from ansible.modules.network.f5.bigip_vcmp_guest import ArgumentSpec | ||||||
|  |         from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError | ||||||
|  |     except ImportError: | ||||||
|  |         raise SkipTest("F5 Ansible modules require the f5-sdk Python library") | ||||||
|  | 
 | ||||||
|  | fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') | ||||||
|  | fixture_data = {} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def set_module_args(args): | ||||||
|  |     args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) | ||||||
|  |     basic._ANSIBLE_ARGS = to_bytes(args) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def load_fixture(name): | ||||||
|  |     path = os.path.join(fixture_path, name) | ||||||
|  | 
 | ||||||
|  |     if path in fixture_data: | ||||||
|  |         return fixture_data[path] | ||||||
|  | 
 | ||||||
|  |     with open(path) as f: | ||||||
|  |         data = f.read() | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         data = json.loads(data) | ||||||
|  |     except Exception: | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     fixture_data[path] = data | ||||||
|  |     return data | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TestParameters(unittest.TestCase): | ||||||
|  |     def test_module_parameters(self): | ||||||
|  |         args = dict( | ||||||
|  |             initial_image='BIGIP-12.1.0.1.0.1447-HF1.iso', | ||||||
|  |             mgmt_network='bridged', | ||||||
|  |             mgmt_address='1.2.3.4/24', | ||||||
|  |             vlans=[ | ||||||
|  |                 'vlan1', | ||||||
|  |                 'vlan2' | ||||||
|  |             ] | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         p = Parameters(args) | ||||||
|  |         assert p.initial_image == 'BIGIP-12.1.0.1.0.1447-HF1.iso' | ||||||
|  |         assert p.mgmt_network == 'bridged' | ||||||
|  | 
 | ||||||
|  |     def test_module_parameters_mgmt_bridged_without_subnet(self): | ||||||
|  |         args = dict( | ||||||
|  |             mgmt_network='bridged', | ||||||
|  |             mgmt_address='1.2.3.4' | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         p = Parameters(args) | ||||||
|  |         assert p.mgmt_network == 'bridged' | ||||||
|  |         assert p.mgmt_address == '1.2.3.4/32' | ||||||
|  | 
 | ||||||
|  |     def test_module_parameters_mgmt_address_cidr(self): | ||||||
|  |         args = dict( | ||||||
|  |             mgmt_network='bridged', | ||||||
|  |             mgmt_address='1.2.3.4/24' | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         p = Parameters(args) | ||||||
|  |         assert p.mgmt_network == 'bridged' | ||||||
|  |         assert p.mgmt_address == '1.2.3.4/24' | ||||||
|  | 
 | ||||||
|  |     def test_module_parameters_mgmt_address_subnet(self): | ||||||
|  |         args = dict( | ||||||
|  |             mgmt_network='bridged', | ||||||
|  |             mgmt_address='1.2.3.4/255.255.255.0' | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         p = Parameters(args) | ||||||
|  |         assert p.mgmt_network == 'bridged' | ||||||
|  |         assert p.mgmt_address == '1.2.3.4/24' | ||||||
|  | 
 | ||||||
|  |     def test_module_parameters_mgmt_route(self): | ||||||
|  |         args = dict( | ||||||
|  |             mgmt_route='1.2.3.4' | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         p = Parameters(args) | ||||||
|  |         assert p.mgmt_route == '1.2.3.4' | ||||||
|  | 
 | ||||||
|  |     def test_module_parameters_vcmp_software_image_facts(self): | ||||||
|  |         # vCMP images may include a forward slash in their names. This is probably | ||||||
|  |         # related to the slots on the system, but it is not a valid value to specify | ||||||
|  |         # that slot when providing an initial image | ||||||
|  |         args = dict( | ||||||
|  |             initial_image='BIGIP-12.1.0.1.0.1447-HF1.iso/1', | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         p = Parameters(args) | ||||||
|  |         assert p.initial_image == 'BIGIP-12.1.0.1.0.1447-HF1.iso/1' | ||||||
|  | 
 | ||||||
|  |     def test_api_parameters(self): | ||||||
|  |         args = dict( | ||||||
|  |             initialImage="BIGIP-tmos-tier2-13.1.0.0.0.931.iso", | ||||||
|  |             managementGw="2.2.2.2", | ||||||
|  |             managementIp="1.1.1.1/24", | ||||||
|  |             managementNetwork="bridged", | ||||||
|  |             state="deployed", | ||||||
|  |             vlans=[ | ||||||
|  |                 "/Common/vlan1", | ||||||
|  |                 "/Common/vlan2" | ||||||
|  |             ] | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         p = Parameters(args) | ||||||
|  |         assert p.initial_image == 'BIGIP-tmos-tier2-13.1.0.0.0.931.iso' | ||||||
|  |         assert p.mgmt_route == '2.2.2.2' | ||||||
|  |         assert p.mgmt_address == '1.1.1.1/24' | ||||||
|  |         assert '/Common/vlan1' in p.vlans | ||||||
|  |         assert '/Common/vlan2' in p.vlans | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @patch('ansible.module_utils.f5_utils.AnsibleF5Client._get_mgmt_root', | ||||||
|  |        return_value=True) | ||||||
|  | class TestManager(unittest.TestCase): | ||||||
|  |     def setUp(self): | ||||||
|  |         self.spec = ArgumentSpec() | ||||||
|  | 
 | ||||||
|  |     def test_create_vlan(self, *args): | ||||||
|  |         set_module_args(dict( | ||||||
|  |             name="guest1", | ||||||
|  |             mgmt_network="bridged", | ||||||
|  |             mgmt_address="10.10.10.10/24", | ||||||
|  |             initial_image="BIGIP-13.1.0.0.0.931.iso", | ||||||
|  |             server='localhost', | ||||||
|  |             password='password', | ||||||
|  |             user='admin' | ||||||
|  |         )) | ||||||
|  | 
 | ||||||
|  |         client = AnsibleF5Client( | ||||||
|  |             argument_spec=self.spec.argument_spec, | ||||||
|  |             supports_check_mode=self.spec.supports_check_mode, | ||||||
|  |             f5_product_name=self.spec.f5_product_name | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         # Override methods to force specific logic in the module to happen | ||||||
|  |         mm = ModuleManager(client) | ||||||
|  |         mm.create_on_device = Mock(return_value=True) | ||||||
|  |         mm.exists = Mock(return_value=False) | ||||||
|  |         mm.is_deployed = Mock(side_effect=[False, True, True, True, True]) | ||||||
|  |         mm.deploy_on_device = Mock(return_value=True) | ||||||
|  | 
 | ||||||
|  |         results = mm.exec_module() | ||||||
|  | 
 | ||||||
|  |         assert results['changed'] is True | ||||||
|  |         assert results['name'] == 'guest1' | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue