diff --git a/.zuul.yaml b/.zuul.yaml
index c29417b77175bf82e82775aedff241397b1b8089..a1a2c63b9ad6e06fd1b3774fa7619383523e761e 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -67,6 +67,7 @@
         auth
         catalog_service
         client_config
+        compute_flavor
         dns
         dns_zone_info
         endpoint
@@ -89,7 +90,6 @@
         logging
         loadbalancer
         network
-        nova_flavor
         nova_services
         object
         object_container
diff --git a/ci/roles/compute_flavor/defaults/main.yml b/ci/roles/compute_flavor/defaults/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..59a058a838c3272d0cd67071eba90224f1a1e761
--- /dev/null
+++ b/ci/roles/compute_flavor/defaults/main.yml
@@ -0,0 +1,14 @@
+expected_fields:
+  - description
+  - disk
+  - ephemeral
+  - extra_specs
+  - id
+  - is_disabled
+  - is_public
+  - name
+  - original_name
+  - ram
+  - rxtx_factor
+  - swap
+  - vcpus
diff --git a/ci/roles/compute_flavor/tasks/main.yml b/ci/roles/compute_flavor/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..72b6adb592d26a2a62bb82bf28ca1faf275b72c9
--- /dev/null
+++ b/ci/roles/compute_flavor/tasks/main.yml
@@ -0,0 +1,254 @@
+---
+- name: Delete resources before tests
+  openstack.cloud.compute_flavor:
+    cloud: "{{ cloud }}"
+    state: absent
+    name: "{{ item }}"
+  loop:
+    - ansible_public_flavor
+    - ansible_private_flavor
+    - ansible_extra_specs_flavor
+    - ansible_defaults_flavor
+
+- name: Create public flavor
+  openstack.cloud.compute_flavor:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_public_flavor
+    is_public: True
+    ram: 1024
+    vcpus: 1
+    disk: 10
+    ephemeral: 10
+    swap: 1
+    id: 12345
+  register: result
+
+- assert:
+    that: item in result.flavor
+  loop: "{{ expected_fields }}"
+
+- name: Assert changed
+  assert:
+    that: result is changed
+
+- name: Create public flavor again
+  openstack.cloud.compute_flavor:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_public_flavor
+    is_public: True
+    ram: 1024
+    vcpus: 1
+    disk: 10
+    ephemeral: 10
+    swap: 1
+    id: 12345
+  register: result
+
+- name: Assert not changed
+  assert:
+    that: result is not changed
+
+- name: Delete public flavor
+  openstack.cloud.compute_flavor:
+    cloud: "{{ cloud }}"
+    state: absent
+    name: ansible_public_flavor
+  register: result
+
+- name: Assert changed
+  assert:
+    that: result is changed
+
+- name: Delete public flavor again
+  openstack.cloud.compute_flavor:
+    cloud: "{{ cloud }}"
+    state: absent
+    name: ansible_public_flavor
+  register: result
+
+- name: Assert not changed
+  assert:
+    that: result is not changed
+
+- name: Create private flavor
+  openstack.cloud.compute_flavor:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_private_flavor
+    is_public: False
+    ram: 1024
+    vcpus: 1
+    disk: 10
+    ephemeral: 10
+    swap: 1
+    id: 12345
+
+- name: Delete private flavor
+  openstack.cloud.compute_flavor:
+    cloud: "{{ cloud }}"
+    state: absent
+    name: ansible_private_flavor
+
+- name: Create flavor (defaults)
+  openstack.cloud.compute_flavor:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_defaults_flavor
+    ram: 1024
+    vcpus: 1
+    disk: 10
+  register: result
+
+- name: Assert changed
+  assert:
+    that: result is changed
+
+- name: Create flavor (defaults) again
+  openstack.cloud.compute_flavor:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_defaults_flavor
+    ram: 1024
+    vcpus: 1
+    disk: 10
+  register: result
+
+- name: Assert not changed
+  assert:
+    that: result is not changed
+
+- name: Delete flavor (defaults)
+  openstack.cloud.compute_flavor:
+    cloud: "{{ cloud }}"
+    state: absent
+    name: ansible_defaults_flavor
+
+- name: Create flavor (extra_specs)
+  openstack.cloud.compute_flavor:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_extra_specs_flavor
+    ram: 1024
+    vcpus: 1
+    disk: 10
+    extra_specs:
+      "os:secure_boot": "required"
+  register: result
+
+- name: Assert returned value
+  assert:
+    that:
+      - result is changed
+      - result.flavor.extra_specs['os:secure_boot'] == 'required'
+
+- name: Create flavor (extra_specs) again
+  openstack.cloud.compute_flavor:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_extra_specs_flavor
+    ram: 1024
+    vcpus: 1
+    disk: 10
+    extra_specs:
+      "os:secure_boot": "required"
+  register: result
+
+- name: Assert not changed
+  assert:
+    that: result is not changed
+
+- name: Change extra_specs value
+  openstack.cloud.compute_flavor:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_extra_specs_flavor
+    ram: 1024
+    vcpus: 1
+    disk: 10
+    extra_specs:
+      "os:secure_boot": "disabled"
+  register: result
+
+- name: Assert returned value
+  assert:
+    that:
+      - result is changed
+      - result.flavor.extra_specs['os:secure_boot'] == 'disabled'
+
+- name: Append extra_specs value
+  openstack.cloud.compute_flavor:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_extra_specs_flavor
+    ram: 1024
+    vcpus: 1
+    disk: 10
+    extra_specs:
+      "os:secure_boot": "disabled"
+      "hw_video:ram_max_mb": 200
+  register: result
+
+- name: Assert returned value
+  assert:
+    that:
+      - result is changed
+      - result.flavor.extra_specs | length == 2
+      - "'os:secure_boot' in result.flavor.extra_specs"
+      - "'hw_video:ram_max_mb' in result.flavor.extra_specs"
+
+- name: Drop extra_specs value
+  openstack.cloud.compute_flavor:
+    cloud: "{{ cloud }}"
+    state: present
+    name: ansible_extra_specs_flavor
+    ram: 1024
+    vcpus: 1
+    disk: 10
+    extra_specs:
+      "hw_video:ram_max_mb": 200
+  register: result
+
+- name: Assert returned value
+  assert:
+    that:
+      - result is changed
+      - result.flavor.extra_specs | length == 1
+      - "'hw_video:ram_max_mb' in result.flavor.extra_specs"
+
+- name: Assert changed
+  assert:
+    that: result is changed
+
+- name: Clean up
+  openstack.cloud.compute_flavor:
+    cloud: "{{ cloud }}"
+    state: absent
+    name: "{{ item }}"
+  loop:
+    - ansible_public_flavor
+    - ansible_private_flavor
+    - ansible_extra_specs_flavor
+    - ansible_defaults_flavor
+
+- name: List flavors
+  openstack.cloud.compute_flavor_info:
+    cloud: "{{ cloud }}"
+  register: flavor_info
+
+- assert:
+    that: item in flavor_info.openstack_flavors[0]
+  loop: "{{ expected_fields }}"
+
+- name: List flavors with filter
+  openstack.cloud.compute_flavor_info:
+    cloud: "{{ cloud }}"
+    name: "m1.tiny"
+  register: flavor_name
+
+- name: Check output of list flavors with filter
+  assert:
+    that:
+      - flavor_name.openstack_flavors | length == 1
+
diff --git a/ci/roles/compute_flavor_info/tasks/main.yml b/ci/roles/compute_flavor_info/tasks/main.yml
deleted file mode 100644
index 982d0c297460c431f021acc7b7ddfa17ac644663..0000000000000000000000000000000000000000
--- a/ci/roles/compute_flavor_info/tasks/main.yml
+++ /dev/null
@@ -1,33 +0,0 @@
----
-- name: List flavors
-  openstack.cloud.compute_flavor_info:
-    cloud: "{{ cloud }}"
-
-- name: List flavors with filter
-  openstack.cloud.compute_flavor_info:
-    cloud: "{{ cloud }}"
-    name: "m1.tiny"
-  register: flavor_name
-
-- name: Check output of list flavors with filter
-  assert:
-    that:
-      - flavor_name.openstack_flavors | length == 1
-
-- name: Assert fields on SDK 0.*
-  when: sdk_version is version(1.0, '<')
-  assert:
-    that:
-      - '["name", "description", "disk", "is_public", "ram",
-          "vcpus", "swap", "ephemeral", "is_disabled", "rxtx_factor", "id",
-          "extra_specs"] | difference(flavor_name.openstack_flavors.0.keys())
-          | length == 0'
-
-- name: Assert fields on SDK 1.*
-  when: sdk_version is version(1.0, '>=')
-  assert:
-    that:
-      - '["name", "original_name", "description", "disk", "is_public", "ram",
-          "vcpus", "swap", "ephemeral", "is_disabled", "rxtx_factor", "id",
-          "extra_specs"] | difference(flavor_name.openstack_flavors.0.keys())
-          | length == 0'
diff --git a/ci/roles/nova_flavor/tasks/main.yml b/ci/roles/nova_flavor/tasks/main.yml
deleted file mode 100644
index 97dd1904cbe6de14ba8941ac7f44ec35ddf69cd6..0000000000000000000000000000000000000000
--- a/ci/roles/nova_flavor/tasks/main.yml
+++ /dev/null
@@ -1,53 +0,0 @@
----
-- name: Create public flavor
-  openstack.cloud.compute_flavor:
-     cloud: "{{ cloud }}"
-     state: present
-     name: ansible_public_flavor
-     is_public: True
-     ram: 1024
-     vcpus: 1
-     disk: 10
-     ephemeral: 10
-     swap: 1
-     flavorid: 12345
-
-- name: Delete public flavor
-  openstack.cloud.compute_flavor:
-     cloud: "{{ cloud }}"
-     state: absent
-     name: ansible_public_flavor
-
-- name: Create private flavor
-  openstack.cloud.compute_flavor:
-     cloud: "{{ cloud }}"
-     state: present
-     name: ansible_private_flavor
-     is_public: False
-     ram: 1024
-     vcpus: 1
-     disk: 10
-     ephemeral: 10
-     swap: 1
-     flavorid: 12345
-
-- name: Delete private flavor
-  openstack.cloud.compute_flavor:
-     cloud: "{{ cloud }}"
-     state: absent
-     name: ansible_private_flavor
-
-- name: Create flavor (defaults)
-  openstack.cloud.compute_flavor:
-     cloud: "{{ cloud }}"
-     state: present
-     name: ansible_defaults_flavor
-     ram: 1024
-     vcpus: 1
-     disk: 10
-
-- name: Delete flavor (defaults)
-  openstack.cloud.compute_flavor:
-     cloud: "{{ cloud }}"
-     state: absent
-     name: ansible_defaults_flavor
diff --git a/ci/run-collection.yml b/ci/run-collection.yml
index 48edfbeec230484ac3b664dfc4da5ef273b9aee1..ed5efeca4d6682a6c4fca45d98a7c66c214f513a 100644
--- a/ci/run-collection.yml
+++ b/ci/run-collection.yml
@@ -8,6 +8,7 @@
     - { role: auth, tags: auth }
     - { role: catalog_service, tags: catalog_service }
     - { role: client_config, tags: client_config }
+    - { role: compute_flavor, tags: compute_flavor }
     - { role: dns_zone_info, tags: dns_zone_info }
     - role: object_container
       tags: object_container
@@ -44,9 +45,6 @@
       tags:
         - rbac
         - neutron_rbac
-    - { role: nova_flavor, tags: nova_flavor }
-    - role: compute_flavor_info
-      tags: nova_flavor
     - role: nova_services
       tags: nova_services
       when: sdk_version is version(0.44, '>=')
diff --git a/plugins/modules/compute_flavor.py b/plugins/modules/compute_flavor.py
index a27f7c480ce75e1162641a8c089afd1e38498dd9..338382bfab72985448581167b2e66394302ef618 100644
--- a/plugins/modules/compute_flavor.py
+++ b/plugins/modules/compute_flavor.py
@@ -12,72 +12,74 @@ author: OpenStack Ansible SIG
 description:
    - Add or remove flavors from OpenStack.
 options:
-   state:
-     description:
-        - Indicate desired state of the resource. When I(state) is 'present',
-          then I(ram), I(vcpus), and I(disk) are all required. There are no
-          default values for those parameters.
-     choices: ['present', 'absent']
-     default: present
-     type: str
-   name:
-     description:
-        - Flavor name.
-     required: true
-     type: str
-   ram:
-     description:
-        - Amount of memory, in MB.
-     type: int
-   vcpus:
-     description:
-        - Number of virtual CPUs.
-     type: int
-   disk:
-     description:
-        - Size of local disk, in GB.
-     default: 0
-     type: int
-   ephemeral:
-     description:
-        - Ephemeral space size, in GB.
-     default: 0
-     type: int
-   swap:
-     description:
-        - Swap space size, in MB.
-     default: 0
-     type: int
-   rxtx_factor:
-     description:
-        - RX/TX factor.
-     default: 1.0
-     type: float
-   is_public:
-     description:
-        - Make flavor accessible to the public.
-     type: bool
-     default: 'yes'
-   flavorid:
-     description:
-        - ID for the flavor. This is optional as a unique UUID will be
-          assigned if a value is not specified.
-     default: "auto"
-     type: str
-   extra_specs:
-     description:
-        - Metadata dictionary
-     type: dict
+  state:
+    description:
+       - Indicate desired state of the resource. When I(state) is 'present',
+         then I(ram), I(vcpus), and I(disk) are all required. There are no
+         default values for those parameters.
+    choices: ['present', 'absent']
+    default: present
+    type: str
+  name:
+    description:
+       - Flavor name.
+    required: true
+    type: str
+  ram:
+    description:
+       - Amount of memory, in MB.
+    type: int
+  vcpus:
+    description:
+       - Number of virtual CPUs.
+    type: int
+  disk:
+    description:
+       - Size of local disk, in GB.
+    default: 0
+    type: int
+  ephemeral:
+    description:
+       - Ephemeral space size, in GB.
+    default: 0
+    type: int
+  swap:
+    description:
+       - Swap space size, in MB.
+    default: 0
+    type: int
+  rxtx_factor:
+    description:
+       - RX/TX factor.
+    default: 1.0
+    type: float
+  is_public:
+    description:
+       - Make flavor accessible to the public.
+    type: bool
+    default: 'yes'
+  id:
+    description:
+       - ID for the flavor. This is optional as a unique UUID will be
+         assigned if a value is not specified.
+       - Note that this ID will only be used when first creating the flavor.
+    default: "auto"
+    type: str
+    aliases: ['flavorid']
+  extra_specs:
+    description:
+       - Metadata dictionary
+    type: dict
 requirements:
-    - "python >= 3.6"
-    - "openstacksdk"
+   - "python >= 3.6"
+   - "openstacksdk"
 
 extends_documentation_fragment:
 - openstack.cloud.openstack
 '''
 
 EXAMPLES = '''
-- name: "Create 'tiny' flavor with 1024MB of RAM, 1 virtual CPU, and 10GB of local disk, and 10GB of ephemeral."
+- name: Create tiny flavor with 1024MB RAM, 1 vCPU, 10GB disk, 10GB ephemeral
   openstack.cloud.compute_flavor:
     cloud: mycloud
     state: present
@@ -87,7 +89,7 @@ EXAMPLES = '''
     disk: 10
     ephemeral: 10
 
-- name: "Delete 'tiny' flavor"
+- name: Delete tiny flavor
   openstack.cloud.compute_flavor:
     cloud: mycloud
     state: absent
@@ -108,57 +110,77 @@ EXAMPLES = '''
 
 RETURN = '''
 flavor:
-    description: Dictionary describing the flavor.
-    returned: On success when I(state) is 'present'
-    type: complex
-    contains:
-        id:
-            description: Flavor ID.
-            returned: success
-            type: str
-            sample: "515256b8-7027-4d73-aa54-4e30a4a4a339"
-        name:
-            description: Flavor name.
-            returned: success
-            type: str
-            sample: "tiny"
-        disk:
-            description: Size of local disk, in GB.
-            returned: success
-            type: int
-            sample: 10
-        ephemeral:
-            description: Ephemeral space size, in GB.
-            returned: success
-            type: int
-            sample: 10
-        ram:
-            description: Amount of memory, in MB.
-            returned: success
-            type: int
-            sample: 1024
-        swap:
-            description: Swap space size, in MB.
-            returned: success
-            type: int
-            sample: 100
-        vcpus:
-            description: Number of virtual CPUs.
-            returned: success
-            type: int
-            sample: 2
-        is_public:
-            description: Make flavor accessible to the public.
-            returned: success
-            type: bool
-            sample: true
-        extra_specs:
-            description: Flavor metadata
-            returned: success
-            type: dict
-            sample:
-                "quota:disk_read_iops_sec": 5000
-                "aggregate_instance_extra_specs:pinned": false
+  description: Dictionary describing the flavor.
+  returned: On success when I(state) is 'present'
+  type: dict
+  contains:
+    description:
+      description: Description attached to flavor
+      returned: success
+      type: str
+      sample: Example description
+    disk:
+      description: Size of local disk, in GB.
+      returned: success
+      type: int
+      sample: 10
+    ephemeral:
+      description: Ephemeral space size, in GB.
+      returned: success
+      type: int
+      sample: 10
+    extra_specs:
+      description: Flavor metadata
+      returned: success
+      type: dict
+      sample:
+        "quota:disk_read_iops_sec": 5000
+        "aggregate_instance_extra_specs:pinned": false
+    id:
+      description: Flavor ID.
+      returned: success
+      type: str
+      sample: "515256b8-7027-4d73-aa54-4e30a4a4a339"
+    is_disabled:
+      description: Whether the flavor is disabled
+      returned: success
+      type: bool
+      sample: true
+    is_public:
+      description: Make flavor accessible to the public.
+      returned: success
+      type: bool
+      sample: true
+    name:
+      description: Flavor name.
+      returned: success
+      type: str
+      sample: "tiny"
+    original_name:
+      description: The name of this flavor when returned by server list/show
+      type: str
+      returned: success
+    ram:
+      description: Amount of memory, in MB.
+      returned: success
+      type: int
+      sample: 1024
+    rxtx_factor:
+      description: |
+        The bandwidth scaling factor this flavor receives on the network
+      returned: success
+      type: int
+      sample: 100
+    swap:
+      description: Swap space size, in MB.
+      returned: success
+      type: int
+      sample: 100
+    vcpus:
+      description: Number of virtual CPUs.
+      returned: success
+      type: int
+      sample: 2
 '''
 
 from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule
@@ -179,7 +201,7 @@ class ComputeFlavorModule(OpenStackModule):
         swap=dict(default=0, type='int'),
         rxtx_factor=dict(default=1.0, type='float'),
         is_public=dict(default=True, type='bool'),
-        flavorid=dict(default="auto"),
+        id=dict(default='auto', aliases=['flavorid']),
         extra_specs=dict(type='dict'),
     )
 
@@ -190,45 +212,57 @@ class ComputeFlavorModule(OpenStackModule):
         supports_check_mode=True
     )
 
-    def _system_state_change(self, flavor):
+    def _system_state_change(self, flavor, extra_specs, old_extra_specs):
         state = self.params['state']
-        if state == 'present' and not flavor:
-            return True
+        if state == 'present':
+            if not flavor:
+                return True
+            return self._needs_update(flavor) or extra_specs != old_extra_specs
         if state == 'absent' and flavor:
             return True
         return False
 
+    def _needs_update(self, flavor):
+        fields = ['ram', 'vcpus', 'disk', 'ephemeral', 'swap', 'rxtx_factor',
+                  'is_public']
+        for k in fields:
+            if self.params[k] is not None and self.params[k] != flavor[k]:
+                self.debug['diff'] = (k, self.params[k], flavor[k])
+                return True
+
+    def _build_flavor_specs_diff(self, extra_specs, old_extra_specs):
+        new_extra_specs = dict([(k, str(v)) for k, v in extra_specs.items()])
+        unset_keys = set(old_extra_specs.keys()) - set(extra_specs.keys())
+        return new_extra_specs, unset_keys
+
     def run(self):
         state = self.params['state']
         name = self.params['name']
         extra_specs = self.params['extra_specs'] or {}
 
-        flavor = self.conn.get_flavor(name)
+        flavor = self.conn.compute.find_flavor(name, get_extra_specs=True)
+        old_extra_specs = {}
+        if flavor:
+            old_extra_specs = flavor['extra_specs']
+            if flavor['swap'] == '':
+                flavor['swap'] = 0
 
         if self.ansible.check_mode:
-            self.exit_json(changed=self._system_state_change(flavor))
+            self.exit_json(changed=self._system_state_change(
+                flavor, extra_specs, old_extra_specs))
 
         if state == 'present':
-            old_extra_specs = {}
-            require_update = False
-
-            if flavor:
-                old_extra_specs = flavor['extra_specs']
-                if flavor['swap'] == "":
-                    flavor['swap'] = 0
-                for param_key in ['ram', 'vcpus', 'disk', 'ephemeral',
-                                  'swap', 'rxtx_factor', 'is_public']:
-                    if self.params[param_key] != flavor[param_key]:
-                        require_update = True
-                        break
-            flavorid = self.params['flavorid']
-            if flavor and require_update:
-                self.conn.delete_flavor(name)
+            flavorid = self.params['id']
+            if flavor and self._needs_update(flavor):
+                # Because only flavor descriptions are updateable, we have to
+                # delete and recreate a flavor to "update" it
+                self.conn.compute.delete_flavor(flavor)
                 old_extra_specs = {}
                 if flavorid == 'auto':
                     flavorid = flavor['id']
                 flavor = None
 
+            changed = False
             if not flavor:
                 flavor = self.conn.create_flavor(
                     name=name,
@@ -242,26 +276,27 @@ class ComputeFlavorModule(OpenStackModule):
                     is_public=self.params['is_public']
                 )
                 changed = True
-            else:
-                changed = False
 
-            new_extra_specs = dict([(k, str(v)) for k, v in extra_specs.items()])
-            unset_keys = set(old_extra_specs.keys()) - set(extra_specs.keys())
+            new_extra_specs, unset_keys = self._build_flavor_specs_diff(
+                extra_specs, old_extra_specs)
 
-            if unset_keys and not require_update:
+            if unset_keys:
                 self.conn.unset_flavor_specs(flavor['id'], unset_keys)
 
             if old_extra_specs != new_extra_specs:
-                self.conn.set_flavor_specs(flavor['id'], extra_specs)
+                self.conn.compute.create_flavor_extra_specs(
+                    flavor['id'], extra_specs)
+                changed = True
 
-            changed = (changed or old_extra_specs != new_extra_specs)
+            # Have to refetch updated extra_specs
+            flavor = self.conn.compute.fetch_flavor_extra_specs(flavor)
 
             self.exit_json(
-                changed=changed, flavor=flavor, id=flavor['id'])
+                changed=changed, flavor=flavor.to_dict(computed=False))
 
         elif state == 'absent':
             if flavor:
-                self.conn.delete_flavor(name)
+                self.conn.compute.delete_flavor(flavor)
                 self.exit_json(changed=True)
             self.exit_json(changed=False)