From 1aa248f4e2766e8330f7f6b287299da4cd1c3968 Mon Sep 17 00:00:00 2001
From: Tim Rupp <caphrim007@gmail.com>
Date: Fri, 11 May 2018 13:09:59 -0700
Subject: [PATCH] Adds various features to bigip virtual server (#40031)

* Add type to virtual_server
* Add address_translation to virtual_server
* Add port_translation to virtual_server
* Add ip_protocol to virtual_server
* Add firewall_enforced_policy to virtual_server
* Add firewall_staged_policy to virtual_server
* Add security_log_profiles to virtual_server
---
 .../network/f5/bigip_virtual_server.py        | 1289 +++++++++++++++--
 .../network/f5/test_bigip_virtual_server.py   |  246 +++-
 2 files changed, 1427 insertions(+), 108 deletions(-)

diff --git a/lib/ansible/modules/network/f5/bigip_virtual_server.py b/lib/ansible/modules/network/f5/bigip_virtual_server.py
index 18e7057d97..953640e65d 100644
--- a/lib/ansible/modules/network/f5/bigip_virtual_server.py
+++ b/lib/ansible/modules/network/f5/bigip_virtual_server.py
@@ -18,7 +18,7 @@ module: bigip_virtual_server
 short_description: Manage LTM virtual servers on a BIG-IP
 description:
   - Manage LTM virtual servers on a BIG-IP.
-version_added: "2.1"
+version_added: 2.1
 options:
   state:
     description:
@@ -32,6 +32,60 @@ options:
       - absent
       - enabled
       - disabled
+  type:
+    description:
+      - Specifies the network service provided by this virtual server.
+      - When creating a new virtual server, if this parameter is not provided, the
+        default will be C(standard).
+      - This value cannot be changed after it is set.
+      - When C(standard), specifies a virtual server that directs client traffic to
+        a load balancing pool and is the most basic type of virtual server. When you
+        first create the virtual server, you assign an existing default pool to it.
+        From then on, the virtual server automatically directs traffic to that default pool.
+      - When C(forwarding-l2), specifies a virtual server that shares the same IP address as a
+        node in an associated VLAN.
+      - When C(forwarding-ip), specifies a virtual server like other virtual servers, except
+        that the virtual server has no pool members to load balance. The virtual server simply
+        forwards the packet directly to the destination IP address specified in the client request.
+      - When C(performance-http), specifies a virtual server with which you associate a Fast HTTP
+        profile. Together, the virtual server and profile increase the speed at which the virtual
+        server processes HTTP requests.
+      - When C(performance-l4), specifies a virtual server with which you associate a Fast L4 profile.
+        Together, the virtual server and profile increase the speed at which the virtual server
+        processes layer 4 requests.
+      - When C(stateless), specifies a virtual server that accepts traffic matching the virtual
+        server address and load balances the packet to the pool members without attempting to
+        match the packet to a pre-existing connection in the connection table. New connections
+        are immediately removed from the connection table. This addresses the requirement for
+        one-way UDP traffic that needs to be processed at very high throughput levels, for example,
+        load balancing syslog traffic to a pool of syslog servers. Stateless virtual servers are
+        not suitable for processing traffic that requires stateful tracking, such as TCP traffic.
+        Stateless virtual servers do not support iRules, persistence, connection mirroring,
+        rateshaping, or SNAT automap.
+      - When C(reject), specifies that the BIG-IP system rejects any traffic destined for the
+        virtual server IP address.
+      - When C(dhcp), specifies a virtual server that relays Dynamic Host Control Protocol (DHCP)
+        client requests for an IP address to one or more DHCP servers, and provides DHCP server
+        responses with an available IP address for the client.
+      - When C(internal), specifies a virtual server that supports modification of HTTP requests
+        and responses. Internal virtual servers enable usage of ICAP (Internet Content Adaptation
+        Protocol) servers to modify HTTP requests and responses by creating and applying an ICAP
+        profile and adding Request Adapt or Response Adapt profiles to the virtual server.
+      - When C(message-routing), specifies a virtual server that uses a SIP application protocol
+        and functions in accordance with a SIP session profile and SIP router profile.
+    choices:
+      - standard
+      - forwarding-l2
+      - forwarding-ip
+      - performance-http
+      - performance-l4
+      - stateless
+      - reject
+      - dhcp
+      - internal
+      - message-routing
+    default: standard
+    version_added: 2.6
   name:
     description:
       - Virtual server name.
@@ -42,7 +96,8 @@ options:
     description:
       - Destination IP of the virtual server.
       - Required when C(state) is C(present) and virtual server does not exist.
-    required: True
+      - When C(type) is C(internal), this parameter is ignored. For all other types,
+        it is required.
     aliases:
       - address
       - ip
@@ -64,6 +119,22 @@ options:
         and virtual server does not exist.
       - If you do not want to specify a particular port, use the value C(0).
         The result is that the virtual server will listen on any port.
+      - When C(type) is C(dhcp), this module will force the C(port) parameter to be C(67).
+      - When C(type) is C(internal), this module will force the C(port) parameter to be C(0).
+      - In addition to specifying a port number, a select number of service names may also
+        be provided.
+      - The string C(ftp) may be substituted for for port C(21).
+      - The string C(http) may be substituted for for port C(80).
+      - The string C(https) may be substituted for for port C(443).
+      - The string C(telnet) may be substituted for for port C(23).
+      - The string C(smtp) may be substituted for for port C(25).
+      - The string C(snmp) may be substituted for for port C(161).
+      - The string C(snmp-trap) may be substituted for for port C(162).
+      - The string C(ssh) may be substituted for for port C(22).
+      - The string C(tftp) may be substituted for for port C(69).
+      - The string C(isakmp) may be substituted for for port C(500).
+      - The string C(mqtt) may be substituted for for port C(1883).
+      - The string C(mqtt-tls) may be substituted for for port C(8883).
   profiles:
     description:
       - List of profiles (HTTP, ClientSSL, ServerSSL, etc) to apply to both sides
@@ -73,6 +144,20 @@ options:
       - If you only want to apply a particular profile to the server-side of
         the connection, specify C(server-side) for the profile's C(context).
       - If C(context) is not provided, it will default to C(all).
+      - If you want to remove a profile from the list of profiles currently active
+        on the virtual, then simply remove it from the C(profiles) list. See
+        examples for an illustration of this.
+      - If you want to add a profile to the list of profiles currently active
+        on the virtual, then simply add it to the C(profiles) list. See
+        examples for an illustration of this.
+      - B(Profiles matter). There is a good chance that this module will fail to configure
+        a BIG-IP if you mix up your profiles, or, if you attempt to set an IP protocol
+        which your current, or new, profiles do not support. Both this module, and BIG-IP,
+        will tell you when you are wrong, with an error resembling C(lists profiles
+        incompatible with its protocol).
+      - If you are unsure what correct profile combinations are, then have a BIG-IP
+        available to you in which you can make changes and copy what the correct
+        combinations are.
     suboptions:
       name:
         description:
@@ -80,7 +165,6 @@ options:
           - If this is not specified, then it is assumed that the profile item is
             only a name of a profile.
           - This must be specified if a context is specified.
-        required: false
       context:
         description:
           - The side of the connection on which the profile should be applied.
@@ -92,11 +176,15 @@ options:
     aliases:
       - all_profiles
   irules:
-    version_added: "2.2"
+    version_added: 2.2
     description:
       - List of rules to be applied in priority order.
       - If you want to remove existing iRules, specify a single empty value; C("").
         See the documentation for an example.
+      - When C(type) is C(dhcp), this parameter will be ignored.
+      - When C(type) is C(stateless), this parameter will be ignored.
+      - When C(type) is C(reject), this parameter will be ignored.
+      - When C(type) is C(internal), this parameter will be ignored.
     aliases:
       - all_rules
   enabled_vlans:
@@ -118,15 +206,24 @@ options:
       - Default pool for the virtual server.
       - If you want to remove the existing pool, specify an empty value; C("").
         See the documentation for an example.
+      - When creating a new virtual server, and C(type) is C(stateless), this parameter
+        is required.
+      - If C(type) is C(stateless), the C(pool) that is used must not have any members
+        which define a C(rate_limit).
   policies:
     description:
-      - Specifies the policies for the virtual server
+      - Specifies the policies for the virtual server.
+      - When C(type) is C(dhcp), this parameter will be ignored.
+      - When C(type) is C(reject), this parameter will be ignored.
+      - When C(type) is C(internal), this parameter will be ignored.
     aliases:
       - all_policies
   snat:
     description:
       - Source network address policy.
-    required: false
+      - When C(type) is C(dhcp), this parameter is ignored.
+      - When C(type) is C(reject), this parameter will be ignored.
+      - When C(type) is C(internal), this parameter will be ignored.
     choices:
       - None
       - Automap
@@ -137,6 +234,7 @@ options:
       - Default Profile which manages the session persistence.
       - If you want to remove the existing default persistence profile, specify an
         empty value; C(""). See the documentation for an example.
+      - When C(type) is C(dhcp), this parameter will be ignored.
   description:
     description:
       - Virtual server description.
@@ -146,6 +244,7 @@ options:
         cannot use the specified default persistence profile.
       - If you want to remove the existing fallback persistence profile, specify an
         empty value; C(""). See the documentation for an example.
+      - When C(type) is C(dhcp), this parameter will be ignored.
     version_added: 2.3
   partition:
     description:
@@ -161,6 +260,78 @@ options:
         that are numbers.
       - Data will be persisted, not ephemeral.
     version_added: 2.5
+  address_translation:
+    description:
+      - Specifies, when C(enabled), that the system translates the address of the
+        virtual server.
+      - When C(disabled), specifies that the system uses the address without translation.
+      - This option is useful when the system is load balancing devices that have the
+        same IP address.
+      - When creating a new virtual server, the default is C(enabled).
+    type: bool
+    version_added: 2.6
+  port_translation:
+    description:
+      - Specifies, when C(enabled), that the system translates the port of the virtual
+        server.
+      - When C(disabled), specifies that the system uses the port without translation.
+        Turning off port translation for a virtual server is useful if you want to use
+        the virtual server to load balance connections to any service.
+      - When creating a new virtual server, the default is C(enabled).
+    type: bool
+    version_added: 2.6
+  ip_protocol:
+    description:
+      - Specifies a network protocol name you want the system to use to direct traffic
+        on this virtual server.
+      - When creating a new virtual server, if this parameter is not specified, the default is C(tcp).
+      - The Protocol setting is not available when you select Performance (HTTP) as the Type.
+      - The value of this argument can be specified in either it's numeric value, or,
+        for convenience, in a select number of named values. Refer to C(choices) for examples.
+      - For a list of valid IP protocol numbers, refer to this page
+        https://en.wikipedia.org/wiki/List_of_IP_protocol_numbers
+      - When C(type) is C(dhcp), this module will force the C(ip_protocol) parameter to be C(17) (UDP).
+    choices:
+      - ah
+      - bna
+      - esp
+      - etherip
+      - gre
+      - icmp
+      - ipencap
+      - ipv6
+      - ipv6-auth
+      - ipv6-crypt
+      - ipv6-icmp
+      - isp-ip
+      - mux
+      - ospf
+      - sctp
+      - tcp
+      - udp
+      - udplite
+    version_added: 2.6
+  firewall_enforced_policy:
+    description:
+      - Applies the specify AFM policy to the virtual in an enforcing way.
+      - When creating a new virtual, if this parameter is not specified, the enforced
+        policy is disabled.
+    version_added: 2.6
+  firewall_staged_policy:
+    description:
+      - Applies the specify AFM policy to the virtual in an enforcing way.
+      - A staged policy shows the results of the policy rules in the log, while not
+        actually applying the rules to traffic.
+      - When creating a new virtual, if this parameter is not specified, the staged
+        policy is disabled.
+    version_added: 2.6
+  security_log_profiles:
+    description:
+      - Specifies the log profile applied to the virtual server.
+      - To make use of this feature, the AFM module must be licensed and provisioned.
+      - The C(Log all requests) and C(Log illegal requests) are mutually exclusive and
+        therefore, this module will raise an error if the two are specified together.
+    version_added: 2.6
 notes:
   - Requires BIG-IP software version >= 11
   - Requires the netaddr Python package on the host. This is as easy as pip
@@ -283,6 +454,44 @@ EXAMPLES = r'''
       ansible: 2.4
       updated_at: 2017-12-20T17:50:46Z
   delegate_to: localhost
+
+- name: Add virtual with two profiles
+  bigip_pool:
+    server: lb.mydomain.com
+    user: admin
+    password: secret
+    state: absent
+    name: my-pool
+    partition: Common
+    profiles:
+      - http
+      - tcp
+  delegate_to: localhost
+
+- name: Remove HTTP profile from previous virtual
+  bigip_pool:
+    server: lb.mydomain.com
+    user: admin
+    password: secret
+    state: absent
+    name: my-pool
+    partition: Common
+    profiles:
+      - tcp
+  delegate_to: localhost
+
+- name: Add the HTTP profile back to the previous virtual
+  bigip_pool:
+    server: lb.mydomain.com
+    user: admin
+    password: secret
+    state: absent
+    name: my-pool
+    partition: Common
+    profiles:
+      - http
+      - tcp
+  delegate_to: localhost
 '''
 
 RETURN = r'''
@@ -366,6 +575,36 @@ metadata:
   returned: changed
   type: dict
   sample: {'key1': 'foo', 'key2': 'bar'}
+address_translation:
+  description: The new value specifying whether address translation is on or off.
+  returned: changed
+  type: bool
+  sample: True
+port_translation:
+  description: The new value specifying whether port translation is on or off.
+  returned: changed
+  type: bool
+  sample: True
+ip_protocol:
+  description: The new value of the IP protocol.
+  returned: changed
+  type: int
+  sample: 6
+firewall_enforced_policy:
+  description: The new enforcing firewall policy.
+  returned: changed
+  type: string
+  sample: /Common/my-enforced-fw
+firewall_staged_policy:
+  description: The new staging firewall policy.
+  returned: changed
+  type: string
+  sample: /Common/my-staged-fw
+security_log_profiles:
+  description: The new list of security log profiles.
+  returned: changed
+  type: list
+  sample: ['/Common/profile1', '/Common/profile2']
 '''
 
 import re
@@ -376,27 +615,24 @@ from ansible.module_utils.six import iteritems
 from collections import namedtuple
 
 try:
-    # Sideband repository used for dev
     from library.module_utils.network.f5.bigip import HAS_F5SDK
     from library.module_utils.network.f5.bigip import F5Client
     from library.module_utils.network.f5.common import F5ModuleError
     from library.module_utils.network.f5.common import AnsibleF5Parameters
     from library.module_utils.network.f5.common import cleanup_tokens
-    from library.module_utils.network.f5.common import fqdn_name
+    from library.module_utils.network.f5.common import fq_name
     from library.module_utils.network.f5.common import f5_argument_spec
     try:
         from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
     except ImportError:
         HAS_F5SDK = False
-    HAS_DEVEL_IMPORTS = True
 except ImportError:
-    # Upstream Ansible
     from ansible.module_utils.network.f5.bigip import HAS_F5SDK
     from ansible.module_utils.network.f5.bigip import F5Client
     from ansible.module_utils.network.f5.common import F5ModuleError
     from ansible.module_utils.network.f5.common import AnsibleF5Parameters
     from ansible.module_utils.network.f5.common import cleanup_tokens
-    from ansible.module_utils.network.f5.common import fqdn_name
+    from ansible.module_utils.network.f5.common import fq_name
     from ansible.module_utils.network.f5.common import f5_argument_spec
     try:
         from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
@@ -419,7 +655,13 @@ class Parameters(AnsibleF5Parameters):
         'vlansDisabled': 'vlans_disabled',
         'profilesReference': 'profiles',
         'policiesReference': 'policies',
-        'rules': 'irules'
+        'rules': 'irules',
+        'translateAddress': 'address_translation',
+        'translatePort': 'port_translation',
+        'ipProtocol': 'ip_protocol',
+        'fwEnforcedPolicy': 'firewall_enforced_policy',
+        'fwStagedPolicy': 'firewall_staged_policy',
+        'securityLogProfiles': 'security_log_profiles'
     }
 
     api_attributes = [
@@ -428,6 +670,7 @@ class Parameters(AnsibleF5Parameters):
         'disabled',
         'enabled',
         'fallbackPersistence',
+        # 'ipProtocol',
         'metadata',
         'persist',
         'policies',
@@ -439,9 +682,21 @@ class Parameters(AnsibleF5Parameters):
         'vlans',
         'vlansEnabled',
         'vlansDisabled',
+        'translateAddress',
+        'translatePort',
+        'l2Forward',
+        'ipForward',
+        'stateless',
+        'reject',
+        'dhcpRelay',
+        'internal',
+        'fwEnforcedPolicy',
+        'fwStagedPolicy',
+        'securityLogProfiles',
     ]
 
     updatables = [
+        'address_translation',
         'description',
         'default_persistence_profile',
         'destination',
@@ -449,17 +704,24 @@ class Parameters(AnsibleF5Parameters):
         'enabled',
         'enabled_vlans',
         'fallback_persistence_profile',
+        # 'ip_protocol',
         'irules',
         'metadata',
         'pool',
         'policies',
         'port',
+        'port_translation',
         'profiles',
         'snat',
-        'source'
+        'source',
+        'type',
+        'firewall_enforced_policy',
+        'firewall_staged_policy',
+        'security_log_profiles',
     ]
 
     returnables = [
+        'address_translation',
         'description',
         'default_persistence_profile',
         'destination',
@@ -468,17 +730,23 @@ class Parameters(AnsibleF5Parameters):
         'enabled',
         'enabled_vlans',
         'fallback_persistence_profile',
+        # 'ip_protocol',
         'irules',
         'metadata',
         'pool',
         'policies',
         'port',
+        'port_translation',
         'profiles',
         'snat',
         'source',
         'vlans',
         'vlans_enabled',
-        'vlans_disabled'
+        'vlans_disabled',
+        'type',
+        'firewall_enforced_policy',
+        'firewall_staged_policy',
+        'security_log_profiles',
     ]
 
     profiles_mutex = [
@@ -486,21 +754,37 @@ class Parameters(AnsibleF5Parameters):
         'diametersession', 'radius', 'ftp', 'tftp', 'dns', 'pptp', 'fix'
     ]
 
+    ip_protocols_map = [
+        ('ah', 51),
+        ('bna', 49),
+        ('esp', 50),
+        ('etherip', 97),
+        ('gre', 47),
+        ('icmp', 1),
+        ('ipencap', 4),
+        ('ipv6', 41),
+        ('ipv6-auth', 51),   # not in the official list
+        ('ipv6-crypt', 50),  # not in the official list
+        ('ipv6-icmp', 58),
+        ('iso-ip', 80),
+        ('mux', 18),
+        ('ospf', 89),
+        ('sctp', 132),
+        ('tcp', 6),
+        ('udp', 17),
+        ('udplite', 136),
+    ]
+
     def to_return(self):
         result = {}
         for returnable in self.returnables:
             try:
                 result[returnable] = getattr(self, returnable)
-            except Exception as ex:
+            except Exception:
                 pass
         result = self._filter_params(result)
         return result
 
-    def _fqdn_name(self, value):
-        if value is not None and not value.startswith('/'):
-            return '/{0}/{1}'.format(self.partition, value)
-        return value
-
     def is_valid_ip(self, value):
         try:
             netaddr.IPAddress(value)
@@ -523,30 +807,146 @@ class Parameters(AnsibleF5Parameters):
         if port is None:
             if route_domain is None:
                 result = '{0}'.format(
-                    self._fqdn_name(address)
+                    fq_name(self.partition, address)
                 )
             else:
                 result = '{0}%{1}'.format(
-                    self._fqdn_name(address),
+                    fq_name(self.partition, address),
                     route_domain
                 )
         else:
             port = self._format_port_for_destination(address, port)
             if route_domain is None:
                 result = '{0}{1}'.format(
-                    self._fqdn_name(address),
+                    fq_name(self.partition, address),
                     port
                 )
             else:
                 result = '{0}%{1}{2}'.format(
-                    self._fqdn_name(address),
+                    fq_name(self.partition, address),
                     route_domain,
                     port
                 )
         return result
 
+    @property
+    def ip_protocol(self):
+        if self._values['ip_protocol'] is None:
+            return None
+        if self._values['ip_protocol'] == 'any':
+            return 'any'
+        for x in self.ip_protocols_map:
+            if x[0] == self._values['ip_protocol']:
+                return int(x[1])
+        try:
+            return int(self._values['ip_protocol'])
+        except ValueError:
+            raise F5ModuleError(
+                "Specified ip_protocol was neither a number nor in the list of common protocols."
+            )
+
+    @property
+    def has_message_routing_profiles(self):
+        if self.profiles is None:
+            return None
+        current = self._read_current_message_routing_profiles_from_device()
+        result = [x['name'] for x in self.profiles if x['name'] in current]
+        if len(result) > 0:
+            return True
+        return False
+
+    @property
+    def has_fastl4_profiles(self):
+        if self.profiles is None:
+            return None
+        current = self._read_current_fastl4_profiles_from_device()
+        result = [x['name'] for x in self.profiles if x['name'] in current]
+        if len(result) > 0:
+            return True
+        return False
+
+    @property
+    def has_fasthttp_profiles(self):
+        """Check if ``fasthttp`` profile is in API profiles
+
+        This method is used to determine the server type when doing comparisons
+        in the Difference class.
+
+        Returns:
+             bool: True if server has ``fasthttp`` profiles. False otherwise.
+        """
+        if self.profiles is None:
+            return None
+        current = self._read_current_fasthttp_profiles_from_device()
+        result = [x['name'] for x in self.profiles if x['name'] in current]
+        if len(result) > 0:
+            return True
+        return False
+
+    def _read_current_message_routing_profiles_from_device(self):
+        collection1 = self.client.api.tm.ltm.profile.diameters.get_collection()
+        collection2 = self.client.api.tm.ltm.profile.sips.get_collection()
+        result = [x.name for x in collection1]
+        result += [x.name for x in collection2]
+        return result
+
+    def _read_current_fastl4_profiles_from_device(self):
+        collection = self.client.api.tm.ltm.profile.fastl4s.get_collection()
+        result = [x.name for x in collection]
+        return result
+
+    def _read_current_fasthttp_profiles_from_device(self):
+        collection = self.client.api.tm.ltm.profile.fasthttps.get_collection()
+        result = [x.name for x in collection]
+        return result
+
 
 class ApiParameters(Parameters):
+    @property
+    def type(self):
+        """Attempt to determine the current server type
+
+        This check is very unscientific. It turns out that this information is not
+        exactly available anywhere on a BIG-IP. Instead, we rely on a semi-reliable
+        means for determining what the type of the virtual server is. Hopefully it
+        always works.
+
+        There are a handful of attributes that can be used to determine a specific
+        type. There are some types though that can only be determined by looking at
+        the profiles that are assigned to them. We follow that method for those
+        complicated types; message-routing, fasthttp, and fastl4.
+
+        Because type determination is an expensive operation, we cache the result
+        from the operation.
+
+        Returns:
+            string: The server type.
+        """
+        if self._values['type']:
+            return self._values['type']
+        if self.l2Forward is True:
+            result = 'forwarding-l2'
+        elif self.ipForward is True:
+            result = 'forwarding-ip'
+        elif self.stateless is True:
+            result = 'stateless'
+        elif self.reject is True:
+            result = 'reject'
+        elif self.dhcpRelay is True:
+            result = 'dhcp'
+        elif self.internal is True:
+            result = 'internal'
+        elif self.has_fasthttp_profiles:
+            result = 'performance-http'
+        elif self.has_fastl4_profiles:
+            result = 'performance-l4'
+        elif self.has_message_routing_profiles:
+            result = 'message-routing'
+        else:
+            result = 'standard'
+        self._values['type'] = result
+        return result
+
     @property
     def destination(self):
         if self._values['destination'] is None:
@@ -676,12 +1076,35 @@ class ApiParameters(Parameters):
 
     @property
     def route_domain(self):
+        """Return a route domain number from the destination
+
+        Returns:
+            int: The route domain number
+        """
         destination = self.destination_tuple
         self._values['route_domain'] = destination.route_domain
-        return destination.route_domain
+        return int(destination.route_domain)
 
     @property
     def profiles(self):
+        """Returns a list of profiles from the API
+
+        The profiles are formatted so that they are usable in this module and
+        are able to be compared by the Difference engine.
+
+        Returns:
+             list (:obj:`list` of :obj:`dict`): List of profiles.
+
+             Each dictionary in the list contains the following three (3) keys.
+
+             * name
+             * context
+             * fullPath
+
+        Raises:
+            F5ModuleError: If the specified context is a value other that
+                ``all``, ``serverside``, or ``clientside``.
+        """
         if 'items' not in self._values['profiles']:
             return None
         result = []
@@ -696,6 +1119,10 @@ class ApiParameters(Parameters):
                 )
         return result
 
+    @property
+    def profile_types(self):
+        return [x['name'] for x in iteritems(self.profiles)]
+
     @property
     def policies(self):
         if 'items' not in self._values['policies']:
@@ -709,11 +1136,17 @@ class ApiParameters(Parameters):
 
     @property
     def default_persistence_profile(self):
+        """Get the name of the current default persistence profile
+
+        These persistence profiles are always lists when we get them
+        from the REST API even though there can only be one. We'll
+        make it a list again when we get to the Difference engine.
+
+        Returns:
+             string: The name of the default persistence profile
+        """
         if self._values['default_persistence_profile'] is None:
             return None
-        # These persistence profiles are always lists when we get them
-        # from the REST API even though there can only be one. We'll
-        # make it a list again when we get to the Difference engine.
         return self._values['default_persistence_profile'][0]
 
     @property
@@ -743,8 +1176,39 @@ class ApiParameters(Parameters):
             result.append(tmp)
         return result
 
+    @property
+    def security_log_profiles(self):
+        if self._values['security_log_profiles'] is None:
+            return None
+        # At the moment, BIG-IP wraps the names of log profiles in double-quotes if
+        # the profile name contains spaces. This is likely due to the REST code being
+        # too close to actual tmsh code and, at the tmsh level, a space in the profile
+        # name would cause tmsh to see the 2nd word (and beyond) as "the next parameter".
+        #
+        # This seems like a bug to me.
+        result = list(set([x.strip('"') for x in self._values['security_log_profiles']]))
+        result.sort()
+        return result
+
 
 class ModuleParameters(Parameters):
+    services_map = {
+        'ftp': 21,
+        'http': 80,
+        'https': 443,
+        'telnet': 23,
+        'pptp': 1723,
+        'smtp': 25,
+        'snmp': 161,
+        'snmp-trap': 162,
+        'ssh': 22,
+        'tftp': 69,
+        'isakmp': 500,
+        'mqtt': 1883,
+        'mqtt-tls': 8883,
+        'rtsp': 554
+    }
+
     def _handle_profile_context(self, tmp):
         if 'context' not in tmp:
             tmp['context'] = 'all'
@@ -762,6 +1226,19 @@ class ModuleParameters(Parameters):
         if profile['context'] != 'clientside':
             profile['context'] = 'clientside'
 
+    def _check_port(self):
+        try:
+            port = int(self._values['port'])
+        except ValueError:
+            raise F5ModuleError(
+                "The specified port was not a valid integer"
+            )
+        if 0 <= port <= 65535:
+            return port
+        raise F5ModuleError(
+            "Valid ports must be in range 0 - 65535"
+        )
+
     @property
     def destination(self):
         addr = self._values['destination'].split("%")[0]
@@ -801,22 +1278,12 @@ class ModuleParameters(Parameters):
             return None
         if self._values['port'] in ['*', 'any']:
             return 0
+        if self._values['port'] in self.services_map:
+            port = self._values['port']
+            self._values['port'] = self.services_map[port]
         self._check_port()
         return int(self._values['port'])
 
-    def _check_port(self):
-        try:
-            port = int(self._values['port'])
-        except ValueError:
-            raise F5ModuleError(
-                "The specified port was not a valid integer"
-            )
-        if 0 <= port <= 65535:
-            return port
-        raise F5ModuleError(
-            "Valid ports must be in range 0 - 65535"
-        )
-
     @property
     def irules(self):
         results = []
@@ -825,7 +1292,7 @@ class ModuleParameters(Parameters):
         if len(self._values['irules']) == 1 and self._values['irules'][0] == '':
             return ''
         for irule in self._values['irules']:
-            result = self._fqdn_name(irule)
+            result = fq_name(self.partition, irule)
             results.append(result)
         return results
 
@@ -843,12 +1310,12 @@ class ModuleParameters(Parameters):
                 self._handle_profile_context(tmp)
                 if 'name' not in profile:
                     tmp['name'] = profile
-                tmp['fullPath'] = self._fqdn_name(tmp['name'])
+                tmp['fullPath'] = fq_name(self.partition, tmp['name'])
                 self._handle_clientssl_profile_nuances(tmp)
             else:
                 tmp['name'] = profile
                 tmp['context'] = 'all'
-                tmp['fullPath'] = self._fqdn_name(tmp['name'])
+                tmp['fullPath'] = fq_name(self.partition, tmp['name'])
                 self._handle_clientssl_profile_nuances(tmp)
             result.append(tmp)
         mutually_exclusive = [x['name'] for x in result if x in self.profiles_mutex]
@@ -867,7 +1334,7 @@ class ModuleParameters(Parameters):
         if len(self._values['policies']) == 1 and self._values['policies'][0] == '':
             return ''
         result = []
-        policies = [self._fqdn_name(p) for p in self._values['policies']]
+        policies = [fq_name(self.partition, p) for p in self._values['policies']]
         policies = set(policies)
         for policy in policies:
             parts = policy.split('/')
@@ -888,7 +1355,7 @@ class ModuleParameters(Parameters):
             return None
         if self._values['pool'] == '':
             return ''
-        return self._fqdn_name(self._values['pool'])
+        return fq_name(self.partition, self._values['pool'])
 
     @property
     def vlans_enabled(self):
@@ -917,7 +1384,7 @@ class ModuleParameters(Parameters):
         if self._values['enabled_vlans'] is None:
             return None
         elif any(x.lower() for x in self._values['enabled_vlans'] if x.lower() in ['all', '*']):
-            result = [self._fqdn_name('all')]
+            result = [fq_name(self.partition, 'all')]
             if result[0].endswith('/all'):
                 if self._values['__warnings'] is None:
                     self._values['__warnings'] = []
@@ -928,7 +1395,7 @@ class ModuleParameters(Parameters):
                     )
                 )
             return result
-        results = list(set([self._fqdn_name(x) for x in self._values['enabled_vlans']]))
+        results = list(set([fq_name(self.partition, x) for x in self._values['enabled_vlans']]))
         results.sort()
         return results
 
@@ -940,7 +1407,7 @@ class ModuleParameters(Parameters):
             raise F5ModuleError(
                 "You cannot disable all VLANs. You must name them individually."
             )
-        results = list(set([self._fqdn_name(x) for x in self._values['disabled_vlans']]))
+        results = list(set([fq_name(self.partition, x) for x in self._values['disabled_vlans']]))
         results.sort()
         return results
 
@@ -964,7 +1431,7 @@ class ModuleParameters(Parameters):
         lowercase = self._values['snat'].lower()
         if lowercase in ['automap', 'none']:
             return dict(type=lowercase)
-        snat_pool = self._fqdn_name(self._values['snat'])
+        snat_pool = fq_name(self.partition, self._values['snat'])
         return dict(pool=snat_pool, type='snat')
 
     @property
@@ -973,7 +1440,7 @@ class ModuleParameters(Parameters):
             return None
         if self._values['default_persistence_profile'] == '':
             return ''
-        profile = self._fqdn_name(self._values['default_persistence_profile'])
+        profile = fq_name(self.partition, self._values['default_persistence_profile'])
         parts = profile.split('/')
         if len(parts) != 3:
             raise F5ModuleError(
@@ -991,7 +1458,7 @@ class ModuleParameters(Parameters):
             return None
         if self._values['fallback_persistence_profile'] == '':
             return ''
-        result = self._fqdn_name(self._values['fallback_persistence_profile'])
+        result = fq_name(self.partition, self._values['fallback_persistence_profile'])
         return result
 
     @property
@@ -1033,12 +1500,56 @@ class ModuleParameters(Parameters):
             )
         return result
 
+    @property
+    def address_translation(self):
+        if self._values['address_translation'] is None:
+            return None
+        if self._values['address_translation']:
+            return 'enabled'
+        return 'disabled'
+
+    @property
+    def port_translation(self):
+        if self._values['port_translation'] is None:
+            return None
+        if self._values['port_translation']:
+            return 'enabled'
+        return 'disabled'
+
+    @property
+    def firewall_enforced_policy(self):
+        if self._values['firewall_enforced_policy'] is None:
+            return None
+        return fq_name(self.partition, self._values['firewall_enforced_policy'])
+
+    @property
+    def firewall_staged_policy(self):
+        if self._values['firewall_staged_policy'] is None:
+            return None
+        return fq_name(self.partition, self._values['firewall_staged_policy'])
+
+    @property
+    def security_log_profiles(self):
+        if self._values['security_log_profiles'] is None:
+            return None
+        if len(self._values['security_log_profiles']) == 1 and self._values['security_log_profiles'][0] == '':
+            return ''
+        result = list(set([fq_name(self.partition, x) for x in self._values['security_log_profiles']]))
+        result.sort()
+        return result
+
 
 class Changes(Parameters):
     pass
 
 
 class UsableChanges(Changes):
+    @property
+    def destination(self):
+        if self._values['type'] == 'internal':
+            return None
+        return self._values['destination']
+
     @property
     def vlans(self):
         if self._values['vlans'] is None:
@@ -1049,6 +1560,89 @@ class UsableChanges(Changes):
             return []
         return self._values['vlans']
 
+    @property
+    def irules(self):
+        if self._values['irules'] is None:
+            return None
+        if self._values['type'] in ['dhcp', 'stateless', 'reject', 'internal']:
+            return None
+        return self._values['irules']
+
+    @property
+    def policies(self):
+        if self._values['policies'] is None:
+            return None
+        if self._values['type'] in ['dhcp', 'reject', 'internal']:
+            return None
+        return self._values['policies']
+
+    @property
+    def default_persistence_profile(self):
+        if self._values['default_persistence_profile'] is None:
+            return None
+        if self._values['type'] == 'dhcp':
+            return None
+        if not self._values['default_persistence_profile']:
+            return []
+        return [self._values['default_persistence_profile']]
+
+    @property
+    def fallback_persistence_profile(self):
+        if self._values['fallback_persistence_profile'] is None:
+            return None
+        if self._values['type'] == 'dhcp':
+            return None
+        return self._values['fallback_persistence_profile']
+
+    @property
+    def snat(self):
+        if self._values['snat'] is None:
+            return None
+        if self._values['type'] in ['dhcp', 'reject', 'internal']:
+            return None
+        return self._values['snat']
+
+    @property
+    def dhcpRelay(self):
+        if self._values['type'] == 'dhcp':
+            return True
+
+    @property
+    def reject(self):
+        if self._values['type'] == 'reject':
+            return True
+
+    @property
+    def stateless(self):
+        if self._values['type'] == 'stateless':
+            return True
+
+    @property
+    def internal(self):
+        if self._values['type'] == 'internal':
+            return True
+
+    @property
+    def ipForward(self):
+        if self._values['type'] == 'forwarding-ip':
+            return True
+
+    @property
+    def l2Forward(self):
+        if self._values['type'] == 'forwarding-l2':
+            return True
+
+    @property
+    def security_log_profiles(self):
+        if self._values['security_log_profiles'] is None:
+            return None
+        mutex = ('Log all requests', 'Log illegal requests')
+        if len([x for x in self._values['security_log_profiles'] if x.endswith(mutex)]) >= 2:
+            raise F5ModuleError(
+                "The 'Log all requests' and 'Log illegal requests' are mutually exclusive."
+            )
+        return self._values['security_log_profiles']
+
 
 class ReportableChanges(Changes):
     @property
@@ -1102,6 +1696,474 @@ class ReportableChanges(Changes):
         if len(self._values['vlans']) > 0 and self._values['vlans_disabled'] is True:
             return self._values['vlans']
 
+    @property
+    def address_translation(self):
+        if self._values['address_translation'] == 'enabled':
+            return True
+        return False
+
+    @property
+    def port_translation(self):
+        if self._values['port_translation'] == 'enabled':
+            return True
+        return False
+
+
+class VirtualServerValidator(object):
+    def __init__(self, module=None, client=None, want=None, have=None):
+        self.have = have if have else ApiParameters()
+        self.want = want if want else ModuleParameters()
+        self.client = client
+        self.module = module
+
+    def check_update(self):
+        # TODO(Remove in Ansible 2.9)
+        self._override_standard_type_from_profiles()
+
+        # Regular checks
+        self._override_port_by_type()
+        self._override_protocol_by_type()
+        self._verify_type_has_correct_profiles()
+        self._verify_default_persistence_profile_for_type()
+        self._verify_fallback_persistence_profile_for_type()
+        self._update_persistence_profile()
+        self._ensure_server_type_supports_vlans()
+        self._verify_type_has_correct_ip_protocol()
+
+        # For different server types
+        self._verify_dhcp_profile()
+        self._verify_fastl4_profile()
+        self._verify_stateless_profile()
+
+    def check_create(self):
+        # TODO(Remove in Ansible 2.9)
+        self._override_standard_type_from_profiles()
+
+        # Regular checks
+        self._set_default_ip_protocol()
+        self._set_default_profiles()
+        self._override_port_by_type()
+        self._override_protocol_by_type()
+        self._verify_type_has_correct_profiles()
+        self._verify_default_persistence_profile_for_type()
+        self._verify_fallback_persistence_profile_for_type()
+        self._update_persistence_profile()
+        self._verify_virtual_has_required_parameters()
+        self._ensure_server_type_supports_vlans()
+        self._override_vlans_if_all_specified()
+        self._check_source_and_destination_match()
+        self._verify_type_has_correct_ip_protocol()
+        self._verify_minimum_profile()
+
+        # For different server types
+        self._verify_dhcp_profile()
+        self._verify_fastl4_profile()
+        self._verify_stateless_profile_on_create()
+
+    def _ensure_server_type_supports_vlans(self):
+        """Verifies the specified server type supports VLANs
+
+        A select number of server types do not support VLANs. This method
+        checks to see if the specified types were provided along with VLANs.
+        If they were, the module will raise an error informing the user that
+        they need to either remove the VLANs, or, change the ``type``.
+
+        Returns:
+            None: Returned if no VLANs are specified.
+        Raises:
+            F5ModuleError: Raised if the server type conflicts with VLANs.
+        """
+        if self.want.enabled_vlans is None:
+            return
+        if self.want.type == 'internal':
+            raise F5ModuleError(
+                "The 'internal' server type does not support VLANs."
+            )
+
+    def _override_vlans_if_all_specified(self):
+        """Overrides any specified VLANs if "all" VLANs are specified
+
+        The special setting "all VLANs" in a BIG-IP requires that no other VLANs
+        be specified. If you specify any number of VLANs, AND include the "all"
+        VLAN, this method will erase all of the other VLANs and only return the
+        "all" VLAN.
+        """
+        all_vlans = ['/common/all', 'all']
+        if self.want.enabled_vlans is not None:
+            if any(x for x in self.want.enabled_vlans if x.lower() in all_vlans):
+                self.want.update(
+                    dict(
+                        enabled_vlans=[],
+                        vlans_disabled=True,
+                        vlans_enabled=False
+                    )
+                )
+
+    def _override_port_by_type(self):
+        if self.want.type == 'dhcp':
+            self.want.update({'port': 67})
+        elif self.want.type == 'internal':
+            self.want.update({'port': 0})
+
+    def _override_protocol_by_type(self):
+        if self.want.type in ['stateless']:
+            self.want.update({'ip_protocol': 17})
+
+    def _override_standard_type_from_profiles(self):
+        """Overrides a standard virtual server type given the specified profiles
+
+        For legacy purposes, this module will do some basic overriding of the default
+        ``type`` parameter to support cases where changing the ``type`` only requires
+        specifying a different set of profiles.
+
+        Ideally, ``type`` would always be specified, but in the past, this module only
+        supported an implicit "standard" type. Module users would specify some different
+        types of profiles and this would change the type...in some circumstances.
+
+        Now that this module supports a ``type`` param, the implicit ``type`` changing
+        that used to happen is technically deprecated (and will be warned on). Users
+        should always specify a ``type`` now, or, accept the default standard type.
+
+        Returns:
+            void
+        """
+        if self.want.type == 'standard':
+            if self.want.has_fastl4_profiles:
+                self.want.update({'type': 'performance-l4'})
+                self.module.deprecate(
+                    msg="Specifying 'performance-l4' profiles on a 'standard' type is deprecated and will be removed.",
+                    version='2.6'
+                )
+            if self.want.has_fasthttp_profiles:
+                self.want.update({'type': 'performance-http'})
+                self.module.deprecate(
+                    msg="Specifying 'performance-http' profiles on a 'standard' type is deprecated and will be removed.",
+                    version='2.6'
+                )
+            if self.want.has_message_routing_profiles:
+                self.want.update({'type': 'message-routing'})
+                self.module.deprecate(
+                    msg="Specifying 'message-routing' profiles on a 'standard' type is deprecated and will be removed.",
+                    version='2.6'
+                )
+
+    def _check_source_and_destination_match(self):
+        """Verify that destination and source are of the same IP version
+
+        BIG-IP does not allow for mixing of the IP versions for destination and
+        source addresses. For example, a destination IPv6 address cannot be
+        associated with a source IPv4 address.
+
+        This method checks that you specified the same IP version for these
+        parameters
+
+        Raises:
+            F5ModuleError: Raised when the IP versions of source and destination differ.
+        """
+        if self.want.source and self.want.destination:
+            want = netaddr.IPNetwork(self.want.source)
+            have = netaddr.IPNetwork(self.want.destination_tuple.ip)
+            if want.version != have.version:
+                raise F5ModuleError(
+                    "The source and destination addresses for the virtual server must be be the same type (IPv4 or IPv6)."
+                )
+
+    def _verify_type_has_correct_ip_protocol(self):
+        if self.want.ip_protocol is None:
+            return
+        if self.want.type == 'standard':
+            # Standard supports
+            # - tcp
+            # - udp
+            # - sctp
+            # - ipsec-ah
+            # - ipsec esp
+            # - all protocols
+            if self.want.ip_protocol not in [6, 17, 132, 51, 50, 'any']:
+                raise F5ModuleError(
+                    "The 'standard' server type does not support the specified 'ip_protocol'."
+                )
+        elif self.want.type == 'performance-http':
+            # Perf HTTP supports
+            #
+            # - tcp
+            if self.want.ip_protocol not in [6]:
+                raise F5ModuleError(
+                    "The 'performance-http' server type does not support the specified 'ip_protocol'."
+                )
+        elif self.want.type == 'stateless':
+            # Stateless supports
+            #
+            # - udp
+            if self.want.ip_protocol not in [17]:
+                raise F5ModuleError(
+                    "The 'stateless' server type does not support the specified 'ip_protocol'."
+                )
+        elif self.want.type == 'dhcp':
+            # DHCP supports no IP protocols
+            if self.want.ip_protocol is not None:
+                raise F5ModuleError(
+                    "The 'dhcp' server type does not support an 'ip_protocol'."
+                )
+        elif self.want.type == 'internal':
+            # Internal supports
+            #
+            # - tcp
+            # - udp
+            if self.want.ip_protocol not in [6, 17]:
+                raise F5ModuleError(
+                    "The 'internal' server type does not support the specified 'ip_protocol'."
+                )
+        elif self.want.type == 'message-routing':
+            # Message Routing supports
+            #
+            # - tcp
+            # - udp
+            # - sctp
+            # - all protocols
+            if self.want.ip_protocol not in [6, 17, 132, 'all']:
+                raise F5ModuleError(
+                    "The 'message-routing' server type does not support the specified 'ip_protocol'."
+                )
+
+    def _verify_virtual_has_required_parameters(self):
+        """Verify that the virtual has required parameters
+
+        Virtual servers require several parameters that are not necessarily required
+        when updating the virtual. This method will check for the required params
+        upon creation.
+
+        Ansible supports ``default`` variables in an Argument Spec, but those defaults
+        apply to all operations; including create, update, and delete. Since users are not
+        required to always specify these parameters, we cannot use Ansible's facility.
+        If we did, and then users would be required to provide them when, for example,
+        they attempted to delete a virtual (even though they are not required to delete
+        a virtual.
+
+        Raises:
+             F5ModuleError: Raised when the user did not specify required parameters.
+        """
+        required_resources = ['destination', 'port']
+        if self.want.type == 'internal':
+            return
+        if all(getattr(self.want, v) is None for v in required_resources):
+            raise F5ModuleError(
+                "You must specify both of " + ', '.join(required_resources)
+            )
+
+    def _verify_default_persistence_profile_for_type(self):
+        """Verify that the server type supports default persistence profiles
+
+        Verifies that the specified server type supports default persistence profiles.
+        Some virtual servers do not support these types of profiles. This method will
+        check that the type actually supports what you are sending it.
+
+        Types that do not, at this time, support default persistence profiles include,
+
+        * dhcp
+        * message-routing
+        * reject
+        * stateless
+        * forwarding-ip
+        * forwarding-l2
+
+        Raises:
+            F5ModuleError: Raised if server type does not support default persistence profiles.
+        """
+        default_profile_not_allowed = [
+            'dhcp', 'message-routing', 'reject', 'stateless', 'forwarding-ip', 'forwarding-l2'
+        ]
+        if self.want.ip_protocol in default_profile_not_allowed:
+            raise F5ModuleError(
+                "The '{0}' server type does not support a 'default_persistence_profile'".format(self.want.type)
+            )
+
+    def _verify_fallback_persistence_profile_for_type(self):
+        """Verify that the server type supports fallback persistence profiles
+
+        Verifies that the specified server type supports fallback persistence profiles.
+        Some virtual servers do not support these types of profiles. This method will
+        check that the type actually supports what you are sending it.
+
+        Types that do not, at this time, support fallback persistence profiles include,
+
+        * dhcp
+        * message-routing
+        * reject
+        * stateless
+        * forwarding-ip
+        * forwarding-l2
+        * performance-http
+
+        Raises:
+            F5ModuleError: Raised if server type does not support fallback persistence profiles.
+        """
+        default_profile_not_allowed = [
+            'dhcp', 'message-routing', 'reject', 'stateless', 'forwarding-ip', 'forwarding-l2',
+            'performance-http'
+        ]
+        if self.want.ip_protocol in default_profile_not_allowed:
+            raise F5ModuleError(
+                "The '{0}' server type does not support a 'fallback_persistence_profile'".format(self.want.type)
+            )
+
+    def _update_persistence_profile(self):
+        # This must be changed back to a list to make a valid REST API
+        # value. The module manipulates this as a normal dictionary
+        if self.want.default_persistence_profile is not None:
+            self.want.update({'default_persistence_profile': self.want.default_persistence_profile})
+
+    def _verify_type_has_correct_profiles(self):
+        """Verify that specified server type does not include forbidden profiles
+
+        The type of the server determines the ``type``s of profiles that it accepts. This
+        method checks that the server ``type`` that you specified is indeed one that can
+        accept the profiles that you specified.
+
+        The common situations are
+
+        * ``standard`` types that include ``fasthttp``, ``fastl4``, or ``message routing`` profiles
+        * ``fasthttp`` types that are missing a ``fasthttp`` profile
+        * ``fastl4`` types that are missing a ``fastl4`` profile
+        * ``message-routing`` types that are missing ``diameter`` or ``sip`` profiles
+
+        Raises:
+            F5ModuleError: Raised when a validation check fails.
+        """
+        if self.want.type == 'standard':
+            if self.want.has_fasthttp_profiles:
+                raise F5ModuleError("A 'standard' type may not have 'fasthttp' profiles.")
+            if self.want.has_fastl4_profiles:
+                raise F5ModuleError("A 'standard' type may not have 'fastl4' profiles.")
+            if self.want.has_message_routing_profiles:
+                raise F5ModuleError("A 'standard' type may not have 'message-routing' profiles.")
+        elif self.want.type == 'performance-http':
+            if not self.want.has_fasthttp_profiles:
+                raise F5ModuleError("A 'fasthttp' type must have at least one 'fasthttp' profile.")
+        elif self.want.type == 'performance-l4':
+            if not self.want.has_fastl4_profiles:
+                raise F5ModuleError("A 'fastl4' type must have at least one 'fastl4' profile.")
+        elif self.want.type == 'message-routing':
+            if not self.want.has_message_routing_profiles:
+                raise F5ModuleError("A 'message-routing' type must have either a 'sip' or 'diameter' profile.")
+
+    def _set_default_ip_protocol(self):
+        if self.want.type == 'dhcp':
+            return
+        if self.want.ip_protocol is None:
+            self.want.update({'ip_protocol': 6})
+
+    def _set_default_profiles(self):
+        if self.want.type == 'standard':
+            if not self.want.profiles:
+                # Sets a default profiles when creating a new standard virtual.
+                #
+                # It appears that if no profiles are deliberately specified, then under
+                # certain circumstances, the server type will default to ``performance-l4``.
+                #
+                # It's unclear what these circumstances are, but they are met in issue 00093.
+                # If this block of profile setting code is removed, the virtual server's
+                # type will change to performance-l4 for some reason.
+                #
+                if self.want.ip_protocol == 6:
+                    self.want.update({'profiles': ['tcp']})
+                if self.want.ip_protocol == 17:
+                    self.want.update({'profiles': ['udp']})
+                if self.want.ip_protocol == 132:
+                    self.want.update({'profiles': ['sctp']})
+
+    def _verify_minimum_profile(self):
+        if self.want.profiles:
+            return None
+        if self.want.type == 'internal' and self.want.profiles == '':
+            raise F5ModuleError(
+                "An 'internal' server must have at least one profile relevant to its 'ip_protocol'. "
+                "For example, 'tcp', 'udp', or variations of those."
+            )
+
+    def _verify_dhcp_profile(self):
+        if self.want.type != 'dhcp':
+            return
+        if self.want.profiles is None:
+            return
+        have = set(self.read_dhcp_profiles_from_device())
+        want = set([x['fullPath'] for x in self.want.profiles])
+        if have.intersection(want):
+            return True
+        raise F5ModuleError(
+            "A dhcp profile, such as 'dhcpv4', or 'dhcpv6' must be specified when 'type' is 'dhcp'."
+        )
+
+    def _verify_fastl4_profile(self):
+        if self.want.type != 'performance-l4':
+            return
+        if self.want.profiles is None:
+            return
+        have = set(self.read_fastl4_profiles_from_device())
+        want = set([x['fullPath'] for x in self.want.profiles])
+        if have.intersection(want):
+            return True
+        raise F5ModuleError(
+            "A performance-l4 profile, such as 'fastL4', must be specified when 'type' is 'performance-l4'."
+        )
+
+    def _verify_fasthttp_profile(self):
+        if self.want.type != 'performance-http':
+            return
+        if self.want.profiles is None:
+            return
+        have = set(self.read_fasthttp_profiles_from_device())
+        want = set([x['fullPath'] for x in self.want.profiles])
+        if have.intersection(want):
+            return True
+        raise F5ModuleError(
+            "A performance-http profile, such as 'fasthttp', must be specified when 'type' is 'performance-http'."
+        )
+
+    def _verify_stateless_profile_on_create(self):
+        if self.want.type != 'stateless':
+            return
+        result = self._verify_stateless_profile()
+        if result is None:
+            raise F5ModuleError(
+                "A udp profile, must be specified when 'type' is 'stateless'."
+            )
+
+    def _verify_stateless_profile(self):
+        if self.want.type != 'stateless':
+            return
+        if self.want.profiles is None:
+            return
+        have = set(self.read_udp_profiles_from_device())
+        want = set([x['fullPath'] for x in self.want.profiles])
+        if have.intersection(want):
+            return True
+        raise F5ModuleError(
+            "A udp profile, must be specified when 'type' is 'stateless'."
+        )
+
+    def read_dhcp_profiles_from_device(self):
+        collection = self.client.api.tm.ltm.profile.dhcpv4s.get_collection()
+        result = [fq_name(self.want.partition, x.name) for x in collection]
+        collection = self.client.api.tm.ltm.profile.dhcpv6s.get_collection()
+        result += [fq_name(self.want.partition, x.name) for x in collection]
+        return result
+
+    def read_fastl4_profiles_from_device(self):
+        collection = self.client.api.tm.ltm.profile.fastl4s.get_collection()
+        result = [fq_name(self.want.partition, x.name) for x in collection]
+        return result
+
+    def read_fasthttp_profiles_from_device(self):
+        collection = self.client.api.tm.ltm.profile.fasthttps.get_collection()
+        result = [fq_name(self.want.partition, x.name) for x in collection]
+        return result
+
+    def read_udp_profiles_from_device(self):
+        collection = self.client.api.tm.ltm.profile.udps.get_collection()
+        result = [fq_name(self.want.partition, x.name) for x in collection]
+        return result
+
 
 class Difference(object):
     def __init__(self, want, have=None):
@@ -1160,6 +2222,10 @@ class Difference(object):
 
     @property
     def destination(self):
+        # The internal type does not support the 'destination' parameter, so it is ignored.
+        if self.want.type == 'internal':
+            return
+
         addr_tuple = [self.want.destination, self.want.port, self.want.route_domain]
         if all(x for x in addr_tuple if x is None):
             return None
@@ -1177,7 +2243,7 @@ class Difference(object):
 
         want = self.want._format_destination(address, self.want.port, self.want.route_domain)
         if want != self.have.destination:
-            return self.want._fqdn_name(want)
+            return fq_name(self.want.partition, want)
 
     @property
     def source(self):
@@ -1258,6 +2324,7 @@ class Difference(object):
             return None
         want = set([(p['name'], p['context'], p['fullPath']) for p in self.want.profiles])
         have = set([(p['name'], p['context'], p['fullPath']) for p in self.have.profiles])
+
         if len(have) == 0:
             return self.want.profiles
         elif len(have) == 1:
@@ -1265,16 +2332,23 @@ class Difference(object):
                 return self.want.profiles
         else:
             if not any(x[0] == 'tcp' for x in want):
-                have = set([x for x in have if x[0] != 'tcp'])
+                if self.want.type != 'stateless':
+                    have = set([x for x in have if x[0] != 'tcp'])
             if not any(x[0] == 'udp' for x in want):
                 have = set([x for x in have if x[0] != 'udp'])
             if not any(x[0] == 'sctp' for x in want):
-                have = set([x for x in have if x[0] != 'sctp'])
+                if self.want.type != 'stateless':
+                    have = set([x for x in have if x[0] != 'sctp'])
             want = set([(p[2], p[1]) for p in want])
             have = set([(p[2], p[1]) for p in have])
             if want != have:
                 return self.want.profiles
 
+    @property
+    def ip_protocol(self):
+        if self.want.ip_protocol != self.have.ip_protocol:
+            return self.want.ip_protocol
+
     @property
     def fallback_persistence_profile(self):
         if self.want.fallback_persistence_profile is None:
@@ -1295,13 +2369,17 @@ class Difference(object):
         if self.want.default_persistence_profile == '' and self.have.default_persistence_profile is None:
             return None
         if self.have.default_persistence_profile is None:
-            return [self.want.default_persistence_profile]
+            return dict(
+                default_persistence_profile=self.want.default_persistence_profile
+            )
         w_name = self.want.default_persistence_profile.get('name', None)
         w_partition = self.want.default_persistence_profile.get('partition', None)
         h_name = self.have.default_persistence_profile.get('name', None)
         h_partition = self.have.default_persistence_profile.get('partition', None)
         if w_name != h_name or w_partition != h_partition:
-            return [self.want.default_persistence_profile]
+            return dict(
+                default_persistence_profile=self.want.default_persistence_profile
+            )
 
     @property
     def policies(self):
@@ -1383,12 +2461,32 @@ class Difference(object):
         result = self._diff_complex_items(self.want.metadata, self.have.metadata)
         return result
 
+    @property
+    def type(self):
+        if self.want.type != self.have.type:
+            raise F5ModuleError(
+                "Changing the 'type' parameter is not supported."
+            )
+
+    @property
+    def security_log_profiles(self):
+        if self.want.security_log_profiles is None:
+            return None
+        if self.have.security_log_profiles is None and self.want.security_log_profiles == '':
+            return None
+        if self.have.security_log_profiles is not None and self.want.security_log_profiles == '':
+            return []
+        if self.have.security_log_profiles is None:
+            return self.want.security_log_profiles
+        if set(self.want.security_log_profiles) != set(self.have.security_log_profiles):
+            return self.want.security_log_profiles
+
 
 class ModuleManager(object):
     def __init__(self, *args, **kwargs):
         self.module = kwargs.get('module', None)
         self.client = kwargs.get('client', None)
-        self.have = ApiParameters()
+        self.have = ApiParameters(client=self.client)
         self.want = ModuleParameters(client=self.client, params=self.module.params)
         self.changes = UsableChanges()
 
@@ -1409,17 +2507,8 @@ class ModuleManager(object):
         changes = reportable.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.module.deprecate(
-                msg=warning['msg'],
-                version=warning['version']
-            )
-
     def present(self):
         if self.exists():
             return self.update()
@@ -1433,6 +2522,9 @@ class ModuleManager(object):
 
     def update(self):
         self.have = self.read_current_from_device()
+        validator = VirtualServerValidator(module=self.module, client=self.client, have=self.have, want=self.want)
+        validator.check_update()
+
         if not self.should_update():
             return False
         if self.module.check_mode:
@@ -1492,38 +2584,10 @@ class ModuleManager(object):
         return result
 
     def create(self):
-        required_resources = ['destination', 'port']
+        validator = VirtualServerValidator(module=self.module, client=self.client, have=self.have, want=self.want)
+        validator.check_create()
 
         self._set_changed_options()
-        # This must be changed back to a list to make a valid REST API
-        # value. The module manipulates this as a normal dictionary
-        if self.want.default_persistence_profile is not None:
-            self.want.update({'default_persistence_profile': [self.want.default_persistence_profile]})
-
-        if self.want.destination is None:
-            raise F5ModuleError(
-                "'destination' must be specified when creating a virtual server"
-            )
-        if all(getattr(self.want, v) is None for v in required_resources):
-            raise F5ModuleError(
-                "You must specify both of " + ', '.join(required_resources)
-            )
-        if self.want.enabled_vlans is not None:
-            if any(x for x in self.want.enabled_vlans if x.lower() in ['/common/all', 'all']):
-                self.want.update(
-                    dict(
-                        enabled_vlans=[],
-                        vlans_disabled=True,
-                        vlans_enabled=False
-                    )
-                )
-        if self.want.source and self.want.destination:
-            want = netaddr.IPNetwork(self.want.source)
-            have = netaddr.IPNetwork(self.want.destination_tuple.ip)
-            if want.version != have.version:
-                raise F5ModuleError(
-                    "The source and destination addresses for the virtual server must be be the same type (IPv4 or IPv6)."
-                )
         if self.module.check_mode:
             return True
         self.create_on_device()
@@ -1549,11 +2613,11 @@ class ModuleManager(object):
         )
         params = result.attrs
         params.update(dict(kind=result.to_dict().get('kind', None)))
-        result = ApiParameters(params=params)
+        result = ApiParameters(params=params, client=self.client)
         return result
 
     def create_on_device(self):
-        params = self.want.api_params()
+        params = self.changes.api_params()
         self.client.api.tm.ltm.virtuals.virtual.create(
             name=self.want.name,
             partition=self.want.partition,
@@ -1584,14 +2648,12 @@ class ArgumentSpec(object):
             destination=dict(
                 aliases=['address', 'ip']
             ),
-            port=dict(
-                type='int'
-            ),
+            port=dict(),
             profiles=dict(
                 type='list',
                 aliases=['all_profiles'],
                 options=dict(
-                    name=dict(required=False),
+                    name=dict(),
                     context=dict(default='all', choices=['all', 'server-side', 'client-side'])
                 )
             ),
@@ -1619,7 +2681,26 @@ class ArgumentSpec(object):
             partition=dict(
                 default='Common',
                 fallback=(env_fallback, ['F5_PARTITION'])
-            )
+            ),
+            address_translation=dict(type='bool'),
+            port_translation=dict(type='bool'),
+            ip_protocol=dict(
+                choices=[
+                    'ah', 'bna', 'esp', 'etherip', 'gre', 'icmp', 'ipencap', 'ipv6',
+                    'ipv6-auth', 'ipv6-crypt', 'ipv6-icmp', 'isp-ip', 'mux', 'ospf',
+                    'sctp', 'tcp', 'udp', 'udplite'
+                ]
+            ),
+            type=dict(
+                default='standard',
+                choices=[
+                    'standard', 'forwarding-ip', 'forwarding-l2', 'internal', 'message-routing',
+                    'performance-http', 'performance-l4', 'reject', 'stateless', 'dhcp'
+                ]
+            ),
+            firewall_staged_policy=dict(),
+            firewall_enforced_policy=dict(),
+            security_log_profiles=dict(type='list')
         )
         self.argument_spec = {}
         self.argument_spec.update(f5_argument_spec)
diff --git a/test/units/modules/network/f5/test_bigip_virtual_server.py b/test/units/modules/network/f5/test_bigip_virtual_server.py
index d9bb5e52c8..c2b170f638 100644
--- a/test/units/modules/network/f5/test_bigip_virtual_server.py
+++ b/test/units/modules/network/f5/test_bigip_virtual_server.py
@@ -20,10 +20,10 @@ from ansible.compat.tests.mock import patch
 from ansible.module_utils.basic import AnsibleModule
 
 try:
-    from library.bigip_virtual_server import ModuleParameters
-    from library.bigip_virtual_server import ApiParameters
-    from library.bigip_virtual_server import ModuleManager
-    from library.bigip_virtual_server import ArgumentSpec
+    from library.modules.bigip_virtual_server import ModuleParameters
+    from library.modules.bigip_virtual_server import ApiParameters
+    from library.modules.bigip_virtual_server import ModuleManager
+    from library.modules.bigip_virtual_server import ArgumentSpec
     from library.module_utils.network.f5.common import F5ModuleError
     from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
     from test.unit.modules.utils import set_module_args
@@ -338,6 +338,20 @@ class TestParameters(unittest.TestCase):
         assert p.profiles[0]['fullPath'] == '/Common/http'
         assert '/Common/net1' in p.vlans
 
+    def test_module_address_translation_enabled(self):
+        args = dict(
+            address_translation=True
+        )
+        p = ModuleParameters(params=args)
+        assert p.address_translation == 'enabled'
+
+    def test_module_address_translation_disabled(self):
+        args = dict(
+            address_translation=False
+        )
+        p = ModuleParameters(params=args)
+        assert p.address_translation == 'disabled'
+
 
 class TestManager(unittest.TestCase):
 
@@ -674,3 +688,227 @@ class TestManager(unittest.TestCase):
         assert 'context' in results['profiles'][1]
         assert results['profiles'][1]['name'] == 'clientssl'
         assert results['profiles'][1]['context'] == 'clientside'
+
+    def test_create_virtual_server_with_address_translation_bool_true(self, *args):
+        set_module_args(dict(
+            destination="10.10.10.10",
+            address_translation=True,
+            name="my-snat-pool",
+            partition="Common",
+            password="secret",
+            port="443",
+            server="localhost",
+            state="present",
+            user="admin",
+            validate_certs="no"
+        ))
+
+        module = AnsibleModule(
+            argument_spec=self.spec.argument_spec,
+            supports_check_mode=self.spec.supports_check_mode
+        )
+
+        # Override methods to force specific logic in the module to happen
+        mm = ModuleManager(module=module)
+        mm.exists = Mock(return_value=False)
+        mm.create_on_device = Mock(return_value=True)
+        results = mm.exec_module()
+
+        assert results['changed'] is True
+        assert results['address_translation'] is True
+
+    def test_create_virtual_server_with_address_translation_string_yes(self, *args):
+        set_module_args(dict(
+            destination="10.10.10.10",
+            address_translation='yes',
+            name="my-snat-pool",
+            partition="Common",
+            password="secret",
+            port="443",
+            server="localhost",
+            state="present",
+            user="admin",
+            validate_certs="no"
+        ))
+
+        module = AnsibleModule(
+            argument_spec=self.spec.argument_spec,
+            supports_check_mode=self.spec.supports_check_mode
+        )
+
+        # Override methods to force specific logic in the module to happen
+        mm = ModuleManager(module=module)
+        mm.exists = Mock(return_value=False)
+        mm.create_on_device = Mock(return_value=True)
+        results = mm.exec_module()
+
+        assert results['changed'] is True
+        assert results['address_translation'] is True
+
+    def test_create_virtual_server_with_address_translation_bool_false(self, *args):
+        set_module_args(dict(
+            destination="10.10.10.10",
+            address_translation=False,
+            name="my-snat-pool",
+            partition="Common",
+            password="secret",
+            port="443",
+            server="localhost",
+            state="present",
+            user="admin",
+            validate_certs="no"
+        ))
+
+        module = AnsibleModule(
+            argument_spec=self.spec.argument_spec,
+            supports_check_mode=self.spec.supports_check_mode
+        )
+
+        # Override methods to force specific logic in the module to happen
+        mm = ModuleManager(module=module)
+        mm.exists = Mock(return_value=False)
+        mm.create_on_device = Mock(return_value=True)
+        results = mm.exec_module()
+
+        assert results['changed'] is True
+        assert results['address_translation'] is False
+
+    def test_create_virtual_server_with_address_translation_string_no(self, *args):
+        set_module_args(dict(
+            destination="10.10.10.10",
+            address_translation='no',
+            name="my-snat-pool",
+            partition="Common",
+            password="secret",
+            port="443",
+            server="localhost",
+            state="present",
+            user="admin",
+            validate_certs="no"
+        ))
+
+        module = AnsibleModule(
+            argument_spec=self.spec.argument_spec,
+            supports_check_mode=self.spec.supports_check_mode
+        )
+
+        # Override methods to force specific logic in the module to happen
+        mm = ModuleManager(module=module)
+        mm.exists = Mock(return_value=False)
+        mm.create_on_device = Mock(return_value=True)
+        results = mm.exec_module()
+
+        assert results['changed'] is True
+        assert results['address_translation'] is False
+
+    def test_create_virtual_server_with_port_translation_bool_true(self, *args):
+        set_module_args(dict(
+            destination="10.10.10.10",
+            port_translation=True,
+            name="my-snat-pool",
+            partition="Common",
+            password="secret",
+            port="443",
+            server="localhost",
+            state="present",
+            user="admin",
+            validate_certs="no"
+        ))
+
+        module = AnsibleModule(
+            argument_spec=self.spec.argument_spec,
+            supports_check_mode=self.spec.supports_check_mode
+        )
+
+        # Override methods to force specific logic in the module to happen
+        mm = ModuleManager(module=module)
+        mm.exists = Mock(return_value=False)
+        mm.create_on_device = Mock(return_value=True)
+        results = mm.exec_module()
+
+        assert results['changed'] is True
+        assert results['port_translation'] is True
+
+    def test_create_virtual_server_with_port_translation_string_yes(self, *args):
+        set_module_args(dict(
+            destination="10.10.10.10",
+            port_translation='yes',
+            name="my-snat-pool",
+            partition="Common",
+            password="secret",
+            port="443",
+            server="localhost",
+            state="present",
+            user="admin",
+            validate_certs="no"
+        ))
+
+        module = AnsibleModule(
+            argument_spec=self.spec.argument_spec,
+            supports_check_mode=self.spec.supports_check_mode
+        )
+
+        # Override methods to force specific logic in the module to happen
+        mm = ModuleManager(module=module)
+        mm.exists = Mock(return_value=False)
+        mm.create_on_device = Mock(return_value=True)
+        results = mm.exec_module()
+
+        assert results['changed'] is True
+        assert results['port_translation'] is True
+
+    def test_create_virtual_server_with_port_translation_bool_false(self, *args):
+        set_module_args(dict(
+            destination="10.10.10.10",
+            port_translation=False,
+            name="my-snat-pool",
+            partition="Common",
+            password="secret",
+            port="443",
+            server="localhost",
+            state="present",
+            user="admin",
+            validate_certs="no"
+        ))
+
+        module = AnsibleModule(
+            argument_spec=self.spec.argument_spec,
+            supports_check_mode=self.spec.supports_check_mode
+        )
+
+        # Override methods to force specific logic in the module to happen
+        mm = ModuleManager(module=module)
+        mm.exists = Mock(return_value=False)
+        mm.create_on_device = Mock(return_value=True)
+        results = mm.exec_module()
+
+        assert results['changed'] is True
+        assert results['port_translation'] is False
+
+    def test_create_virtual_server_with_port_translation_string_no(self, *args):
+        set_module_args(dict(
+            destination="10.10.10.10",
+            port_translation='no',
+            name="my-snat-pool",
+            partition="Common",
+            password="secret",
+            port="443",
+            server="localhost",
+            state="present",
+            user="admin",
+            validate_certs="no"
+        ))
+
+        module = AnsibleModule(
+            argument_spec=self.spec.argument_spec,
+            supports_check_mode=self.spec.supports_check_mode
+        )
+
+        # Override methods to force specific logic in the module to happen
+        mm = ModuleManager(module=module)
+        mm.exists = Mock(return_value=False)
+        mm.create_on_device = Mock(return_value=True)
+        results = mm.exec_module()
+
+        assert results['changed'] is True
+        assert results['port_translation'] is False