diff --git a/.gitreview b/.gitreview index c2b7eef7078..23383d4f695 100644 --- a/.gitreview +++ b/.gitreview @@ -2,3 +2,4 @@ host=review.opendev.org port=29418 project=openstack/nova.git +defaultbranch=unmaintained/yoga diff --git a/.zuul.yaml b/.zuul.yaml index 4c1f0c442c6..c76f430707a 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -2,13 +2,12 @@ # for job naming conventions. - job: - name: nova-tox-functional-centos8-py36 - parent: openstack-tox-functional-py36 - nodeset: devstack-single-node-centos-8-stream + name: nova-tox-functional-py38 + parent: openstack-tox-functional-py38 description: | Run tox-based functional tests for the OpenStack Nova project - under cPython version 3.6 with Nova specific irrelevant-files list. - Uses tox with the ``functional-py36`` environment. + under cPython version 3.8 with Nova specific irrelevant-files list. + Uses tox with the ``functional-py38`` environment. This job also provides a parent for other projects to run the nova functional tests on their own changes. @@ -22,28 +21,6 @@ - ^doc/(source|test)/.*$ - ^nova/locale/.*$ - ^releasenotes/.*$ - vars: - # explicitly stating the work dir makes this job reusable by other - # projects - zuul_work_dir: src/opendev.org/openstack/nova - bindep_profile: test py36 - timeout: 3600 - -- job: - name: nova-tox-functional-py38 - parent: openstack-tox-functional-py38 - description: | - Run tox-based functional tests for the OpenStack Nova project - under cPython version 3.8 with Nova specific irrelevant-files list. - Uses tox with the ``functional-py38`` environment. - - This job also provides a parent for other projects to run the nova - functional tests on their own changes. - required-projects: - # including nova here makes this job reusable by other projects - - openstack/nova - - openstack/placement - irrelevant-files: *functional-irrelevant-files vars: # explicitly stating the work dir makes this job reusable by other # projects @@ -90,7 +67,7 @@ description: | Run tempest live migration tests against local qcow2 ephemeral storage and shared LVM/iSCSI cinder volumes. - irrelevant-files: &nova-base-irrelevant-files + irrelevant-files: - ^api-.*$ - ^(test-|)requirements.txt$ - ^.*\.rst$ @@ -101,6 +78,7 @@ - ^nova/policies/.*$ - ^nova/tests/.*$ - ^nova/test.py$ + - ^nova/virt/ironic/.*$ - ^releasenotes/.*$ - ^setup.cfg$ - ^tools/.*$ @@ -130,7 +108,21 @@ the "iptables_hybrid" securitygroup firewall driver, aka "hybrid plug". The external events interactions between Nova and Neutron in these situations has historically been fragile. This job exercises them. - irrelevant-files: *nova-base-irrelevant-files + irrelevant-files: &nova-base-irrelevant-files + - ^api-.*$ + - ^(test-|)requirements.txt$ + - ^.*\.rst$ + - ^.git.*$ + - ^doc/.*$ + - ^nova/hacking/.*$ + - ^nova/locale/.*$ + - ^nova/policies/.*$ + - ^nova/tests/.*$ + - ^nova/test.py$ + - ^releasenotes/.*$ + - ^setup.cfg$ + - ^tools/.*$ + - ^tox.ini$ vars: tox_envlist: all tempest_test_regex: (^tempest\..*compute\..*(migration|resize|reboot).*) @@ -205,24 +197,11 @@ parent: devstack-tempest description: | Run tempest compute API tests using LVM image backend. This only runs - against nova/virt/libvirt/* changes. - # Copy irrelevant-files from nova-dsvm-multinode-base and then exclude - # anything that is not in nova/virt/libvirt/* or nova/privsep/*. - irrelevant-files: - - ^(?!.zuul.yaml)(?!nova/virt/libvirt/)(?!nova/privsep/).*$ - - ^api-.*$ - - ^(test-|)requirements.txt$ - - ^.*\.rst$ - - ^.git.*$ - - ^doc/.*$ - - ^nova/hacking/.*$ - - ^nova/locale/.*$ - - ^nova/tests/.*$ - - ^nova/test.py$ - - ^releasenotes/.*$ - - ^setup.cfg$ - - ^tools/.*$ - - ^tox.ini$ + against nova/virt/libvirt/*, nova/privsep/* and .zuul.yaml changes. + files: + - ^nova/virt/libvirt/.*$ + - ^nova/privsep/.*$ + - .zuul.yaml vars: # We use the "all" environment for tempest_test_regex and # tempest_exclude_regex. @@ -243,8 +222,6 @@ NOVA_BACKEND: LVM # Do not waste time clearing volumes. LVM_VOLUME_CLEAR: none - # Disable SSH validation in tests to save time. - TEMPEST_RUN_VALIDATION: false # Increase the size of the swift loopback device to accommodate RAW # snapshots from the LV based instance disks. # See bug #1913451 for more details. @@ -263,22 +240,11 @@ # NOTE(chateaulav): due to constraints with no IDE support for aarch64, # tests have been limited to eliminate any items that are incompatible. # This is to be re-evaluated as greater support is added and defined. - irrelevant-files: - - ^(?!.zuul.yaml)(?!nova/virt/libvirt/)(?!nova/objects/)(?!nova/scheduler/).*$ - - ^api-.*$ - - ^(test-|)requirements.txt$ - - ^.*\.rst$ - - ^.git.*$ - - ^doc/.*$ - - ^nova/hacking/.*$ - - ^nova/locale/.*$ - - ^nova/policies/.*$ - - ^nova/tests/.*$ - - ^nova/test.py$ - - ^releasenotes/.*$ - - ^setup.cfg$ - - ^tools/.*$ - - ^tox.ini$ + files: + - ^nova/virt/libvirt/.*$ + - ^nova/objects/.*$ + - ^nova/scheduler/.*$ + - .zuul.yaml vars: tox_envlist: all tempest_test_regex: ^tempest\.(api\.compute\.servers|scenario\.test_network_basic_ops) @@ -575,9 +541,14 @@ irrelevant-files: *nova-base-irrelevant-files required-projects: - openstack/nova + - name: openstack/cinder-tempest-plugin + override-checkout: yoga-last pre-run: - playbooks/ceph/glance-copy-policy.yaml vars: + # NOTE(elod.illes): this job is breaking with the following test case on + # unmaintained/yoga, so let's just exclude it to unblock the gate + tempest_exclude_regex: test_nova_image_snapshot_dependency # NOTE(danms): These tests create an empty non-raw image, which nova # will refuse because we set never_download_image_if_on_rbd in this job. # Just skip these tests for this case. @@ -585,6 +556,16 @@ GLANCE_STANDALONE: True GLANCE_USE_IMPORT_WORKFLOW: True DEVSTACK_PARALLEL: True + MYSQL_REDUCE_MEMORY: True + # NOTE(danms): This job is pretty heavy as it is, so we disable some + # services that are not relevant to the nova-glance-ceph scenario + # that this job is intended to validate. + devstack_services: + c-bak: false + s-account: false + s-container: false + s-object: false + s-proxy: false devstack_local_conf: post-config: $NOVA_CONF: @@ -622,7 +603,6 @@ - check-requirements - integrated-gate-compute - openstack-cover-jobs - - openstack-lower-constraints-jobs - openstack-python3-yoga-jobs - openstack-python3-yoga-jobs-arm64 - periodic-stable-jobs @@ -638,21 +618,20 @@ - nova-ceph-multistore: irrelevant-files: *nova-base-irrelevant-files - neutron-linuxbridge-tempest: - irrelevant-files: + files: # NOTE(mriedem): This job has its own irrelevant-files section # so that we only run it on changes to networking and libvirt/vif # code; we don't need to run this on all changes. - - ^(?!nova/network/.*)(?!nova/virt/libvirt/vif.py).*$ + - ^nova/network/.*$ + - nova/virt/libvirt/vif.py - nova-live-migration - nova-live-migration-ceph - nova-lvm - nova-multi-cell - nova-next - nova-ovs-hybrid-plug - - nova-emulation - nova-tox-validate-backport: voting: false - - nova-tox-functional-centos8-py36 - nova-tox-functional-py38 - nova-tox-functional-py39 - tempest-integrated-compute: @@ -674,8 +653,6 @@ - ^setup.cfg$ - ^tools/.*$ - ^tox.ini$ - - grenade-skip-level: - irrelevant-files: *policies-irrelevant-files - nova-grenade-multinode: irrelevant-files: *policies-irrelevant-files - tempest-ipv6-only: @@ -688,16 +665,10 @@ - barbican-tempest-plugin-simple-crypto: irrelevant-files: *nova-base-irrelevant-files voting: false - - tempest-integrated-compute-centos-8-stream: - irrelevant-files: *nova-base-irrelevant-files - - tempest-centos8-stream-fips: - irrelevant-files: *nova-base-irrelevant-files - voting: false gate: jobs: - nova-live-migration - nova-live-migration-ceph - - nova-tox-functional-centos8-py36 - nova-tox-functional-py38 - nova-tox-functional-py39 - nova-multi-cell @@ -706,11 +677,12 @@ - nova-ceph-multistore: irrelevant-files: *nova-base-irrelevant-files - neutron-linuxbridge-tempest: - irrelevant-files: + files: # NOTE(mriedem): This job has its own irrelevant-files section # so that we only run it on changes to networking and libvirt/vif # code; we don't need to run this on all changes. - - ^(?!nova/network/.*)(?!nova/virt/libvirt/vif.py).*$ + - ^nova/network/.*$ + - nova/virt/libvirt/vif.py - tempest-integrated-compute: irrelevant-files: *policies-irrelevant-files - nova-grenade-multinode: @@ -719,8 +691,6 @@ irrelevant-files: *nova-base-irrelevant-files - openstacksdk-functional-devstack: irrelevant-files: *nova-base-irrelevant-files - - tempest-integrated-compute-centos-8-stream: - irrelevant-files: *nova-base-irrelevant-files experimental: jobs: - ironic-tempest-bfv: @@ -730,6 +700,7 @@ - devstack-plugin-nfs-tempest-full: irrelevant-files: *nova-base-irrelevant-files - nova-osprofiler-redis + - nova-emulation - tempest-pg-full: irrelevant-files: *nova-base-irrelevant-files - nova-tempest-full-oslo.versionedobjects: @@ -740,11 +711,7 @@ irrelevant-files: *nova-base-irrelevant-files - neutron-ovs-tempest-iptables_hybrid: irrelevant-files: *nova-base-irrelevant-files - - os-vif-ovs: - irrelevant-files: *nova-base-irrelevant-files - - devstack-platform-fedora-latest: - irrelevant-files: *nova-base-irrelevant-files - - devstack-platform-fedora-latest-virt-preview: + - os-vif-ovn: irrelevant-files: *nova-base-irrelevant-files - devstack-plugin-ceph-compute-local-ephemeral: irrelevant-files: *nova-base-irrelevant-files diff --git a/doc/source/admin/configuration/cross-cell-resize.rst b/doc/source/admin/configuration/cross-cell-resize.rst index e51e4257748..0c34fd13f51 100644 --- a/doc/source/admin/configuration/cross-cell-resize.rst +++ b/doc/source/admin/configuration/cross-cell-resize.rst @@ -284,7 +284,7 @@ Troubleshooting Timeouts ~~~~~~~~ -Configure a :ref:`service user ` in case the user token +Configure a :ref:`service user ` in case the user token times out, e.g. during the snapshot and download of a large server image. If RPC calls are timing out with a ``MessagingTimeout`` error in the logs, diff --git a/doc/source/admin/configuration/index.rst b/doc/source/admin/configuration/index.rst index 233597b1fe4..f5b6fde9dac 100644 --- a/doc/source/admin/configuration/index.rst +++ b/doc/source/admin/configuration/index.rst @@ -19,6 +19,7 @@ A list of config options based on different topics can be found below: .. toctree:: :maxdepth: 1 + /admin/configuration/service-user-token /admin/configuration/api /admin/configuration/resize /admin/configuration/cross-cell-resize diff --git a/doc/source/admin/configuration/service-user-token.rst b/doc/source/admin/configuration/service-user-token.rst new file mode 100644 index 00000000000..740730af1d0 --- /dev/null +++ b/doc/source/admin/configuration/service-user-token.rst @@ -0,0 +1,59 @@ +.. _service_user_token: + +=================== +Service User Tokens +=================== + +.. note:: + + Configuration of service user tokens is **required** for every Nova service + for security reasons. See https://bugs.launchpad.net/nova/+bug/2004555 for + details. + +Configure Nova to send service user tokens alongside regular user tokens when +making REST API calls to other services. The identity service (Keystone) will +authenticate a request using the service user token if the regular user token +has expired. + +This is important when long-running operations such as live migration or +snapshot take long enough to exceed the expiry of the user token. Without the +service token, if a long-running operation exceeds the expiry of the user +token, post operations such as cleanup after a live migration could fail when +Nova calls other service APIs like block-storage (Cinder) or networking +(Neutron). + +The service token is also used by services to validate whether the API caller +is a service. Some service APIs are restricted to service users only. + +To set up service tokens, create a ``nova`` service user and ``service`` role +in the identity service (Keystone) and assign the ``service`` role to the +``nova`` service user. + +Then, configure the :oslo.config:group:`service_user` section of the Nova +configuration file, for example: + +.. code-block:: ini + + [service_user] + send_service_user_token = true + auth_url = https://104.130.216.102/identity + auth_strategy = keystone + auth_type = password + project_domain_name = Default + project_name = service + user_domain_name = Default + username = nova + password = secretservice + ... + +And configure the other identity options as necessary for the service user, +much like you would configure nova to work with the image service (Glance) or +networking service (Neutron). + +.. note:: + + Please note that the role assigned to the :oslo.config:group:`service_user` + needs to be in the configured + :oslo.config:option:`keystone_authtoken.service_token_roles` of other + services such as block-storage (Cinder), image (Glance), and networking + (Neutron). diff --git a/doc/source/admin/evacuate.rst b/doc/source/admin/evacuate.rst index ef9eccd9312..18796d9c237 100644 --- a/doc/source/admin/evacuate.rst +++ b/doc/source/admin/evacuate.rst @@ -97,3 +97,17 @@ instances up and running. using a pattern you might want to use the ``--strict`` flag which got introduced in version 10.2.0 to make sure nova matches the ``FAILED_HOST`` exactly. + +.. note:: + .. code-block:: bash + + +------+--------+--------------+ + | Name | Status | Task State | + +------+--------+--------------+ + | vm_1 | ACTIVE | powering-off | + +------------------------------+ + + If the instance task state is not None, evacuation will be possible. However, + depending on the ongoing operation, there may be clean up required in other + services which the instance was using, such as neutron, cinder, glance, or + the storage backend. \ No newline at end of file diff --git a/doc/source/admin/index.rst b/doc/source/admin/index.rst index e83f680df2e..34babb5f152 100644 --- a/doc/source/admin/index.rst +++ b/doc/source/admin/index.rst @@ -198,6 +198,7 @@ instance for these kind of workloads. virtual-gpu file-backed-memory ports-with-resource-requests + vdpa virtual-persistent-memory emulated-tpm uefi diff --git a/doc/source/admin/live-migration-usage.rst b/doc/source/admin/live-migration-usage.rst index 783ab5e27c2..a1e7f187566 100644 --- a/doc/source/admin/live-migration-usage.rst +++ b/doc/source/admin/live-migration-usage.rst @@ -320,4 +320,4 @@ To make live-migration succeed, you have several options: If live migrations routinely timeout or fail during cleanup operations due to the user token timing out, consider configuring nova to use -:ref:`service user tokens `. +:ref:`service user tokens `. diff --git a/doc/source/admin/migrate-instance-with-snapshot.rst b/doc/source/admin/migrate-instance-with-snapshot.rst index 65059679abb..230431091e0 100644 --- a/doc/source/admin/migrate-instance-with-snapshot.rst +++ b/doc/source/admin/migrate-instance-with-snapshot.rst @@ -67,7 +67,7 @@ Create a snapshot of the instance If snapshot operations routinely fail because the user token times out while uploading a large disk image, consider configuring nova to use - :ref:`service user tokens `. + :ref:`service user tokens `. #. Use the :command:`openstack image list` command to check the status until the status is ``ACTIVE``: diff --git a/doc/source/admin/resource-limits.rst b/doc/source/admin/resource-limits.rst index c74ad31c17b..8ef248a9a1d 100644 --- a/doc/source/admin/resource-limits.rst +++ b/doc/source/admin/resource-limits.rst @@ -38,7 +38,8 @@ CPU limits Libvirt enforces CPU limits in terms of *shares* and *quotas*, configured via :nova:extra-spec:`quota:cpu_shares` and :nova:extra-spec:`quota:cpu_period` / :nova:extra-spec:`quota:cpu_quota`, respectively. Both are implemented using -the `cgroups v1 cpu controller`__. +the `cgroups cpu controller`__. Note that allowed values for *shares* are +platform dependant. CPU shares are a proportional weighted share of total CPU resources relative to other instances. It does not limit CPU usage if CPUs are not busy. There is no diff --git a/doc/source/admin/support-compute.rst b/doc/source/admin/support-compute.rst index 8522e51d795..31e32fd1ddc 100644 --- a/doc/source/admin/support-compute.rst +++ b/doc/source/admin/support-compute.rst @@ -478,67 +478,3 @@ Ensure the ``compute`` endpoint in the identity service catalog is pointing at ``/v2.1`` instead of ``/v2``. The former route supports microversions, while the latter route is considered the legacy v2.0 compatibility-mode route which renders all requests as if they were made on the legacy v2.0 API. - - -.. _user_token_timeout: - -User token times out during long-running operations ---------------------------------------------------- - -Problem -~~~~~~~ - -Long-running operations such as live migration or snapshot can sometimes -overrun the expiry of the user token. In such cases, post operations such -as cleaning up after a live migration can fail when the nova-compute service -needs to cleanup resources in other services, such as in the block-storage -(cinder) or networking (neutron) services. - -For example: - -.. code-block:: console - - 2018-12-17 13:47:29.591 16987 WARNING nova.virt.libvirt.migration [req-7bc758de-b2e4-461b-a971-f79be6cd4703 313d1247d7b845da9c731eec53e50a26 2f693c782fa748c2baece8db95b4ba5b - default default] [instance: ead8ecc3-f473-4672-a67b-c44534c6042d] Live migration not completed after 2400 sec - 2018-12-17 13:47:30.097 16987 WARNING nova.virt.libvirt.driver [req-7bc758de-b2e4-461b-a971-f79be6cd4703 313d1247d7b845da9c731eec53e50a26 2f693c782fa748c2baece8db95b4ba5b - default default] [instance: ead8ecc3-f473-4672-a67b-c44534c6042d] Migration operation was cancelled - 2018-12-17 13:47:30.299 16987 ERROR nova.virt.libvirt.driver [req-7bc758de-b2e4-461b-a971-f79be6cd4703 313d1247d7b845da9c731eec53e50a26 2f693c782fa748c2baece8db95b4ba5b - default default] [instance: ead8ecc3-f473-4672-a67b-c44534c6042d] Live Migration failure: operation aborted: migration job: canceled by client: libvirtError: operation aborted: migration job: canceled by client - 2018-12-17 13:47:30.685 16987 INFO nova.compute.manager [req-7bc758de-b2e4-461b-a971-f79be6cd4703 313d1247d7b845da9c731eec53e50a26 2f693c782fa748c2baece8db95b4ba5b - default default] [instance: ead8ecc3-f473-4672-a67b-c44534c6042d] Swapping old allocation on 3e32d595-bd1f-4136-a7f4-c6703d2fbe18 held by migration 17bec61d-544d-47e0-a1c1-37f9d7385286 for instance - 2018-12-17 13:47:32.450 16987 ERROR nova.volume.cinder [req-7bc758de-b2e4-461b-a971-f79be6cd4703 313d1247d7b845da9c731eec53e50a26 2f693c782fa748c2baece8db95b4ba5b - default default] Delete attachment failed for attachment 58997d5b-24f0-4073-819e-97916fb1ee19. Error: The request you have made requires authentication. (HTTP 401) Code: 401: Unauthorized: The request you have made requires authentication. (HTTP 401) - -Solution -~~~~~~~~ - -Configure nova to use service user tokens to supplement the regular user token -used to initiate the operation. The identity service (keystone) will then -authenticate a request using the service user token if the user token has -already expired. - -To use, create a service user in the identity service similar as you would when -creating the ``nova`` service user. - -Then configure the :oslo.config:group:`service_user` section of the nova -configuration file, for example: - -.. code-block:: ini - - [service_user] - send_service_user_token = True - auth_type = password - project_domain_name = Default - project_name = service - user_domain_name = Default - password = secretservice - username = nova - auth_url = https://104.130.216.102/identity - ... - -And configure the other identity options as necessary for the service user, -much like you would configure nova to work with the image service (glance) -or networking service. - -.. note:: - - Please note that the role of the :oslo.config:group:`service_user` you - configure needs to be a superset of - :oslo.config:option:`keystone_authtoken.service_token_roles` (The option - :oslo.config:option:`keystone_authtoken.service_token_roles` is configured - in cinder, glance and neutron). diff --git a/doc/source/admin/vdpa.rst b/doc/source/admin/vdpa.rst new file mode 100644 index 00000000000..8583d327ccc --- /dev/null +++ b/doc/source/admin/vdpa.rst @@ -0,0 +1,92 @@ +============================ +Using ports vnic_type='vdpa' +============================ +.. versionadded:: 23.0.0 (Wallaby) + + Introduced support for vDPA. + +.. important:: + The functionality described below is only supported by the + libvirt/KVM virt driver. + +The kernel vDPA (virtio Data Path Acceleration) framework +provides a vendor independent framework for offloading data-plane +processing to software or hardware virtio device backends. +While the kernel vDPA framework supports many types of vDPA devices, +at this time nova only support ``virtio-net`` devices +using the ``vhost-vdpa`` front-end driver. Support for ``virtio-blk`` or +``virtio-gpu`` may be added in the future but is not currently planned +for any specific release. + +vDPA device tracking +~~~~~~~~~~~~~~~~~~~~ +When implementing support for vDPA based neutron ports one of the first +decisions nova had to make was how to model the availability of vDPA devices +and the capability to virtualize vDPA devices. As the initial use-case +for this technology was to offload networking to hardware offload OVS via +neutron ports the decision was made to extend the existing PCI tracker that +is used for SR-IOV and pci-passthrough to support vDPA devices. As a result +a simplification was made to assume that the parent device of a vDPA device +is an SR-IOV Virtual Function (VF). As a result software only vDPA device such +as those created by the kernel ``vdpa-sim`` sample module are not supported. + +To make vDPA device available to be scheduled to guests the operator should +include the device using the PCI address or vendor ID and product ID of the +parent VF in the PCI ``device_spec``. +See: :nova-doc:`pci-passthrough ` for details. + +Nova will not create the VFs or vDPA devices automatically. It is expected +that the operator will allocate them before starting the nova-compute agent. +While no specific mechanisms is prescribed to do this udev rules or systemd +service files are generally the recommended approach to ensure the devices +are created consistently across reboots. + +.. note:: + As vDPA is an offload only for the data plane and not the control plane a + vDPA control plane is required to properly support vDPA device passthrough. + At the time of writing only hardware offloaded OVS is supported when using + vDPA with nova. Because of this vDPA devices cannot be requested using the + PCI alias. While nova could allow vDPA devices to be requested by the + flavor using a PCI alias we would not be able to correctly configure the + device as there would be no suitable control plane. For this reason vDPA + devices are currently only consumable via neutron ports. + +Virt driver support +~~~~~~~~~~~~~~~~~~~ + +Supporting neutron ports with ``vnic_type=vdpa`` depends on the capability +of the virt driver. At this time only the ``libvirt`` virt driver with KVM +is fully supported. QEMU may also work but is untested. + +vDPA support depends on kernel 5.7+, Libvirt 6.9.0+ and QEMU 5.1+. + +vDPA lifecycle operations +~~~~~~~~~~~~~~~~~~~~~~~~~ + +At this time vDPA ports can only be added to a VM when it is first created. +To do this the normal SR-IOV workflow is used where by the port is first created +in neutron and passed into nova as part of the server create request. + +.. code-block:: bash + + openstack port create --network --vnic-type vdpa vdpa-port + openstack server create --flavor --image --port vdpa-vm + +When vDPA support was first introduced no move operations were supported. +As this documentation was added in the change that enabled some move operations +The following should be interpreted both as a retrospective and future looking +viewpoint and treated as a living document which will be updated as functionality evolves. + +23.0.0: initial support is added for creating a VM with vDPA ports, move operations +are blocked in the API but implemented in code. +26.0.0: support for all move operation except live migration is tested and api blocks are removed. +25.x.y: (planned) api block removal backported to stable/Yoga +24.x.y: (planned) api block removal backported to stable/Xena +23.x.y: (planned) api block removal backported to stable/wallaby +26.0.0: (in progress) interface attach/detach, suspend/resume and hot plug live migration +are implemented to fully support all lifecycle operations on instances with vDPA ports. + +.. note:: + The ``(planned)`` and ``(in progress)`` qualifiers will be removed when those items are + completed. If your current version of the document contains those qualifiers then those + lifecycle operations are unsupported. diff --git a/doc/source/contributor/development-environment.rst b/doc/source/contributor/development-environment.rst index 32b8f8334e0..3e19ef1ca23 100644 --- a/doc/source/contributor/development-environment.rst +++ b/doc/source/contributor/development-environment.rst @@ -197,7 +197,7 @@ Using fake computes for tests The number of instances supported by fake computes is not limited by physical constraints. It allows you to perform stress tests on a deployment with few resources (typically a laptop). Take care to avoid using scheduler filters -that will limit the number of instances per compute, such as ``AggregateCoreFilter``. +that will limit the number of instances per compute, such as ``NumInstancesFilter``. Fake computes can also be used in multi hypervisor-type deployments in order to take advantage of fake and "real" computes during tests: diff --git a/doc/source/install/compute-install-obs.rst b/doc/source/install/compute-install-obs.rst index c5c1d29fb3d..c227b6eba43 100644 --- a/doc/source/install/compute-install-obs.rst +++ b/doc/source/install/compute-install-obs.rst @@ -92,6 +92,26 @@ Install and configure components Comment out or remove any other options in the ``[keystone_authtoken]`` section. + * In the ``[service_user]`` section, configure :ref:`service user + tokens `: + + .. path /etc/nova/nova.conf + .. code-block:: ini + + [service_user] + send_service_user_token = true + auth_url = https://controller/identity + auth_strategy = keystone + auth_type = password + project_domain_name = Default + project_name = service + user_domain_name = Default + username = nova + password = NOVA_PASS + + Replace ``NOVA_PASS`` with the password you chose for the ``nova`` user in + the Identity service. + * In the ``[DEFAULT]`` section, configure the ``my_ip`` option: .. path /etc/nova/nova.conf diff --git a/doc/source/install/compute-install-rdo.rst b/doc/source/install/compute-install-rdo.rst index 0a5ad685a62..0c6203a6673 100644 --- a/doc/source/install/compute-install-rdo.rst +++ b/doc/source/install/compute-install-rdo.rst @@ -84,6 +84,26 @@ Install and configure components Comment out or remove any other options in the ``[keystone_authtoken]`` section. + * In the ``[service_user]`` section, configure :ref:`service user + tokens `: + + .. path /etc/nova/nova.conf + .. code-block:: ini + + [service_user] + send_service_user_token = true + auth_url = https://controller/identity + auth_strategy = keystone + auth_type = password + project_domain_name = Default + project_name = service + user_domain_name = Default + username = nova + password = NOVA_PASS + + Replace ``NOVA_PASS`` with the password you chose for the ``nova`` user in + the Identity service. + * In the ``[DEFAULT]`` section, configure the ``my_ip`` option: .. path /etc/nova/nova.conf diff --git a/doc/source/install/compute-install-ubuntu.rst b/doc/source/install/compute-install-ubuntu.rst index 8605c73316e..baf0585e52b 100644 --- a/doc/source/install/compute-install-ubuntu.rst +++ b/doc/source/install/compute-install-ubuntu.rst @@ -74,6 +74,26 @@ Install and configure components Comment out or remove any other options in the ``[keystone_authtoken]`` section. + * In the ``[service_user]`` section, configure :ref:`service user + tokens `: + + .. path /etc/nova/nova.conf + .. code-block:: ini + + [service_user] + send_service_user_token = true + auth_url = https://controller/identity + auth_strategy = keystone + auth_type = password + project_domain_name = Default + project_name = service + user_domain_name = Default + username = nova + password = NOVA_PASS + + Replace ``NOVA_PASS`` with the password you chose for the ``nova`` user in + the Identity service. + * In the ``[DEFAULT]`` section, configure the ``my_ip`` option: .. path /etc/nova/nova.conf diff --git a/doc/source/install/controller-install-obs.rst b/doc/source/install/controller-install-obs.rst index 18499612c3e..01b7bb0f5ab 100644 --- a/doc/source/install/controller-install-obs.rst +++ b/doc/source/install/controller-install-obs.rst @@ -260,6 +260,26 @@ Install and configure components Comment out or remove any other options in the ``[keystone_authtoken]`` section. + * In the ``[service_user]`` section, configure :ref:`service user + tokens `: + + .. path /etc/nova/nova.conf + .. code-block:: ini + + [service_user] + send_service_user_token = true + auth_url = https://controller/identity + auth_strategy = keystone + auth_type = password + project_domain_name = Default + project_name = service + user_domain_name = Default + username = nova + password = NOVA_PASS + + Replace ``NOVA_PASS`` with the password you chose for the ``nova`` user in + the Identity service. + * In the ``[DEFAULT]`` section, configure the ``my_ip`` option to use the management interface IP address of the controller node: diff --git a/doc/source/install/controller-install-rdo.rst b/doc/source/install/controller-install-rdo.rst index fd2419631ec..b6098f1776b 100644 --- a/doc/source/install/controller-install-rdo.rst +++ b/doc/source/install/controller-install-rdo.rst @@ -247,6 +247,26 @@ Install and configure components Comment out or remove any other options in the ``[keystone_authtoken]`` section. + * In the ``[service_user]`` section, configure :ref:`service user + tokens `: + + .. path /etc/nova/nova.conf + .. code-block:: ini + + [service_user] + send_service_user_token = true + auth_url = https://controller/identity + auth_strategy = keystone + auth_type = password + project_domain_name = Default + project_name = service + user_domain_name = Default + username = nova + password = NOVA_PASS + + Replace ``NOVA_PASS`` with the password you chose for the ``nova`` user in + the Identity service. + * In the ``[DEFAULT]`` section, configure the ``my_ip`` option to use the management interface IP address of the controller node: diff --git a/doc/source/install/controller-install-ubuntu.rst b/doc/source/install/controller-install-ubuntu.rst index 7282b0b2e22..1363a98ba8b 100644 --- a/doc/source/install/controller-install-ubuntu.rst +++ b/doc/source/install/controller-install-ubuntu.rst @@ -237,6 +237,26 @@ Install and configure components Comment out or remove any other options in the ``[keystone_authtoken]`` section. + * In the ``[service_user]`` section, configure :ref:`service user + tokens `: + + .. path /etc/nova/nova.conf + .. code-block:: ini + + [service_user] + send_service_user_token = true + auth_url = https://controller/identity + auth_strategy = keystone + auth_type = password + project_domain_name = Default + project_name = service + user_domain_name = Default + username = nova + password = NOVA_PASS + + Replace ``NOVA_PASS`` with the password you chose for the ``nova`` user in + the Identity service. + * In the ``[DEFAULT]`` section, configure the ``my_ip`` option to use the management interface IP address of the controller node: diff --git a/lower-constraints.txt b/lower-constraints.txt deleted file mode 100644 index 93e757994e2..00000000000 --- a/lower-constraints.txt +++ /dev/null @@ -1,166 +0,0 @@ -alembic==1.5.0 -amqp==2.5.0 -appdirs==1.4.3 -asn1crypto==0.24.0 -attrs==17.4.0 -automaton==1.14.0 -bandit==1.1.0 -cachetools==2.0.1 -castellan==0.16.0 -cffi==1.14.0 -cliff==2.11.0 -cmd2==0.8.1 -colorama==0.3.9 -coverage==4.0 -cryptography==2.7 -cursive==0.2.1 -dataclasses==0.7 -ddt==1.2.1 -debtcollector==1.19.0 -decorator==4.1.0 -deprecation==2.0 -dogpile.cache==0.6.5 -enum-compat==0.0.2 -eventlet==0.30.1 -extras==1.0.0 -fasteners==0.14.1 -fixtures==3.0.0 -future==0.16.0 -futurist==1.8.0 -gabbi==1.35.0 -gitdb2==2.0.3 -GitPython==2.1.8 -greenlet==0.4.15 -idna==2.6 -iso8601==0.1.11 -Jinja2==2.10 -jmespath==0.9.3 -jsonpatch==1.21 -jsonpath-rw==1.4.0 -jsonpath-rw-ext==1.1.3 -jsonpointer==2.0 -jsonschema==3.2.0 -keystoneauth1==3.16.0 -keystonemiddleware==4.20.0 -kombu==4.6.1 -linecache2==1.0.0 -lxml==4.5.0 -Mako==1.0.7 -MarkupSafe==1.1.1 -microversion-parse==0.2.1 -mock==3.0.0 -msgpack==0.6.0 -msgpack-python==0.5.6 -munch==2.2.0 -mypy==0.761 -netaddr==0.7.18 -netifaces==0.10.4 -networkx==2.1.0 -numpy==1.19.0 -openstacksdk==0.35.0 -os-brick==5.2 -os-client-config==1.29.0 -os-resource-classes==1.1.0 -os-service-types==1.7.0 -os-traits==2.7.0 -os-vif==1.15.2 -os-win==5.5.0 -osc-lib==1.10.0 -oslo.cache==1.26.0 -oslo.concurrency==4.5.0 -oslo.config==8.6.0 -oslo.context==3.4.0 -oslo.db==10.0.0 -oslo.i18n==5.1.0 -oslo.log==4.6.1 -oslo.limit==1.5.0 -oslo.messaging==10.3.0 -oslo.middleware==3.31.0 -oslo.policy==3.7.0 -oslo.privsep==2.6.2 -oslo.reports==1.18.0 -oslo.rootwrap==5.8.0 -oslo.serialization==4.2.0 -oslo.service==2.8.0 -oslo.upgradecheck==1.3.0 -oslo.utils==4.12.1 -oslo.versionedobjects==1.35.0 -oslo.vmware==3.6.0 -oslotest==3.8.0 -osprofiler==1.4.0 -ovs==2.10.0 -ovsdbapp==0.15.0 -packaging==20.4 -paramiko==2.7.1 -Paste==2.0.2 -PasteDeploy==1.5.0 -pbr==5.8.0 -pluggy==0.6.0 -ply==3.11 -prettytable==0.7.1 -psutil==3.2.2 -psycopg2-binary==2.8 -py==1.5.2 -pyasn1==0.4.2 -pyasn1-modules==0.2.1 -pycadf==2.7.0 -pycparser==2.18 -pyinotify==0.9.6 -pyroute2==0.5.4 -PyJWT==1.7.0 -PyMySQL==0.8.0 -pyOpenSSL==17.5.0 -pyparsing==2.2.0 -pyperclip==1.6.0 -pypowervm==1.1.15 -pytest==3.4.2 -python-barbicanclient==4.5.2 -python-cinderclient==3.3.0 -python-dateutil==2.7.0 -python-editor==1.0.3 -python-glanceclient==2.8.0 -python-ironicclient==3.0.0 -python-keystoneclient==3.15.0 -python-mimeparse==1.6.0 -python-neutronclient==7.1.0 -python-subunit==1.4.0 -pytz==2018.3 -PyYAML==5.1 -repoze.lru==0.7 -requests==2.25.1 -requests-mock==1.2.0 -requestsexceptions==1.4.0 -retrying==1.3.3 -rfc3986==1.2.0 -Routes==2.3.1 -simplejson==3.13.2 -six==1.15.0 -smmap2==2.0.3 -sortedcontainers==2.1.0 -SQLAlchemy==1.4.13 -sqlalchemy-migrate==0.13.0 -sqlparse==0.2.4 -statsd==3.2.2 -stestr==2.0.0 -stevedore==1.20.0 -suds-jurko==0.6 -taskflow==3.8.0 -Tempita==0.5.2 -tenacity==6.3.1 -testrepository==0.0.20 -testresources==2.0.0 -testscenarios==0.4 -testtools==2.5.0 -tooz==1.58.0 -traceback2==1.4.0 -types-paramiko==0.1.3 -unittest2==1.1.0 -urllib3==1.22 -vine==1.1.4 -voluptuous==0.11.1 -warlock==1.3.1 -WebOb==1.8.2 -websockify==0.9.0 -wrapt==1.10.11 -wsgi-intercept==1.7.0 -zVMCloudConnector==1.3.0 diff --git a/nova/api/openstack/compute/flavor_access.py b/nova/api/openstack/compute/flavor_access.py index e17e6f0ddcd..fc8df15db5b 100644 --- a/nova/api/openstack/compute/flavor_access.py +++ b/nova/api/openstack/compute/flavor_access.py @@ -93,7 +93,14 @@ def _remove_tenant_access(self, req, id, body): vals = body['removeTenantAccess'] tenant = vals['tenant'] - identity.verify_project_id(context, tenant) + # It doesn't really matter if project exists or not: we can delete + # it from flavor's access list in both cases. + try: + identity.verify_project_id(context, tenant) + except webob.exc.HTTPBadRequest as identity_exc: + msg = "Project ID %s is not a valid project." % tenant + if msg not in identity_exc.explanation: + raise # NOTE(gibi): We have to load a flavor from the db here as # flavor.remove_access() will try to emit a notification and that needs diff --git a/nova/api/openstack/compute/remote_consoles.py b/nova/api/openstack/compute/remote_consoles.py index 36015542aa3..7d374ef432e 100644 --- a/nova/api/openstack/compute/remote_consoles.py +++ b/nova/api/openstack/compute/remote_consoles.py @@ -56,6 +56,9 @@ def get_vnc_console(self, req, id, body): raise webob.exc.HTTPNotFound(explanation=e.format_message()) except exception.InstanceNotReady as e: raise webob.exc.HTTPConflict(explanation=e.format_message()) + except exception.InstanceInvalidState as e: + common.raise_http_conflict_for_instance_invalid_state( + e, 'get_vnc_console', id) except NotImplementedError: common.raise_feature_not_supported() diff --git a/nova/api/openstack/compute/services.py b/nova/api/openstack/compute/services.py index 6deb84a7f1a..e9d51d4d0c8 100644 --- a/nova/api/openstack/compute/services.py +++ b/nova/api/openstack/compute/services.py @@ -48,13 +48,10 @@ def __init__(self): self.actions = {"enable": self._enable, "disable": self._disable, "disable-log-reason": self._disable_log_reason} - self._placementclient = None # Lazy-load on first access. @property def placementclient(self): - if self._placementclient is None: - self._placementclient = report.SchedulerReportClient() - return self._placementclient + return report.report_client_singleton() def _get_services(self, req): # The API services are filtered out since they are not RPC services @@ -328,7 +325,7 @@ def delete(self, req, id): "Failed to delete compute node resource provider " "for compute node %s: %s", compute_node.uuid, str(e)) - # remove the host_mapping of this host. + # Remove the host_mapping of this host. try: hm = objects.HostMapping.get_by_host(context, service.host) hm.destroy() diff --git a/nova/api/openstack/identity.py b/nova/api/openstack/identity.py index 7ffc623fede..15ec884aea8 100644 --- a/nova/api/openstack/identity.py +++ b/nova/api/openstack/identity.py @@ -27,24 +27,27 @@ def verify_project_id(context, project_id): """verify that a project_id exists. This attempts to verify that a project id exists. If it does not, - an HTTPBadRequest is emitted. + an HTTPBadRequest is emitted. Also HTTPBadRequest is emitted + if Keystone identity service version 3.0 is not found. """ adap = utils.get_ksa_adapter( 'identity', ksa_auth=context.get_auth_plugin(), min_version=(3, 0), max_version=(3, 'latest')) - failure = webob.exc.HTTPBadRequest( - explanation=_("Project ID %s is not a valid project.") % - project_id) try: resp = adap.get('/projects/%s' % project_id) except kse.EndpointNotFound: LOG.error( - "Keystone identity service version 3.0 was not found. This might " - "be because your endpoint points to the v2.0 versioned endpoint " - "which is not supported. Please fix this.") - raise failure + "Keystone identity service version 3.0 was not found. This " + "might be caused by Nova misconfiguration or Keystone " + "problems.") + msg = _("Nova was unable to find Keystone service endpoint.") + # TODO(astupnik). It may be reasonable to switch to HTTP 503 + # (HTTP Service Unavailable) instead of HTTP Bad Request here. + # If proper Keystone servie is inaccessible, then technially + # this is a server side error and not an error in Nova. + raise webob.exc.HTTPBadRequest(explanation=msg) except kse.ClientException: # something is wrong, like there isn't a keystone v3 endpoint, # or nova isn't configured for the interface to talk to it; @@ -57,7 +60,8 @@ def verify_project_id(context, project_id): return True elif resp.status_code == 404: # we got access, and we know this project is not there - raise failure + msg = _("Project ID %s is not a valid project.") % project_id + raise webob.exc.HTTPBadRequest(explanation=msg) elif resp.status_code == 403: # we don't have enough permission to verify this, so default # to "it's ok". diff --git a/nova/cmd/manage.py b/nova/cmd/manage.py index f704a42698e..f13f6fddfc2 100644 --- a/nova/cmd/manage.py +++ b/nova/cmd/manage.py @@ -2217,7 +2217,7 @@ def heal_allocations(self, max_count=None, verbose=False, dry_run=False, output(_('No cells to process.')) return 4 - placement = report.SchedulerReportClient() + placement = report.report_client_singleton() neutron = None if heal_port_allocations: @@ -2718,7 +2718,7 @@ def audit(self, verbose=False, provider_uuid=None, delete=False): if verbose: output = lambda msg: print(msg) - placement = report.SchedulerReportClient() + placement = report.report_client_singleton() # Resets two in-memory dicts for knowing instances per compute node self.cn_uuid_mapping = collections.defaultdict(tuple) self.instances_mapping = collections.defaultdict(list) @@ -3266,9 +3266,10 @@ def _validate_image_properties(self, image_properties): # Return the dict so we can update the instance system_metadata return image_properties - def _update_image_properties(self, instance, image_properties): + def _update_image_properties(self, ctxt, instance, image_properties): """Update instance image properties + :param ctxt: nova.context.RequestContext :param instance: The instance to update :param image_properties: List of image properties and values to update """ @@ -3292,8 +3293,13 @@ def _update_image_properties(self, instance, image_properties): for image_property, value in image_properties.items(): instance.system_metadata[f'image_{image_property}'] = value + request_spec = objects.RequestSpec.get_by_instance_uuid( + ctxt, instance.uuid) + request_spec.image = instance.image_meta + # Save and return 0 instance.save() + request_spec.save() return 0 @action_description(_( @@ -3328,7 +3334,7 @@ def set(self, instance_uuid=None, image_properties=None): instance = objects.Instance.get_by_uuid( cctxt, instance_uuid, expected_attrs=['system_metadata']) return self._update_image_properties( - instance, image_properties) + ctxt, instance, image_properties) except ValueError as e: print(str(e)) return 6 diff --git a/nova/cmd/status.py b/nova/cmd/status.py index 8a7041b062b..2f310f08714 100644 --- a/nova/cmd/status.py +++ b/nova/cmd/status.py @@ -336,6 +336,15 @@ def _check_machine_type_set(self): return upgradecheck.Result(upgradecheck.Code.SUCCESS) + def _check_service_user_token(self): + if not CONF.service_user.send_service_user_token: + msg = (_(""" +Service user token configuration is required for all Nova services. +For more details see the following: +https://docs.openstack.org/latest/nova/admin/configuration/service-user-token.html""")) # noqa + return upgradecheck.Result(upgradecheck.Code.FAILURE, msg) + return upgradecheck.Result(upgradecheck.Code.SUCCESS) + # The format of the check functions is to return an upgradecheck.Result # object with the appropriate upgradecheck.Code and details set. If the # check hits warnings or failures then those should be stored in the @@ -361,6 +370,8 @@ def _check_machine_type_set(self): (_('Older than N-1 computes'), _check_old_computes), # Added in Wallaby (_('hw_machine_type unset'), _check_machine_type_set), + # Added in Bobcat + (_('Service User Token Configuration'), _check_service_user_token), ) diff --git a/nova/compute/api.py b/nova/compute/api.py index 43a0f66a100..76c11658c24 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -384,7 +384,6 @@ def __init__(self, image_api=None, network_api=None, volume_api=None): self.image_api = image_api or glance.API() self.network_api = network_api or neutron.API() self.volume_api = volume_api or cinder.API() - self._placementclient = None # Lazy-load on first access. self.compute_rpcapi = compute_rpcapi.ComputeAPI() self.compute_task_api = conductor.ComputeTaskAPI() self.servicegroup_api = servicegroup.API() @@ -2573,9 +2572,7 @@ def _local_cleanup_bdm_volumes(self, bdms, instance, context): @property def placementclient(self): - if self._placementclient is None: - self._placementclient = report.SchedulerReportClient() - return self._placementclient + return report.report_client_singleton() def _local_delete(self, context, instance, bdms, delete_type, cb): if instance.vm_state == vm_states.SHELVED_OFFLOADED: @@ -4096,9 +4093,6 @@ def _validate_host_for_cold_migrate( # finally split resize and cold migration into separate code paths @block_extended_resource_request @block_port_accelerators() - # FIXME(sean-k-mooney): Cold migrate and resize to different hosts - # probably works but they have not been tested so block them for now - @reject_vdpa_instances(instance_actions.RESIZE) @block_accelerators() @check_instance_lock @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED]) @@ -4223,6 +4217,19 @@ def resize(self, context, instance, flavor_id=None, clean_shutdown=True, if not same_flavor: request_spec.numa_topology = hardware.numa_get_constraints( new_flavor, instance.image_meta) + # if the flavor is changed then we need to recalculate the + # pci_requests as well because the new flavor might request + # different pci_aliases + new_pci_requests = pci_request.get_pci_requests_from_flavor( + new_flavor) + new_pci_requests.instance_uuid = instance.uuid + # The neutron based InstancePCIRequest cannot change during resize, + # so we just need to copy them from the old request + for request in request_spec.pci_requests.requests or []: + if request.source == objects.InstancePCIRequest.NEUTRON_PORT: + new_pci_requests.requests.append(request) + request_spec.pci_requests = new_pci_requests + # TODO(huaqiang): Remove in Wallaby # check nova-compute nodes have been updated to Victoria to resize # instance to a new mixed instance from a dedicated or shared @@ -4324,10 +4331,7 @@ def _allow_resize_to_same_host(self, cold_migrate, instance): allow_same_host = CONF.allow_resize_to_same_host return allow_same_host - # FIXME(sean-k-mooney): Shelve works but unshelve does not due to bug - # #1851545, so block it for now @block_port_accelerators() - @reject_vdpa_instances(instance_actions.SHELVE) @reject_vtpm_instances(instance_actions.SHELVE) @block_accelerators(until_service=54) @check_instance_lock @@ -4550,6 +4554,7 @@ def rescue(self, context, instance, rescue_password=None, allow_bfv_rescue=False): """Rescue the given instance.""" + image_meta = None if rescue_image_ref: try: image_meta = image_meta_obj.ImageMeta.from_image_ref( @@ -4570,6 +4575,8 @@ def rescue(self, context, instance, rescue_password=None, "image properties set") raise exception.UnsupportedRescueImage( image=rescue_image_ref) + else: + image_meta = instance.image_meta bdms = objects.BlockDeviceMappingList.get_by_instance_uuid( context, instance.uuid) @@ -4578,6 +4585,9 @@ def rescue(self, context, instance, rescue_password=None, volume_backed = compute_utils.is_volume_backed_instance( context, instance, bdms) + allow_bfv_rescue &= 'hw_rescue_bus' in image_meta.properties and \ + 'hw_rescue_device' in image_meta.properties + if volume_backed and allow_bfv_rescue: cn = objects.ComputeNode.get_by_host_and_nodename( context, instance.host, instance.node) @@ -5469,12 +5479,10 @@ def live_migrate_abort(self, context, instance, migration_id, @block_extended_resource_request @block_port_accelerators() - # FIXME(sean-k-mooney): rebuild works but we have not tested evacuate yet - @reject_vdpa_instances(instance_actions.EVACUATE) @reject_vtpm_instances(instance_actions.EVACUATE) @block_accelerators(until_service=SUPPORT_ACCELERATOR_SERVICE_FOR_REBUILD) @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED, - vm_states.ERROR]) + vm_states.ERROR], task_state=None) def evacuate(self, context, instance, host, on_shared_storage, admin_password=None, force=None): """Running evacuate to target host. @@ -5501,7 +5509,7 @@ def evacuate(self, context, instance, host, on_shared_storage, context, instance.uuid) instance.task_state = task_states.REBUILDING - instance.save(expected_task_state=[None]) + instance.save(expected_task_state=None) self._record_action_start(context, instance, instance_actions.EVACUATE) # NOTE(danms): Create this as a tombstone for the source compute @@ -6309,13 +6317,10 @@ class AggregateAPI: def __init__(self): self.compute_rpcapi = compute_rpcapi.ComputeAPI() self.query_client = query.SchedulerQueryClient() - self._placement_client = None # Lazy-load on first access. @property def placement_client(self): - if self._placement_client is None: - self._placement_client = report.SchedulerReportClient() - return self._placement_client + return report.report_client_singleton() @wrap_exception() def create_aggregate(self, context, aggregate_name, availability_zone): diff --git a/nova/compute/build_results.py b/nova/compute/build_results.py index ca9ed51410f..a091c89ff65 100644 --- a/nova/compute/build_results.py +++ b/nova/compute/build_results.py @@ -24,3 +24,11 @@ ACTIVE = 'active' # Instance is running FAILED = 'failed' # Instance failed to build and was not rescheduled RESCHEDULED = 'rescheduled' # Instance failed to build, but was rescheduled +# Instance failed by policy violation (such as affinity or anti-affinity) +# and was not rescheduled. In this case, the node's failed count won't be +# increased. +FAILED_BY_POLICY = 'failed_by_policy' +# Instance failed by policy violation (such as affinity or anti-affinity) +# but was rescheduled. In this case, the node's failed count won't be +# increased. +RESCHEDULED_BY_POLICY = 'rescheduled_by_policy' diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 44352909a2a..427cb3aac35 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -623,6 +623,11 @@ def __init__(self, compute_driver=None, *args, **kwargs): # We want the ComputeManager, ResourceTracker and ComputeVirtAPI all # using the same instance of SchedulerReportClient which has the # ProviderTree cache for this compute service. + # NOTE(danms): We do not use the global placement client + # singleton here, because the above-mentioned stack of objects + # maintain local state in the client. Thus, keeping our own + # private object for that stack avoids any potential conflict + # with other users in our process outside of the above. self.reportclient = report.SchedulerReportClient() self.virtapi = ComputeVirtAPI(self) self.network_api = neutron.API() @@ -1242,6 +1247,20 @@ def _init_instance(self, context, instance): 'updated.', instance=instance) self._set_instance_obj_error_state(instance) return + except exception.PciDeviceNotFoundById: + # This is bug 1981813 where the bound port vnic_type has changed + # from direct to macvtap. Nova does not support that and it + # already printed an ERROR when the change is detected during + # _heal_instance_info_cache. Now we print an ERROR again and skip + # plugging the vifs but let the service startup continue to init + # the other instances + LOG.exception( + 'Virtual interface plugging failed for instance. Probably the ' + 'vnic_type of the bound port has been changed. Nova does not ' + 'support such change.', + instance=instance + ) + return if instance.task_state == task_states.RESIZE_MIGRATING: # We crashed during resize/migration, so roll back for safety @@ -1718,27 +1737,32 @@ def _validate_instance_group_policy(self, context, instance, # hosts. This is a validation step to make sure that starting the # instance here doesn't violate the policy. if scheduler_hints is not None: - # only go through here if scheduler_hints is provided, even if it - # is empty. + # only go through here if scheduler_hints is provided, + # even if it is empty. group_hint = scheduler_hints.get('group') if not group_hint: return else: - # The RequestSpec stores scheduler_hints as key=list pairs so - # we need to check the type on the value and pull the single - # entry out. The API request schema validates that + # The RequestSpec stores scheduler_hints as key=list pairs + # so we need to check the type on the value and pull the + # single entry out. The API request schema validates that # the 'group' hint is a single value. if isinstance(group_hint, list): group_hint = group_hint[0] - - group = objects.InstanceGroup.get_by_hint(context, group_hint) + try: + group = objects.InstanceGroup.get_by_hint( + context, group_hint + ) + except exception.InstanceGroupNotFound: + return else: # TODO(ganso): a call to DB can be saved by adding request_spec # to rpcapi payload of live_migration, pre_live_migration and # check_can_live_migrate_destination try: group = objects.InstanceGroup.get_by_instance_uuid( - context, instance.uuid) + context, instance.uuid + ) except exception.InstanceGroupNotFound: return @@ -1780,11 +1804,8 @@ def _do_validation(context, instance, group): else: max_server = 1 if len(members_on_host) >= max_server: - msg = _("Anti-affinity instance group policy " - "was violated.") - raise exception.RescheduledException( - instance_uuid=instance.uuid, - reason=msg) + raise exception.GroupAffinityViolation( + instance_uuid=instance.uuid, policy='Anti-affinity') # NOTE(ganso): The check for affinity below does not work and it # can easily be violated because the lock happens in different @@ -1794,10 +1815,8 @@ def _do_validation(context, instance, group): elif group.policy and 'affinity' == group.policy: group_hosts = group.get_hosts(exclude=[instance.uuid]) if group_hosts and self.host not in group_hosts: - msg = _("Affinity instance group policy was violated.") - raise exception.RescheduledException( - instance_uuid=instance.uuid, - reason=msg) + raise exception.GroupAffinityViolation( + instance_uuid=instance.uuid, policy='Affinity') _do_validation(context, instance, group) @@ -2232,6 +2251,9 @@ def _locked_do_build_and_run_instance(*args, **kwargs): self.reportclient.delete_allocation_for_instance( context, instance.uuid, force=True) + if result in (build_results.FAILED_BY_POLICY, + build_results.RESCHEDULED_BY_POLICY): + return if result in (build_results.FAILED, build_results.RESCHEDULED): self._build_failed(node) @@ -2330,6 +2352,8 @@ def _do_build_and_run_instance(self, context, instance, image, self._nil_out_instance_obj_host_and_node(instance) self._set_instance_obj_error_state(instance, clean_task_state=True) + if isinstance(e, exception.RescheduledByPolicyException): + return build_results.FAILED_BY_POLICY return build_results.FAILED LOG.debug(e.format_message(), instance=instance) # This will be used for logging the exception @@ -2356,6 +2380,10 @@ def _do_build_and_run_instance(self, context, instance, image, injected_files, requested_networks, security_groups, block_device_mapping, request_spec=request_spec, host_lists=[host_list]) + + if isinstance(e, exception.RescheduledByPolicyException): + return build_results.RESCHEDULED_BY_POLICY + return build_results.RESCHEDULED except (exception.InstanceNotFound, exception.UnexpectedDeletingTaskStateError): @@ -2573,6 +2601,17 @@ def _build_and_run_instance(self, context, instance, image, injected_files, bdms=block_device_mapping) raise exception.BuildAbortException(instance_uuid=instance.uuid, reason=e.format_message()) + except exception.GroupAffinityViolation as e: + LOG.exception('Failed to build and run instance', + instance=instance) + self._notify_about_instance_usage(context, instance, + 'create.error', fault=e) + compute_utils.notify_about_instance_create( + context, instance, self.host, + phase=fields.NotificationPhase.ERROR, exception=e, + bdms=block_device_mapping) + raise exception.RescheduledByPolicyException( + instance_uuid=instance.uuid, reason=str(e)) except Exception as e: LOG.exception('Failed to build and run instance', instance=instance) @@ -2708,7 +2747,8 @@ def _build_resources(self, context, instance, requested_networks, block_device_mapping) resources['block_device_info'] = block_device_info except (exception.InstanceNotFound, - exception.UnexpectedDeletingTaskStateError): + exception.UnexpectedDeletingTaskStateError, + exception.ComputeResourcesUnavailable): with excutils.save_and_reraise_exception(): self._build_resources_cleanup(instance, network_info) except (exception.UnexpectedTaskStateError, @@ -6606,9 +6646,9 @@ def _shelve_offload_instance(self, context, instance, clean_shutdown, instance.power_state = current_power_state # NOTE(mriedem): The vm_state has to be set before updating the - # resource tracker, see vm_states.ALLOW_RESOURCE_REMOVAL. The host/node - # values cannot be nulled out until after updating the resource tracker - # though. + # resource tracker, see vm_states.allow_resource_removal(). The + # host/node values cannot be nulled out until after updating the + # resource tracker though. instance.vm_state = vm_states.SHELVED_OFFLOADED instance.task_state = None instance.save(expected_task_state=[task_states.SHELVING, @@ -8413,7 +8453,8 @@ def _cleanup_pre_live_migration(self, context, dest, instance, migrate_data.migration = migration self._rollback_live_migration(context, instance, dest, migrate_data=migrate_data, - source_bdms=source_bdms) + source_bdms=source_bdms, + pre_live_migration=True) def _do_pre_live_migration_from_source(self, context, dest, instance, block_migration, migration, @@ -8571,8 +8612,9 @@ def _do_live_migration(self, context, dest, instance, block_migration, # host attachment. We fetch BDMs before that to retain connection_info # and attachment_id relating to the source host for post migration # cleanup. - post_live_migration = functools.partial(self._post_live_migration, - source_bdms=source_bdms) + post_live_migration = functools.partial( + self._post_live_migration_update_host, source_bdms=source_bdms + ) rollback_live_migration = functools.partial( self._rollback_live_migration, source_bdms=source_bdms) @@ -8844,6 +8886,42 @@ def _post_live_migration_remove_source_vol_connections( bdm.attachment_id, self.host, str(e), instance=instance) + # TODO(sean-k-mooney): add typing + def _post_live_migration_update_host( + self, ctxt, instance, dest, block_migration=False, + migrate_data=None, source_bdms=None + ): + try: + self._post_live_migration( + ctxt, instance, dest, block_migration, migrate_data, + source_bdms) + except Exception: + # Restore the instance object + node_name = None + try: + # get node name of compute, where instance will be + # running after migration, that is destination host + compute_node = self._get_compute_info(ctxt, dest) + node_name = compute_node.hypervisor_hostname + except exception.ComputeHostNotFound: + LOG.exception('Failed to get compute_info for %s', dest) + + # we can never rollback from post live migration and we can only + # get here if the instance is running on the dest so we ensure + # the instance.host is set correctly and reraise the original + # exception unmodified. + if instance.host != dest: + # apply saves the new fields while drop actually removes the + # migration context from the instance, so migration persists. + instance.apply_migration_context() + instance.drop_migration_context() + instance.host = dest + instance.task_state = None + instance.node = node_name + instance.progress = 0 + instance.save() + raise + @wrap_exception() @wrap_instance_fault def _post_live_migration(self, ctxt, instance, dest, @@ -8855,7 +8933,7 @@ def _post_live_migration(self, ctxt, instance, dest, and mainly updating database record. :param ctxt: security context - :param instance: instance dict + :param instance: instance object :param dest: destination host :param block_migration: if true, prepare for block migration :param migrate_data: if not None, it is a dict which has data @@ -9167,7 +9245,8 @@ def _rollback_volume_bdms(self, context, bdms, original_bdms, instance): def _rollback_live_migration(self, context, instance, dest, migrate_data=None, migration_status='failed', - source_bdms=None): + source_bdms=None, + pre_live_migration=False): """Recovers Instance/volume state from migrating -> running. :param context: security context @@ -9217,8 +9296,14 @@ def _rollback_live_migration(self, context, instance, # for nova-network) # NOTE(mriedem): This is a no-op for neutron. self.network_api.setup_networks_on_host(context, instance, self.host) - self.driver.rollback_live_migration_at_source(context, instance, - migrate_data) + + # NOTE(erlon): We should make sure that rollback_live_migration_at_src + # is not called in the pre_live_migration rollback as that will trigger + # the src host to re-attach interfaces which were not detached + # previously. + if not pre_live_migration: + self.driver.rollback_live_migration_at_source(context, instance, + migrate_data) # NOTE(lyarwood): Fetch the current list of BDMs, disconnect any # connected volumes from the dest and delete any volume attachments @@ -10923,6 +11008,9 @@ def _update_migrate_vifs_profile_with_pci(self, profile['vf_num'] = pci_utils.get_vf_num_by_pci_address( pci_dev.address) + if pci_dev.mac_address: + profile['device_mac_address'] = pci_dev.mac_address + mig_vif.profile = profile LOG.debug("Updating migrate VIF profile for port %(port_id)s:" "%(profile)s", {'port_id': port_id, diff --git a/nova/compute/resource_tracker.py b/nova/compute/resource_tracker.py index 8497bbcd899..d195044a57c 100644 --- a/nova/compute/resource_tracker.py +++ b/nova/compute/resource_tracker.py @@ -103,7 +103,7 @@ def __init__(self, host, driver, reportclient=None): monitor_handler = monitors.MonitorHandler(self) self.monitors = monitor_handler.monitors self.old_resources = collections.defaultdict(objects.ComputeNode) - self.reportclient = reportclient or report.SchedulerReportClient() + self.reportclient = reportclient or report.report_client_singleton() self.ram_allocation_ratio = CONF.ram_allocation_ratio self.cpu_allocation_ratio = CONF.cpu_allocation_ratio self.disk_allocation_ratio = CONF.disk_allocation_ratio @@ -1495,7 +1495,8 @@ def _update_usage_from_instance(self, context, instance, nodename, # NOTE(sfinucan): Both brand new instances as well as instances that # are being unshelved will have is_new_instance == True is_removed_instance = not is_new_instance and (is_removed or - instance['vm_state'] in vm_states.ALLOW_RESOURCE_REMOVAL) + vm_states.allow_resource_removal( + vm_state=instance['vm_state'], task_state=instance.task_state)) if is_new_instance: self.tracked_instances.add(uuid) @@ -1554,7 +1555,9 @@ def _update_usage_from_instances(self, context, instances, nodename): instance_by_uuid = {} for instance in instances: - if instance.vm_state not in vm_states.ALLOW_RESOURCE_REMOVAL: + if not vm_states.allow_resource_removal( + vm_state=instance['vm_state'], + task_state=instance.task_state): self._update_usage_from_instance(context, instance, nodename) instance_by_uuid[instance.uuid] = instance return instance_by_uuid @@ -1856,7 +1859,7 @@ def _merge_provider_configs(self, provider_configs, provider_tree): raise ValueError(_( "Provider config '%(source_file_name)s' attempts " "to define a trait that is owned by the " - "virt driver or specified via the placment api. " + "virt driver or specified via the placement api. " "Invalid traits '%(invalid)s' must be removed " "from '%(source_file_name)s'.") % { 'source_file_name': source_file_name, diff --git a/nova/compute/stats.py b/nova/compute/stats.py index cfbee2e6bc1..e9180ec6d6d 100644 --- a/nova/compute/stats.py +++ b/nova/compute/stats.py @@ -105,7 +105,8 @@ def update_stats_for_instance(self, instance, is_removed=False): (vm_state, task_state, os_type, project_id) = \ self._extract_state_from_instance(instance) - if is_removed or vm_state in vm_states.ALLOW_RESOURCE_REMOVAL: + if is_removed or vm_states.allow_resource_removal( + vm_state=vm_state, task_state=task_state): self._decrement("num_instances") self.states.pop(uuid) else: diff --git a/nova/compute/vm_states.py b/nova/compute/vm_states.py index 633894c1ea4..1c4da06d155 100644 --- a/nova/compute/vm_states.py +++ b/nova/compute/vm_states.py @@ -27,6 +27,7 @@ See http://wiki.openstack.org/VMState """ +from nova.compute import task_states from nova.objects import fields @@ -74,5 +75,11 @@ # states we allow to trigger crash dump ALLOW_TRIGGER_CRASH_DUMP = [ACTIVE, PAUSED, RESCUED, RESIZED, ERROR] -# states we allow resources to be freed in -ALLOW_RESOURCE_REMOVAL = [DELETED, SHELVED_OFFLOADED] + +def allow_resource_removal(vm_state, task_state=None): + """(vm_state, task_state) combinations we allow resources to be freed in""" + + return ( + vm_state == DELETED or + vm_state == SHELVED_OFFLOADED and task_state != task_states.SPAWNING + ) diff --git a/nova/conductor/manager.py b/nova/conductor/manager.py index 99e5514136e..53067bbef7c 100644 --- a/nova/conductor/manager.py +++ b/nova/conductor/manager.py @@ -21,6 +21,7 @@ import functools import sys +from keystoneauth1 import exceptions as ks_exc from oslo_config import cfg from oslo_db import exception as db_exc from oslo_limit import exception as limit_exceptions @@ -243,11 +244,42 @@ def __init__(self): self.network_api = neutron.API() self.servicegroup_api = servicegroup.API() self.query_client = query.SchedulerQueryClient() - self.report_client = report.SchedulerReportClient() self.notifier = rpc.get_notifier('compute') # Help us to record host in EventReporter self.host = CONF.host + try: + # Test our placement client during initialization + self.report_client + except (ks_exc.EndpointNotFound, + ks_exc.DiscoveryFailure, + ks_exc.RequestTimeout, + ks_exc.GatewayTimeout, + ks_exc.ConnectFailure) as e: + # Non-fatal, likely transient (although not definitely); + # continue startup but log the warning so that when things + # fail later, it will be clear why we can not do certain + # things. + LOG.warning('Unable to initialize placement client (%s); ' + 'Continuing with startup, but some operations ' + 'will not be possible.', e) + except (ks_exc.MissingAuthPlugin, + ks_exc.Unauthorized) as e: + # This is almost definitely fatal mis-configuration. The + # Unauthorized error might be transient, but it is + # probably reasonable to consider it fatal. + LOG.error('Fatal error initializing placement client; ' + 'config is incorrect or incomplete: %s', e) + raise + except Exception as e: + # Unknown/unexpected errors here are fatal + LOG.error('Fatal error initializing placement client: %s', e) + raise + + @property + def report_client(self): + return report.report_client_singleton() + def reset(self): LOG.info('Reloading compute RPC API') compute_rpcapi.LAST_VERSION = None diff --git a/nova/conductor/tasks/live_migrate.py b/nova/conductor/tasks/live_migrate.py index 1acae88b264..f8819b0dc85 100644 --- a/nova/conductor/tasks/live_migrate.py +++ b/nova/conductor/tasks/live_migrate.py @@ -347,8 +347,9 @@ def _check_compatible_with_source_hypervisor(self, destination): source_version = source_info.hypervisor_version destination_version = destination_info.hypervisor_version - if source_version > destination_version: - raise exception.DestinationHypervisorTooOld() + if not CONF.workarounds.skip_hypervisor_version_check_on_lm: + if source_version > destination_version: + raise exception.DestinationHypervisorTooOld() return source_info, destination_info def _call_livem_checks_on_host(self, destination, provider_mapping): diff --git a/nova/conductor/tasks/migrate.py b/nova/conductor/tasks/migrate.py index 6ff6206f659..8838d0240a6 100644 --- a/nova/conductor/tasks/migrate.py +++ b/nova/conductor/tasks/migrate.py @@ -54,7 +54,7 @@ def replace_allocation_with_migration(context, instance, migration): # and do any rollback required raise - reportclient = report.SchedulerReportClient() + reportclient = report.report_client_singleton() orig_alloc = reportclient.get_allocs_for_consumer( context, instance.uuid)['allocations'] @@ -94,7 +94,7 @@ def replace_allocation_with_migration(context, instance, migration): def revert_allocation_for_migration(context, source_cn, instance, migration): """Revert an allocation made for a migration back to the instance.""" - reportclient = report.SchedulerReportClient() + reportclient = report.report_client_singleton() # FIXME(gibi): This method is flawed in that it does not handle allocations # against sharing providers in any special way. This leads to duplicate diff --git a/nova/conf/compute.py b/nova/conf/compute.py index 263d7775869..cd4e1706d4a 100644 --- a/nova/conf/compute.py +++ b/nova/conf/compute.py @@ -200,7 +200,7 @@ top-level key called ``interfaces``. This key will contain a list of dictionaries, one for each interface. -Refer to the cloudinit documentaion for more information: +Refer to the cloudinit documentation for more information: https://cloudinit.readthedocs.io/en/latest/topics/datasources.html @@ -426,9 +426,7 @@ Virtual CPU to physical CPU allocation ratio. This option is used to influence the hosts selected by the Placement API by -configuring the allocation ratio for ``VCPU`` inventory. In addition, the -``AggregateCoreFilter`` (deprecated) will fall back to this configuration value -if no per-aggregate setting is found. +configuring the allocation ratio for ``VCPU`` inventory. .. note:: @@ -459,9 +457,7 @@ Virtual RAM to physical RAM allocation ratio. This option is used to influence the hosts selected by the Placement API by -configuring the allocation ratio for ``MEMORY_MB`` inventory. In addition, the -``AggregateRamFilter`` (deprecated) will fall back to this configuration value -if no per-aggregate setting is found. +configuring the allocation ratio for ``MEMORY_MB`` inventory. .. note:: @@ -487,9 +483,7 @@ Virtual disk to physical disk allocation ratio. This option is used to influence the hosts selected by the Placement API by -configuring the allocation ratio for ``DISK_GB`` inventory. In addition, the -``AggregateDiskFilter`` (deprecated) will fall back to this configuration value -if no per-aggregate setting is found. +configuring the allocation ratio for ``DISK_GB`` inventory. When configured, a ratio greater than 1.0 will result in over-subscription of the available physical disk, which can be useful for more efficiently packing @@ -1007,6 +1001,15 @@ * ``[scheduler]query_placement_for_image_type_support`` - enables filtering computes based on supported image types, which is required to be enabled for this to take effect. +"""), + cfg.ListOpt('vmdk_allowed_types', + default=['streamOptimized', 'monolithicSparse'], + help=""" +A list of strings describing allowed VMDK "create-type" subformats +that will be allowed. This is recommended to only include +single-file-with-sparse-header variants to avoid potential host file +exposure due to processing named extents. If this list is empty, then no +form of VMDK image will be allowed. """), cfg.BoolOpt('packing_host_numa_cells_allocation_strategy', default=True, diff --git a/nova/conf/hyperv.py b/nova/conf/hyperv.py index caa7a8702b8..cce3cdc3e2d 100644 --- a/nova/conf/hyperv.py +++ b/nova/conf/hyperv.py @@ -320,7 +320,7 @@ cfg.ListOpt('iscsi_initiator_list', default=[], help=""" -List of iSCSI initiators that will be used for estabilishing iSCSI sessions. +List of iSCSI initiators that will be used for establishing iSCSI sessions. If none are specified, the Microsoft iSCSI initiator service will choose the initiator. diff --git a/nova/conf/libvirt.py b/nova/conf/libvirt.py index 7d9c837ba56..4ea37b8fe97 100644 --- a/nova/conf/libvirt.py +++ b/nova/conf/libvirt.py @@ -453,7 +453,7 @@ Prerequisite: TLS environment is configured correctly on all relevant Compute nodes. This means, Certificate Authority (CA), server, client -certificates, their corresponding keys, and their file permisssions are +certificates, their corresponding keys, and their file permissions are in place, and are validated. Notes: @@ -705,7 +705,7 @@ returns random numbers when read) is accepted. The recommended source of entropy is ``/dev/urandom`` -- it is non-blocking, therefore relatively fast; and avoids the limitations of ``/dev/random``, which is -a legacy interface. For more details (and comparision between different +a legacy interface. For more details (and comparison between different RNG sources), refer to the "Usage" section in the Linux kernel API documentation for ``[u]random``: http://man7.org/linux/man-pages/man4/urandom.4.html and diff --git a/nova/conf/neutron.py b/nova/conf/neutron.py index dc391a268e8..e6774ced55a 100644 --- a/nova/conf/neutron.py +++ b/nova/conf/neutron.py @@ -46,7 +46,7 @@ Specifies the name of floating IP pool used for allocating floating IPs. This option is only used if Neutron does not specify the floating IP pool name in -port binding reponses. +port binding responses. """), cfg.IntOpt('extension_sync_interval', default=600, diff --git a/nova/conf/quota.py b/nova/conf/quota.py index 0d51129d503..e5b4b8dc738 100644 --- a/nova/conf/quota.py +++ b/nova/conf/quota.py @@ -147,7 +147,7 @@ deprecated_group='DEFAULT', deprecated_name='quota_server_groups', help=""" -The maxiumum number of server groups per project. +The maximum number of server groups per project. Server groups are used to control the affinity and anti-affinity scheduling policy for a group of servers or instances. Reducing the quota will not affect diff --git a/nova/conf/scheduler.py b/nova/conf/scheduler.py index 8b3b6169873..03e78fe7017 100644 --- a/nova/conf/scheduler.py +++ b/nova/conf/scheduler.py @@ -780,7 +780,7 @@ Possible values: -* An integer or float value, where the value corresponds to the multipler +* An integer or float value, where the value corresponds to the multiplier ratio for this weigher. Related options: @@ -857,7 +857,7 @@ Possible values: -* An integer or float value, where the value corresponds to the multipler +* An integer or float value, where the value corresponds to the multiplier ratio for this weigher. Related options: diff --git a/nova/conf/workarounds.py b/nova/conf/workarounds.py index 7419f073b49..55d810cce89 100644 --- a/nova/conf/workarounds.py +++ b/nova/conf/workarounds.py @@ -401,6 +401,31 @@ Related options: * :oslo.config:option:`quota.driver` +"""), + cfg.BoolOpt('skip_cpu_compare_on_dest', + default=False, + help=""" +With the libvirt driver, during live migration, skip comparing guest CPU +with the destination host. When using QEMU >= 2.9 and libvirt >= +4.4.0, libvirt will do the correct thing with respect to checking CPU +compatibility on the destination host during live migration. +"""), + cfg.BoolOpt( + 'skip_hypervisor_version_check_on_lm', + default=False, + help=""" +When this is enabled, it will skip version-checking of hypervisors +during live migration. +"""), + cfg.BoolOpt( + 'disable_deep_image_inspection', + default=False, + help=""" +This disables the additional deep image inspection that the compute node does +when downloading from glance. This includes backing-file, data-file, and +known-features detection *before* passing the image to qemu-img. Generally, +this inspection should be enabled for maximum safety, but this workaround +option allows disabling it if there is a compatibility concern. """), ] diff --git a/nova/db/main/api.py b/nova/db/main/api.py index 4c40be905ef..39775d4f461 100644 --- a/nova/db/main/api.py +++ b/nova/db/main/api.py @@ -4176,6 +4176,12 @@ def _get_fk_stmts(metadata, conn, table, column, records): fk_column = fk_table.c.id for fk in fk_table.foreign_keys: + if table != fk.column.table: + # if the foreign key doesn't actually point to the table we're + # archiving entries from then it's not relevant; trying to + # resolve this would result in a cartesian product + continue + # We need to find the records in the referring (child) table that # correspond to the records in our (parent) table so we can archive # them. @@ -4225,6 +4231,7 @@ def _get_fk_stmts(metadata, conn, table, column, records): # deque. fk_delete = fk_table.delete().where(fk_column.in_(fk_records)) deletes.appendleft(fk_delete) + # Repeat for any possible nested child tables. i, d = _get_fk_stmts(metadata, conn, fk_table, fk_column, fk_records) inserts.extendleft(i) diff --git a/nova/exception.py b/nova/exception.py index 4588898aae9..2d6ce3785b6 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -1477,6 +1477,15 @@ class RescheduledException(NovaException): "%(reason)s") +class RescheduledByPolicyException(RescheduledException): + msg_fmt = _("Build of instance %(instance_uuid)s was re-scheduled: " + "%(reason)s") + + +class GroupAffinityViolation(NovaException): + msg_fmt = _("%(policy)s instance group policy was violated") + + class InstanceFaultRollback(NovaException): def __init__(self, inner_exception=None): message = _("Instance rollback performed due to: %s") diff --git a/nova/image/format_inspector.py b/nova/image/format_inspector.py new file mode 100644 index 00000000000..49cb75930a9 --- /dev/null +++ b/nova/image/format_inspector.py @@ -0,0 +1,1038 @@ +# Copyright 2020 Red Hat, Inc +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +This is a python implementation of virtual disk format inspection routines +gathered from various public specification documents, as well as qemu disk +driver code. It attempts to store and parse the minimum amount of data +required, and in a streaming-friendly manner to collect metadata about +complex-format images. +""" + +import struct + +from oslo_log import log as logging +from oslo_utils import units + +LOG = logging.getLogger(__name__) + + +def chunked_reader(fileobj, chunk_size=512): + while True: + chunk = fileobj.read(chunk_size) + if not chunk: + break + yield chunk + + +class CaptureRegion(object): + """Represents a region of a file we want to capture. + + A region of a file we want to capture requires a byte offset into + the file and a length. This is expected to be used by a data + processing loop, calling capture() with the most recently-read + chunk. This class handles the task of grabbing the desired region + of data across potentially multiple fractional and unaligned reads. + + :param offset: Byte offset into the file starting the region + :param length: The length of the region + """ + + def __init__(self, offset, length): + self.offset = offset + self.length = length + self.data = b'' + + @property + def complete(self): + """Returns True when we have captured the desired data.""" + return self.length == len(self.data) + + def capture(self, chunk, current_position): + """Process a chunk of data. + + This should be called for each chunk in the read loop, at least + until complete returns True. + + :param chunk: A chunk of bytes in the file + :param current_position: The position of the file processed by the + read loop so far. Note that this will be + the position in the file *after* the chunk + being presented. + """ + read_start = current_position - len(chunk) + if (read_start <= self.offset <= current_position or + self.offset <= read_start <= (self.offset + self.length)): + if read_start < self.offset: + lead_gap = self.offset - read_start + else: + lead_gap = 0 + self.data += chunk[lead_gap:] + self.data = self.data[:self.length] + + +class ImageFormatError(Exception): + """An unrecoverable image format error that aborts the process.""" + pass + + +class TraceDisabled(object): + """A logger-like thing that swallows tracing when we do not want it.""" + + def debug(self, *a, **k): + pass + + info = debug + warning = debug + error = debug + + +class FileInspector(object): + """A stream-based disk image inspector. + + This base class works on raw images and is subclassed for more + complex types. It is to be presented with the file to be examined + one chunk at a time, during read processing and will only store + as much data as necessary to determine required attributes of + the file. + """ + + def __init__(self, tracing=False): + self._total_count = 0 + + # NOTE(danms): The logging in here is extremely verbose for a reason, + # but should never really be enabled at that level at runtime. To + # retain all that work and assist in future debug, we have a separate + # debug flag that can be passed from a manual tool to turn it on. + if tracing: + self._log = logging.getLogger(str(self)) + else: + self._log = TraceDisabled() + self._capture_regions = {} + + def _capture(self, chunk, only=None): + for name, region in self._capture_regions.items(): + if only and name not in only: + continue + if not region.complete: + region.capture(chunk, self._total_count) + + def eat_chunk(self, chunk): + """Call this to present chunks of the file to the inspector.""" + pre_regions = set(self._capture_regions.keys()) + + # Increment our position-in-file counter + self._total_count += len(chunk) + + # Run through the regions we know of to see if they want this + # data + self._capture(chunk) + + # Let the format do some post-read processing of the stream + self.post_process() + + # Check to see if the post-read processing added new regions + # which may require the current chunk. + new_regions = set(self._capture_regions.keys()) - pre_regions + if new_regions: + self._capture(chunk, only=new_regions) + + def post_process(self): + """Post-read hook to process what has been read so far. + + This will be called after each chunk is read and potentially captured + by the defined regions. If any regions are defined by this call, + those regions will be presented with the current chunk in case it + is within one of the new regions. + """ + pass + + def region(self, name): + """Get a CaptureRegion by name.""" + return self._capture_regions[name] + + def new_region(self, name, region): + """Add a new CaptureRegion by name.""" + if self.has_region(name): + # This is a bug, we tried to add the same region twice + raise ImageFormatError('Inspector re-added region %s' % name) + self._capture_regions[name] = region + + def has_region(self, name): + """Returns True if named region has been defined.""" + return name in self._capture_regions + + @property + def format_match(self): + """Returns True if the file appears to be the expected format.""" + return True + + @property + def virtual_size(self): + """Returns the virtual size of the disk image, or zero if unknown.""" + return self._total_count + + @property + def actual_size(self): + """Returns the total size of the file, usually smaller than + virtual_size. NOTE: this will only be accurate if the entire + file is read and processed. + """ + return self._total_count + + @property + def complete(self): + """Returns True if we have all the information needed.""" + return all(r.complete for r in self._capture_regions.values()) + + def __str__(self): + """The string name of this file format.""" + return 'raw' + + @property + def context_info(self): + """Return info on amount of data held in memory for auditing. + + This is a dict of region:sizeinbytes items that the inspector + uses to examine the file. + """ + return {name: len(region.data) for name, region in + self._capture_regions.items()} + + @classmethod + def from_file(cls, filename): + """Read as much of a file as necessary to complete inspection. + + NOTE: Because we only read as much of the file as necessary, the + actual_size property will not reflect the size of the file, but the + amount of data we read before we satisfied the inspector. + + Raises ImageFormatError if we cannot parse the file. + """ + inspector = cls() + with open(filename, 'rb') as f: + for chunk in chunked_reader(f): + inspector.eat_chunk(chunk) + if inspector.complete: + # No need to eat any more data + break + if not inspector.complete or not inspector.format_match: + raise ImageFormatError('File is not in requested format') + return inspector + + def safety_check(self): + """Perform some checks to determine if this file is safe. + + Returns True if safe, False otherwise. It may raise ImageFormatError + if safety cannot be guaranteed because of parsing or other errors. + """ + return True + + +# The qcow2 format consists of a big-endian 72-byte header, of which +# only a small portion has information we care about: +# +# Dec Hex Name +# 0 0x00 Magic 4-bytes 'QFI\xfb' +# 4 0x04 Version (uint32_t, should always be 2 for modern files) +# . . . +# 8 0x08 Backing file offset (uint64_t) +# 24 0x18 Size in bytes (unint64_t) +# . . . +# 72 0x48 Incompatible features bitfield (6 bytes) +# +# https://gitlab.com/qemu-project/qemu/-/blob/master/docs/interop/qcow2.txt +class QcowInspector(FileInspector): + """QEMU QCOW2 Format + + This should only require about 32 bytes of the beginning of the file + to determine the virtual size, and 104 bytes to perform the safety check. + """ + + BF_OFFSET = 0x08 + BF_OFFSET_LEN = 8 + I_FEATURES = 0x48 + I_FEATURES_LEN = 8 + I_FEATURES_DATAFILE_BIT = 3 + I_FEATURES_MAX_BIT = 4 + + def __init__(self, *a, **k): + super(QcowInspector, self).__init__(*a, **k) + self.new_region('header', CaptureRegion(0, 512)) + + def _qcow_header_data(self): + magic, version, bf_offset, bf_sz, cluster_bits, size = ( + struct.unpack('>4sIQIIQ', self.region('header').data[:32])) + return magic, size + + @property + def has_header(self): + return self.region('header').complete + + @property + def virtual_size(self): + if not self.region('header').complete: + return 0 + if not self.format_match: + return 0 + magic, size = self._qcow_header_data() + return size + + @property + def format_match(self): + if not self.region('header').complete: + return False + magic, size = self._qcow_header_data() + return magic == b'QFI\xFB' + + @property + def has_backing_file(self): + if not self.region('header').complete: + return None + if not self.format_match: + return False + bf_offset_bytes = self.region('header').data[ + self.BF_OFFSET:self.BF_OFFSET + self.BF_OFFSET_LEN] + # nonzero means "has a backing file" + bf_offset, = struct.unpack('>Q', bf_offset_bytes) + return bf_offset != 0 + + @property + def has_unknown_features(self): + if not self.region('header').complete: + return None + if not self.format_match: + return False + i_features = self.region('header').data[ + self.I_FEATURES:self.I_FEATURES + self.I_FEATURES_LEN] + + # This is the maximum byte number we should expect any bits to be set + max_byte = self.I_FEATURES_MAX_BIT // 8 + + # The flag bytes are in big-endian ordering, so if we process + # them in index-order, they're reversed + for i, byte_num in enumerate(reversed(range(self.I_FEATURES_LEN))): + if byte_num == max_byte: + # If we're in the max-allowed byte, allow any bits less than + # the maximum-known feature flag bit to be set + allow_mask = ((1 << self.I_FEATURES_MAX_BIT) - 1) + elif byte_num > max_byte: + # If we're above the byte with the maximum known feature flag + # bit, then we expect all zeroes + allow_mask = 0x0 + else: + # Any earlier-than-the-maximum byte can have any of the flag + # bits set + allow_mask = 0xFF + + if i_features[i] & ~allow_mask: + LOG.warning('Found unknown feature bit in byte %i: %s/%s', + byte_num, bin(i_features[byte_num] & ~allow_mask), + bin(allow_mask)) + return True + + return False + + @property + def has_data_file(self): + if not self.region('header').complete: + return None + if not self.format_match: + return False + i_features = self.region('header').data[ + self.I_FEATURES:self.I_FEATURES + self.I_FEATURES_LEN] + + # First byte of bitfield, which is i_features[7] + byte = self.I_FEATURES_LEN - 1 - self.I_FEATURES_DATAFILE_BIT // 8 + # Third bit of bitfield, which is 0x04 + bit = 1 << (self.I_FEATURES_DATAFILE_BIT - 1 % 8) + return bool(i_features[byte] & bit) + + def __str__(self): + return 'qcow2' + + def safety_check(self): + return (not self.has_backing_file and + not self.has_data_file and + not self.has_unknown_features) + + +class QEDInspector(FileInspector): + def __init__(self, tracing=False): + super().__init__(tracing) + self.new_region('header', CaptureRegion(0, 512)) + + @property + def format_match(self): + if not self.region('header').complete: + return False + return self.region('header').data.startswith(b'QED\x00') + + def safety_check(self): + # QED format is not supported by anyone, but we want to detect it + # and mark it as just always unsafe. + return False + + +# The VHD (or VPC as QEMU calls it) format consists of a big-endian +# 512-byte "footer" at the beginning of the file with various +# information, most of which does not matter to us: +# +# Dec Hex Name +# 0 0x00 Magic string (8-bytes, always 'conectix') +# 40 0x28 Disk size (uint64_t) +# +# https://github.com/qemu/qemu/blob/master/block/vpc.c +class VHDInspector(FileInspector): + """Connectix/MS VPC VHD Format + + This should only require about 512 bytes of the beginning of the file + to determine the virtual size. + """ + + def __init__(self, *a, **k): + super(VHDInspector, self).__init__(*a, **k) + self.new_region('header', CaptureRegion(0, 512)) + + @property + def format_match(self): + return self.region('header').data.startswith(b'conectix') + + @property + def virtual_size(self): + if not self.region('header').complete: + return 0 + + if not self.format_match: + return 0 + + return struct.unpack('>Q', self.region('header').data[40:48])[0] + + def __str__(self): + return 'vhd' + + +# The VHDX format consists of a complex dynamic little-endian +# structure with multiple regions of metadata and data, linked by +# offsets with in the file (and within regions), identified by MSFT +# GUID strings. The header is a 320KiB structure, only a few pieces of +# which we actually need to capture and interpret: +# +# Dec Hex Name +# 0 0x00000 Identity (Technically 9-bytes, padded to 64KiB, the first +# 8 bytes of which are 'vhdxfile') +# 196608 0x30000 The Region table (64KiB of a 32-byte header, followed +# by up to 2047 36-byte region table entry structures) +# +# The region table header includes two items we need to read and parse, +# which are: +# +# 196608 0x30000 4-byte signature ('regi') +# 196616 0x30008 Entry count (uint32-t) +# +# The region table entries follow the region table header immediately +# and are identified by a 16-byte GUID, and provide an offset of the +# start of that region. We care about the "metadata region", identified +# by the METAREGION class variable. The region table entry is (offsets +# from the beginning of the entry, since it could be in multiple places): +# +# 0 0x00000 16-byte MSFT GUID +# 16 0x00010 Offset of the actual metadata region (uint64_t) +# +# When we find the METAREGION table entry, we need to grab that offset +# and start examining the region structure at that point. That +# consists of a metadata table of structures, which point to places in +# the data in an unstructured space that follows. The header is +# (offsets relative to the region start): +# +# 0 0x00000 8-byte signature ('metadata') +# . . . +# 16 0x00010 2-byte entry count (up to 2047 entries max) +# +# This header is followed by the specified number of metadata entry +# structures, identified by GUID: +# +# 0 0x00000 16-byte MSFT GUID +# 16 0x00010 4-byte offset (uint32_t, relative to the beginning of +# the metadata region) +# +# We need to find the "Virtual Disk Size" metadata item, identified by +# the GUID in the VIRTUAL_DISK_SIZE class variable, grab the offset, +# add it to the offset of the metadata region, and examine that 8-byte +# chunk of data that follows. +# +# The "Virtual Disk Size" is a naked uint64_t which contains the size +# of the virtual disk, and is our ultimate target here. +# +# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-vhdx/83e061f8-f6e2-4de1-91bd-5d518a43d477 +class VHDXInspector(FileInspector): + """MS VHDX Format + + This requires some complex parsing of the stream. The first 256KiB + of the image is stored to get the header and region information, + and then we capture the first metadata region to read those + records, find the location of the virtual size data and parse + it. This needs to store the metadata table entries up until the + VDS record, which may consist of up to 2047 32-byte entries at + max. Finally, it must store a chunk of data at the offset of the + actual VDS uint64. + + """ + METAREGION = '8B7CA206-4790-4B9A-B8FE-575F050F886E' + VIRTUAL_DISK_SIZE = '2FA54224-CD1B-4876-B211-5DBED83BF4B8' + VHDX_METADATA_TABLE_MAX_SIZE = 32 * 2048 # From qemu + + def __init__(self, *a, **k): + super(VHDXInspector, self).__init__(*a, **k) + self.new_region('ident', CaptureRegion(0, 32)) + self.new_region('header', CaptureRegion(192 * 1024, 64 * 1024)) + + def post_process(self): + # After reading a chunk, we may have the following conditions: + # + # 1. We may have just completed the header region, and if so, + # we need to immediately read and calculate the location of + # the metadata region, as it may be starting in the same + # read we just did. + # 2. We may have just completed the metadata region, and if so, + # we need to immediately calculate the location of the + # "virtual disk size" record, as it may be starting in the + # same read we just did. + if self.region('header').complete and not self.has_region('metadata'): + region = self._find_meta_region() + if region: + self.new_region('metadata', region) + elif self.has_region('metadata') and not self.has_region('vds'): + region = self._find_meta_entry(self.VIRTUAL_DISK_SIZE) + if region: + self.new_region('vds', region) + + @property + def format_match(self): + return self.region('ident').data.startswith(b'vhdxfile') + + @staticmethod + def _guid(buf): + """Format a MSFT GUID from the 16-byte input buffer.""" + guid_format = '= 2048: + raise ImageFormatError('Region count is %i (limit 2047)' % count) + + # Process the regions until we find the metadata one; grab the + # offset and return + self._log.debug('Region entry first is %x', region_entry_first) + self._log.debug('Region entries %i', count) + meta_offset = 0 + for i in range(0, count): + entry_start = region_entry_first + (i * 32) + entry_end = entry_start + 32 + entry = self.region('header').data[entry_start:entry_end] + self._log.debug('Entry offset is %x', entry_start) + + # GUID is the first 16 bytes + guid = self._guid(entry[:16]) + if guid == self.METAREGION: + # This entry is the metadata region entry + meta_offset, meta_len, meta_req = struct.unpack( + '= 2048: + raise ImageFormatError( + 'Metadata item count is %i (limit 2047)' % count) + + for i in range(0, count): + entry_offset = 32 + (i * 32) + guid = self._guid(meta_buffer[entry_offset:entry_offset + 16]) + if guid == desired_guid: + # Found the item we are looking for by id. + # Stop our region from capturing + item_offset, item_length, _reserved = struct.unpack( + ' 1: + all_formats = [str(inspector) for inspector in detections] + raise ImageFormatError( + 'Multiple formats detected: %s' % ', '.join(all_formats)) + + return inspectors['raw'] if not detections else detections[0] diff --git a/nova/limit/placement.py b/nova/limit/placement.py index 497986c4ab8..eedf7d69e19 100644 --- a/nova/limit/placement.py +++ b/nova/limit/placement.py @@ -43,10 +43,8 @@ def _get_placement_usages( context: 'nova.context.RequestContext', project_id: str ) -> ty.Dict[str, int]: - global PLACEMENT_CLIENT - if not PLACEMENT_CLIENT: - PLACEMENT_CLIENT = report.SchedulerReportClient() - return PLACEMENT_CLIENT.get_usages_counts_for_limits(context, project_id) + return report.report_client_singleton().get_usages_counts_for_limits( + context, project_id) def _get_usage( diff --git a/nova/network/neutron.py b/nova/network/neutron.py index 3ee9774d24c..cc9efa79d71 100644 --- a/nova/network/neutron.py +++ b/nova/network/neutron.py @@ -223,13 +223,15 @@ def _get_auth_plugin(context, admin=False): # support some services (metadata API) where an admin context is used # without an auth token. global _ADMIN_AUTH + user_auth = None if admin or (context.is_admin and not context.auth_token): if not _ADMIN_AUTH: _ADMIN_AUTH = _load_auth_plugin(CONF) - return _ADMIN_AUTH + user_auth = _ADMIN_AUTH - if context.auth_token: - return service_auth.get_auth_plugin(context) + if context.auth_token or user_auth: + # When user_auth = None, user_auth will be extracted from the context. + return service_auth.get_auth_plugin(context, user_auth=user_auth) # We did not get a user token and we should not be using # an admin token so log an error @@ -636,6 +638,7 @@ def _unbind_ports(self, context, ports, # in case the caller forgot to filter the list. if port_id is None: continue + port_req_body: ty.Dict[str, ty.Any] = { 'port': { 'device_id': '', @@ -650,7 +653,7 @@ def _unbind_ports(self, context, ports, except exception.PortNotFound: LOG.debug('Unable to show port %s as it no longer ' 'exists.', port_id) - return + continue except Exception: # NOTE: In case we can't retrieve the binding:profile or # network info assume that they are empty @@ -683,7 +686,8 @@ def _unbind_ports(self, context, ports, for profile_key in ('pci_vendor_info', 'pci_slot', constants.ALLOCATION, 'arq_uuid', 'physical_network', 'card_serial_number', - 'vf_num', 'pf_mac_address'): + 'vf_num', 'pf_mac_address', + 'device_mac_address'): if profile_key in port_profile: del port_profile[profile_key] port_req_body['port'][constants.BINDING_PROFILE] = port_profile @@ -1306,6 +1310,10 @@ def _update_ports_for_instance(self, context, instance, neutron, network=network, neutron=neutron, bind_host_id=bind_host_id, port_arq=port_arq) + # NOTE(gibi): Remove this once we are sure that the fix for + # bug 1942329 is always present in the deployed neutron. The + # _populate_neutron_extension_values() call above already + # populated this MAC to the binding profile instead. self._populate_pci_mac_address(instance, request.pci_request_id, port_req_body) @@ -1621,6 +1629,18 @@ def _get_pci_device_profile(self, pci_dev): if pci_dev.dev_type == obj_fields.PciDeviceType.SRIOV_VF: dev_profile.update( self._get_vf_pci_device_profile(pci_dev)) + + if pci_dev.dev_type == obj_fields.PciDeviceType.SRIOV_PF: + # In general the MAC address information flows fom the neutron + # port to the device in the backend. Except for direct-physical + # ports. In that case the MAC address flows from the physical + # device, the PF, to the neutron port. So when such a port is + # being bound to a host the port's MAC address needs to be + # updated. Nova needs to put the new MAC into the binding + # profile. + if pci_dev.mac_address: + dev_profile['device_mac_address'] = pci_dev.mac_address + return dev_profile raise exception.PciDeviceNotFound(node_id=pci_dev.compute_node_id, @@ -3365,6 +3385,25 @@ def _build_vif_model(self, context, client, current_neutron_port, delegate_create=True, ) + def _log_error_if_vnic_type_changed( + self, port_id, old_vnic_type, new_vnic_type, instance + ): + if old_vnic_type and old_vnic_type != new_vnic_type: + LOG.error( + 'The vnic_type of the bound port %s has ' + 'been changed in neutron from "%s" to ' + '"%s". Changing vnic_type of a bound port ' + 'is not supported by Nova. To avoid ' + 'breaking the connectivity of the instance ' + 'please change the port vnic_type back to ' + '"%s".', + port_id, + old_vnic_type, + new_vnic_type, + old_vnic_type, + instance=instance + ) + def _build_network_info_model(self, context, instance, networks=None, port_ids=None, admin_client=None, preexisting_port_ids=None, @@ -3438,6 +3477,12 @@ def _build_network_info_model(self, context, instance, networks=None, preexisting_port_ids) for index, vif in enumerate(nw_info): if vif['id'] == refresh_vif_id: + self._log_error_if_vnic_type_changed( + vif['id'], + vif['vnic_type'], + refreshed_vif['vnic_type'], + instance, + ) # Update the existing entry. nw_info[index] = refreshed_vif LOG.debug('Updated VIF entry in instance network ' @@ -3487,6 +3532,7 @@ def _build_network_info_model(self, context, instance, networks=None, networks, port_ids = self._gather_port_ids_and_networks( context, instance, networks, port_ids, client) + old_nw_info = instance.get_network_info() nw_info = network_model.NetworkInfo() for port_id in port_ids: current_neutron_port = current_neutron_port_map.get(port_id) @@ -3494,6 +3540,14 @@ def _build_network_info_model(self, context, instance, networks=None, vif = self._build_vif_model( context, client, current_neutron_port, networks, preexisting_port_ids) + for old_vif in old_nw_info: + if old_vif['id'] == port_id: + self._log_error_if_vnic_type_changed( + port_id, + old_vif['vnic_type'], + vif['vnic_type'], + instance, + ) nw_info.append(vif) elif nw_info_refresh: LOG.info('Port %s from network info_cache is no ' @@ -3663,11 +3717,10 @@ def _get_pci_mapping_for_migration(self, instance, migration): migration.get('status') == 'reverted') return instance.migration_context.get_pci_mapping_for_migration(revert) - def _get_port_pci_dev(self, context, instance, port): + def _get_port_pci_dev(self, instance, port): """Find the PCI device corresponding to the port. Assumes the port is an SRIOV one. - :param context: The request context. :param instance: The instance to which the port is attached. :param port: The Neutron port, as obtained from the Neutron API JSON form. @@ -3693,25 +3746,6 @@ def _get_port_pci_dev(self, context, instance, port): return None return device - def _update_port_pci_binding_profile(self, pci_dev, binding_profile): - """Update the binding profile dict with new PCI device data. - - :param pci_dev: The PciDevice object to update the profile with. - :param binding_profile: The dict to update. - """ - binding_profile.update({'pci_slot': pci_dev.address}) - if binding_profile.get('card_serial_number'): - binding_profile.update({ - 'card_serial_number': pci_dev.card_serial_number}) - if binding_profile.get('pf_mac_address'): - binding_profile.update({ - 'pf_mac_address': pci_utils.get_mac_by_pci_address( - pci_dev.parent_addr)}) - if binding_profile.get('vf_num'): - binding_profile.update({ - 'vf_num': pci_utils.get_vf_num_by_pci_address( - pci_dev.address)}) - def _update_port_binding_for_instance( self, context, instance, host, migration=None, provider_mappings=None): @@ -3774,14 +3808,14 @@ def _update_port_binding_for_instance( raise exception.PortUpdateFailed(port_id=p['id'], reason=_("Unable to correlate PCI slot %s") % pci_slot) - # NOTE(artom) If migration is None, this is an unshevle, and we - # need to figure out the pci_slot from the InstancePCIRequest - # and PciDevice objects. + # NOTE(artom) If migration is None, this is an unshelve, and we + # need to figure out the pci related binding information from + # the InstancePCIRequest and PciDevice objects. else: - pci_dev = self._get_port_pci_dev(context, instance, p) + pci_dev = self._get_port_pci_dev(instance, p) if pci_dev: - self._update_port_pci_binding_profile(pci_dev, - binding_profile) + binding_profile.update( + self._get_pci_device_profile(pci_dev)) updates[constants.BINDING_PROFILE] = binding_profile # NOTE(gibi): during live migration the conductor already sets the @@ -3874,7 +3908,7 @@ def get_segment_ids_for_network( either Segment extension isn't enabled in Neutron or if the network isn't configured for routing. """ - client = get_client(context) + client = get_client(context, admin=True) if not self.has_segment_extension(client=client): return [] @@ -3890,7 +3924,7 @@ def get_segment_ids_for_network( 'Failed to get segment IDs for network %s' % network_id) from e # The segment field of an unconfigured subnet could be None return [subnet['segment_id'] for subnet in subnets - if subnet['segment_id'] is not None] + if subnet.get('segment_id') is not None] def get_segment_id_for_subnet( self, @@ -3905,7 +3939,7 @@ def get_segment_id_for_subnet( extension isn't enabled in Neutron or the provided subnet doesn't have segments (if the related network isn't configured for routing) """ - client = get_client(context) + client = get_client(context, admin=True) if not self.has_segment_extension(client=client): return None diff --git a/nova/objects/cell_mapping.py b/nova/objects/cell_mapping.py index 595ec43e480..13551824205 100644 --- a/nova/objects/cell_mapping.py +++ b/nova/objects/cell_mapping.py @@ -279,11 +279,15 @@ def _get_by_project_id_from_db(context, project_id): # SELECT DISTINCT cell_id FROM instance_mappings \ # WHERE project_id = $project_id; cell_ids = context.session.query( - api_db_models.InstanceMapping.cell_id).filter_by( - project_id=project_id).distinct().subquery() + api_db_models.InstanceMapping.cell_id + ).filter_by( + project_id=project_id + ).distinct() # SELECT cell_mappings WHERE cell_id IN ($cell_ids); - return context.session.query(api_db_models.CellMapping).filter( - api_db_models.CellMapping.id.in_(cell_ids)).all() + return context.session.query( + api_db_models.CellMapping).filter( + api_db_models.CellMapping.id.in_(cell_ids) + ).all() @classmethod def get_by_project_id(cls, context, project_id): diff --git a/nova/objects/instance.py b/nova/objects/instance.py index e99762d2777..894b8d2a19c 100644 --- a/nova/objects/instance.py +++ b/nova/objects/instance.py @@ -1089,6 +1089,11 @@ def clear_numa_topology(self): def obj_load_attr(self, attrname): # NOTE(danms): We can't lazy-load anything without a context and a uuid if not self._context: + if 'uuid' in self: + LOG.debug( + "Lazy-load of '%s' attempted by orphaned instance", + attrname, instance=self + ) raise exception.OrphanedObjectError(method='obj_load_attr', objtype=self.obj_name()) if 'uuid' not in self: diff --git a/nova/objects/migrate_data.py b/nova/objects/migrate_data.py index 06f30342e54..cf0e4bf9a31 100644 --- a/nova/objects/migrate_data.py +++ b/nova/objects/migrate_data.py @@ -279,6 +279,9 @@ def obj_make_compatible(self, primitive, target_version): if (target_version < (1, 10) and 'src_supports_numa_live_migration' in primitive): del primitive['src_supports_numa_live_migration'] + if (target_version < (1, 10) and + 'dst_supports_numa_live_migration' in primitive): + del primitive['dst_supports_numa_live_migration'] if target_version < (1, 10) and 'dst_numa_info' in primitive: del primitive['dst_numa_info'] if target_version < (1, 9) and 'vifs' in primitive: diff --git a/nova/objects/pci_device.py b/nova/objects/pci_device.py index 275d5da3564..f30555849c3 100644 --- a/nova/objects/pci_device.py +++ b/nova/objects/pci_device.py @@ -148,6 +148,12 @@ def obj_make_compatible(self, primitive, target_version): reason='dev_type=%s not supported in version %s' % ( dev_type, target_version)) + def __repr__(self): + return ( + f'PciDevice(address={self.address}, ' + f'compute_node_id={self.compute_node_id})' + ) + def update_device(self, dev_dict): """Sync the content from device dictionary to device object. @@ -175,6 +181,9 @@ def update_device(self, dev_dict): # NOTE(ralonsoh): list of parameters currently added to # "extra_info" dict: # - "capabilities": dict of (strings/list of strings) + # - "parent_ifname": the netdev name of the parent (PF) + # device of a VF + # - "mac_address": the MAC address of the PF extra_info = self.extra_info data = v if isinstance(v, str) else jsonutils.dumps(v) extra_info.update({k: data}) @@ -346,10 +355,40 @@ def claim(self, instance_uuid): # Update PF status to CLAIMED if all of it dependants are free # and set their status to UNCLAIMABLE vfs_list = self.child_devices - if not all([vf.is_available() for vf in vfs_list]): - raise exception.PciDeviceVFInvalidStatus( - compute_node_id=self.compute_node_id, - address=self.address) + non_free_dependants = [ + vf for vf in vfs_list if not vf.is_available()] + if non_free_dependants: + # NOTE(gibi): There should not be any dependent devices that + # are UNCLAIMABLE or UNAVAILABLE as the parent is AVAILABLE, + # but we got reports in bug 1969496 that this inconsistency + # can happen. So check if the only non-free devices are in + # state UNCLAIMABLE or UNAVAILABLE then we log a warning but + # allow to claim the parent. + actual_statuses = { + child.status for child in non_free_dependants} + allowed_non_free_statues = { + fields.PciDeviceStatus.UNCLAIMABLE, + fields.PciDeviceStatus.UNAVAILABLE, + } + if actual_statuses - allowed_non_free_statues == set(): + LOG.warning( + "Some child device of parent %s is in an inconsistent " + "state. If you can reproduce this warning then please " + "report a bug at " + "https://bugs.launchpad.net/nova/+filebug with " + "reproduction steps. Inconsistent children with " + "state: %s", + self.address, + ",".join( + "%s - %s" % (child.address, child.status) + for child in non_free_dependants + ), + ) + + else: + raise exception.PciDeviceVFInvalidStatus( + compute_node_id=self.compute_node_id, + address=self.address) self._bulk_update_status(vfs_list, fields.PciDeviceStatus.UNCLAIMABLE) @@ -447,11 +486,30 @@ def allocate(self, instance): instance.pci_devices.objects.append(copy.copy(self)) def remove(self): - if self.status != fields.PciDeviceStatus.AVAILABLE: + # We allow removal of a device is if it is unused. It can be unused + # either by being in available state or being in a state that shows + # that the parent or child device blocks the consumption of this device + expected_states = [ + fields.PciDeviceStatus.AVAILABLE, + fields.PciDeviceStatus.UNAVAILABLE, + fields.PciDeviceStatus.UNCLAIMABLE, + ] + if self.status not in expected_states: raise exception.PciDeviceInvalidStatus( compute_node_id=self.compute_node_id, address=self.address, status=self.status, - hopestatus=[fields.PciDeviceStatus.AVAILABLE]) + hopestatus=expected_states) + # Just to be on the safe side, do not allow removal of device that has + # an owner even if the state of the device suggests that it is not + # owned. + if 'instance_uuid' in self and self.instance_uuid is not None: + raise exception.PciDeviceInvalidOwner( + compute_node_id=self.compute_node_id, + address=self.address, + owner=self.instance_uuid, + hopeowner=None, + ) + self.status = fields.PciDeviceStatus.REMOVED self.instance_uuid = None self.request_id = None @@ -517,6 +575,13 @@ def card_serial_number(self): caps = jsonutils.loads(caps_json) return caps.get('vpd', {}).get('card_serial_number') + @property + def mac_address(self): + """The MAC address of the PF physical device or None if the device is + not a PF or if the MAC is not available. + """ + return self.extra_info.get('mac_address') + @base.NovaObjectRegistry.register class PciDeviceList(base.ObjectListBase, base.NovaObject): @@ -556,3 +621,6 @@ def get_by_parent_address(cls, context, node_id, parent_addr): parent_addr) return base.obj_make_list(context, cls(context), objects.PciDevice, db_dev_list) + + def __repr__(self): + return f"PciDeviceList(objects={[repr(obj) for obj in self.objects]})" diff --git a/nova/objects/request_spec.py b/nova/objects/request_spec.py index 9ce77a40435..cc542932318 100644 --- a/nova/objects/request_spec.py +++ b/nova/objects/request_spec.py @@ -645,6 +645,7 @@ def _from_db_object(context, spec, db_spec): except exception.InstanceGroupNotFound: # NOTE(danms): Instance group may have been deleted spec.instance_group = None + spec.scheduler_hints.pop('group', None) if data_migrated: spec.save() diff --git a/nova/pci/manager.py b/nova/pci/manager.py index fc6a8417246..78cfb05dbbd 100644 --- a/nova/pci/manager.py +++ b/nova/pci/manager.py @@ -217,10 +217,13 @@ def _set_hvdevs(self, devices: ty.List[ty.Dict[str, ty.Any]]) -> None: # from the pci whitelist. try: existed.remove() - except exception.PciDeviceInvalidStatus as e: - LOG.warning("Unable to remove device with %(status)s " - "ownership %(instance_uuid)s because of " - "%(pci_exception)s. " + except ( + exception.PciDeviceInvalidStatus, + exception.PciDeviceInvalidOwner, + ) as e: + LOG.warning("Unable to remove device with status " + "'%(status)s' and ownership %(instance_uuid)s " + "because of %(pci_exception)s. " "Check your [pci]passthrough_whitelist " "configuration to make sure this allocated " "device is whitelisted. If you have removed " @@ -250,7 +253,10 @@ def _set_hvdevs(self, devices: ty.List[ty.Dict[str, ty.Any]]) -> None: else: # Note(yjiang5): no need to update stats if an assigned # device is hot removed. - self.stats.remove_device(existed) + # NOTE(gibi): only remove the device from the pools if it + # is not already removed + if existed in self.stats.get_free_devs(): + self.stats.remove_device(existed) else: # Update tracked devices. new_value: ty.Dict[str, ty.Any] diff --git a/nova/pci/stats.py b/nova/pci/stats.py index c8dda84d4bf..6a53c43c787 100644 --- a/nova/pci/stats.py +++ b/nova/pci/stats.py @@ -279,8 +279,12 @@ def _handle_device_dependents(self, pci_dev: 'objects.PciDevice') -> None: if pci_dev.dev_type == fields.PciDeviceType.SRIOV_PF: vfs_list = pci_dev.child_devices if vfs_list: + free_devs = self.get_free_devs() for vf in vfs_list: - self.remove_device(vf) + # NOTE(gibi): do not try to remove a device that are + # already removed + if vf in free_devs: + self.remove_device(vf) elif pci_dev.dev_type in ( fields.PciDeviceType.SRIOV_VF, fields.PciDeviceType.VDPA, diff --git a/nova/quota.py b/nova/quota.py index b9dd7630127..eafad4cd23d 100644 --- a/nova/quota.py +++ b/nova/quota.py @@ -1348,11 +1348,8 @@ def _instances_cores_ram_count_legacy(context, project_id, user_id=None): def _cores_ram_count_placement(context, project_id, user_id=None): - global PLACEMENT_CLIENT - if not PLACEMENT_CLIENT: - PLACEMENT_CLIENT = report.SchedulerReportClient() - return PLACEMENT_CLIENT.get_usages_counts_for_quota(context, project_id, - user_id=user_id) + return report.report_client_singleton().get_usages_counts_for_quota( + context, project_id, user_id=user_id) def _instances_cores_ram_count_api_db_placement(context, project_id, diff --git a/nova/scheduler/client/report.py b/nova/scheduler/client/report.py index e4d0c8e3db6..ff86527cf55 100644 --- a/nova/scheduler/client/report.py +++ b/nova/scheduler/client/report.py @@ -52,6 +52,7 @@ NESTED_PROVIDER_API_VERSION = '1.14' POST_ALLOCATIONS_API_VERSION = '1.13' GET_USAGES_VERSION = '1.9' +PLACEMENTCLIENT = None AggInfo = collections.namedtuple('AggInfo', ['aggregates', 'generation']) TraitInfo = collections.namedtuple('TraitInfo', ['traits', 'generation']) @@ -67,6 +68,51 @@ def warn_limit(self, msg): LOG.warning(msg) +def report_client_singleton(): + """Return a reference to the global placement client singleton. + + This initializes the placement client once and returns a reference + to that singleton on subsequent calls. Errors are raised + (particularly ks_exc.*) but context-specific error messages are + logged for consistency. + """ + # NOTE(danms): The report client maintains internal state in the + # form of the provider tree, which will be shared across all users + # of this global client. That is not a problem now, but in the + # future it may be beneficial to fix that. One idea would be to + # change the behavior of the client such that the static-config + # pieces of the actual keystone client are separate from the + # internal state, so that we can return a new object here with a + # context-specific local state object, but with the client bits + # shared. + global PLACEMENTCLIENT + if PLACEMENTCLIENT is None: + try: + PLACEMENTCLIENT = SchedulerReportClient() + except ks_exc.EndpointNotFound: + LOG.error('The placement API endpoint was not found.') + raise + except ks_exc.MissingAuthPlugin: + LOG.error('No authentication information found for placement API.') + raise + except ks_exc.Unauthorized: + LOG.error('Placement service credentials do not work.') + raise + except ks_exc.DiscoveryFailure: + LOG.error('Discovering suitable URL for placement API failed.') + raise + except (ks_exc.ConnectFailure, + ks_exc.RequestTimeout, + ks_exc.GatewayTimeout): + LOG.error('Placement API service is not responding.') + raise + except Exception: + LOG.error('Failed to initialize placement client ' + '(is keystone available?)') + raise + return PLACEMENTCLIENT + + def safe_connect(f): @functools.wraps(f) def wrapper(self, *a, **k): diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index 03df615f6a6..10b330653de 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -66,7 +66,7 @@ def __init__(self, *args, **kwargs): self.host_manager = host_manager.HostManager() self.servicegroup_api = servicegroup.API() self.notifier = rpc.get_notifier('scheduler') - self.placement_client = report.SchedulerReportClient() + self.placement_client = report.report_client_singleton() super().__init__(service_name='scheduler', *args, **kwargs) diff --git a/nova/scheduler/request_filter.py b/nova/scheduler/request_filter.py index bd237b06cac..3f96b7a8806 100644 --- a/nova/scheduler/request_filter.py +++ b/nova/scheduler/request_filter.py @@ -311,7 +311,7 @@ def routed_networks_filter( # Get the clients we need network_api = neutron.API() - report_api = report.SchedulerReportClient() + report_api = report.report_client_singleton() for requested_network in requested_networks: network_id = None diff --git a/nova/service_auth.py b/nova/service_auth.py index f5ae0646d8a..aa8fd8fa123 100644 --- a/nova/service_auth.py +++ b/nova/service_auth.py @@ -30,8 +30,10 @@ def reset_globals(): _SERVICE_AUTH = None -def get_auth_plugin(context): - user_auth = context.get_auth_plugin() +def get_auth_plugin(context, user_auth=None): + # user_auth may be passed in when the RequestContext is anonymous, such as + # when get_admin_context() is used for API calls by nova-manage. + user_auth = user_auth or context.get_auth_plugin() if CONF.service_user.send_service_user_token: global _SERVICE_AUTH diff --git a/nova/test.py b/nova/test.py index a6449c01f03..4602f7d0134 100644 --- a/nova/test.py +++ b/nova/test.py @@ -61,6 +61,7 @@ from nova import objects from nova.objects import base as objects_base from nova import quota +from nova.scheduler.client import report from nova.tests import fixtures as nova_fixtures from nova.tests.unit import matchers from nova import utils @@ -290,6 +291,9 @@ def setUp(self): # instead of only once initialized for test worker wsgi_app.init_global_data.reset() + # Reset the placement client singleton + report.PLACEMENTCLIENT = None + def _setup_cells(self): """Setup a normal cellsv2 environment. @@ -355,7 +359,7 @@ def stub_out(self, old, new): self.useFixture(fixtures.MonkeyPatch(old, new)) @staticmethod - def patch_exists(patched_path, result): + def patch_exists(patched_path, result, other=None): """Provide a static method version of patch_exists(), which if you haven't already imported nova.test can be slightly easier to use as a context manager within a test method via: @@ -364,7 +368,7 @@ def test_something(self): with self.patch_exists(path, True): ... """ - return patch_exists(patched_path, result) + return patch_exists(patched_path, result, other) @staticmethod def patch_open(patched_path, read_data): @@ -848,10 +852,12 @@ def __repr__(self): @contextlib.contextmanager -def patch_exists(patched_path, result): +def patch_exists(patched_path, result, other=None): """Selectively patch os.path.exists() so that if it's called with patched_path, return result. Calls with any other path are passed - through to the real os.path.exists() function. + through to the real os.path.exists() function if other is not provided. + If other is provided then that will be the result of the call on paths + other than patched_path. Either import and use as a decorator / context manager, or use the nova.TestCase.patch_exists() static method as a context manager. @@ -885,7 +891,10 @@ def test_my_code(self, mock_exists): def fake_exists(path): if path == patched_path: return result - return real_exists(path) + elif other is not None: + return other + else: + return real_exists(path) with mock.patch.object(os.path, "exists") as mock_exists: mock_exists.side_effect = fake_exists diff --git a/nova/tests/fixtures/libvirt.py b/nova/tests/fixtures/libvirt.py index 891e9572005..5ccf01e40f9 100644 --- a/nova/tests/fixtures/libvirt.py +++ b/nova/tests/fixtures/libvirt.py @@ -309,7 +309,7 @@ def __init__( self, dev_type, bus, slot, function, iommu_group, numa_node, *, vf_ratio=None, multiple_gpu_types=False, generic_types=False, parent=None, vend_id=None, vend_name=None, prod_id=None, - prod_name=None, driver_name=None, vpd_fields=None + prod_name=None, driver_name=None, vpd_fields=None, mac_address=None, ): """Populate pci devices @@ -331,6 +331,8 @@ def __init__( :param prod_id: (str) The product ID. :param prod_name: (str) The product name. :param driver_name: (str) The driver name. + :param mac_address: (str) The MAC of the device. + Used in case of SRIOV PFs """ self.dev_type = dev_type @@ -349,6 +351,7 @@ def __init__( self.prod_id = prod_id self.prod_name = prod_name self.driver_name = driver_name + self.mac_address = mac_address self.vpd_fields = vpd_fields @@ -364,7 +367,9 @@ def generate_xml(self, skip_capability=False): assert not self.vf_ratio, 'vf_ratio does not apply for PCI devices' if self.dev_type in ('PF', 'VF'): - assert self.vf_ratio, 'require vf_ratio for PFs and VFs' + assert ( + self.vf_ratio is not None + ), 'require vf_ratio for PFs and VFs' if self.dev_type == 'VF': assert self.parent, 'require parent for VFs' @@ -497,6 +502,10 @@ def format_vpd_cap(self): def XMLDesc(self, flags): return self.pci_device + @property + def address(self): + return "0000:%02x:%02x.%1x" % (self.bus, self.slot, self.function) + # TODO(stephenfin): Remove all of these HostFooDevicesInfo objects in favour of # a unified devices object @@ -609,7 +618,7 @@ def add_device( self, dev_type, bus, slot, function, iommu_group, numa_node, vf_ratio=None, multiple_gpu_types=False, generic_types=False, parent=None, vend_id=None, vend_name=None, prod_id=None, - prod_name=None, driver_name=None, vpd_fields=None, + prod_name=None, driver_name=None, vpd_fields=None, mac_address=None, ): pci_dev_name = _get_libvirt_nodedev_name(bus, slot, function) @@ -632,6 +641,7 @@ def add_device( prod_name=prod_name, driver_name=driver_name, vpd_fields=vpd_fields, + mac_address=mac_address, ) self.devices[pci_dev_name] = dev return dev @@ -651,6 +661,13 @@ def get_all_mdev_capable_devices(self): return [dev for dev in self.devices if self.devices[dev].is_capable_of_mdevs] + def get_pci_address_mac_mapping(self): + return { + device.address: device.mac_address + for dev_addr, device in self.devices.items() + if device.mac_address + } + class FakeMdevDevice(object): template = """ @@ -2182,6 +2199,15 @@ class LibvirtFixture(fixtures.Fixture): def __init__(self, stub_os_vif=True): self.stub_os_vif = stub_os_vif + self.pci_address_to_mac_map = collections.defaultdict( + lambda: '52:54:00:1e:59:c6') + + def update_sriov_mac_address_mapping(self, pci_address_to_mac_map): + self.pci_address_to_mac_map.update(pci_address_to_mac_map) + + def fake_get_mac_by_pci_address(self, pci_addr, pf_interface=False): + res = self.pci_address_to_mac_map[pci_addr] + return res def setUp(self): super().setUp() @@ -2194,31 +2220,39 @@ def setUp(self): self.useFixture( fixtures.MockPatch('nova.virt.libvirt.utils.get_fs_info')) - self.useFixture( - fixtures.MockPatch('nova.compute.utils.get_machine_ips')) + self.mock_get_machine_ips = self.useFixture( + fixtures.MockPatch('nova.compute.utils.get_machine_ips')).mock # libvirt driver needs to call out to the filesystem to get the # parent_ifname for the SRIOV VFs. - self.useFixture(fixtures.MockPatch( - 'nova.pci.utils.get_ifname_by_pci_address', - return_value='fake_pf_interface_name')) + self.mock_get_ifname_by_pci_address = self.useFixture( + fixtures.MockPatch( + "nova.pci.utils.get_ifname_by_pci_address", + return_value="fake_pf_interface_name", + ) + ).mock self.useFixture(fixtures.MockPatch( 'nova.pci.utils.get_mac_by_pci_address', - return_value='52:54:00:1e:59:c6')) + side_effect=self.fake_get_mac_by_pci_address)) # libvirt calls out to sysfs to get the vfs ID during macvtap plug - self.useFixture(fixtures.MockPatch( - 'nova.pci.utils.get_vf_num_by_pci_address', return_value=1)) + self.mock_get_vf_num_by_pci_address = self.useFixture( + fixtures.MockPatch( + 'nova.pci.utils.get_vf_num_by_pci_address', return_value=1 + ) + ).mock # libvirt calls out to privsep to set the mac and vlan of a macvtap - self.useFixture(fixtures.MockPatch( - 'nova.privsep.linux_net.set_device_macaddr_and_vlan')) + self.mock_set_device_macaddr_and_vlan = self.useFixture( + fixtures.MockPatch( + 'nova.privsep.linux_net.set_device_macaddr_and_vlan')).mock # libvirt calls out to privsep to set the port state during macvtap # plug - self.useFixture(fixtures.MockPatch( - 'nova.privsep.linux_net.set_device_macaddr')) + self.mock_set_device_macaddr = self.useFixture( + fixtures.MockPatch( + 'nova.privsep.linux_net.set_device_macaddr')).mock # Don't assume that the system running tests has a valid machine-id self.useFixture(fixtures.MockPatch( @@ -2233,8 +2267,8 @@ def setUp(self): # Ensure tests perform the same on all host architectures fake_uname = os_uname( 'Linux', '', '5.4.0-0-generic', '', obj_fields.Architecture.X86_64) - self.useFixture( - fixtures.MockPatch('os.uname', return_value=fake_uname)) + self.mock_uname = self.useFixture( + fixtures.MockPatch('os.uname', return_value=fake_uname)).mock # ...and on all machine types fake_loaders = [ diff --git a/nova/tests/fixtures/nova.py b/nova/tests/fixtures/nova.py index ef873f6654a..458c15be116 100644 --- a/nova/tests/fixtures/nova.py +++ b/nova/tests/fixtures/nova.py @@ -22,6 +22,7 @@ import functools import logging as std_logging import os +import time import warnings import eventlet @@ -451,6 +452,13 @@ def _wrap_target_cell(self, context, cell_mapping): # yield to do the actual work. We can do schedulable things # here and not exclude other threads from making progress. # If an exception is raised, we capture that and save it. + # Note that it is possible that another thread has changed the + # global state (step #2) after we released the writer lock but + # before we acquired the reader lock. If this happens, we will + # detect the global state change and retry step #2 a limited number + # of times. If we happen to race repeatedly with another thread and + # exceed our retry limit, we will give up and raise a RuntimeError, + # which will fail the test. # 4. If we changed state in #2, we need to change it back. So we grab # a writer lock again and do that. # 5. Finally, if an exception was raised in #3 while state was @@ -469,29 +477,47 @@ def _wrap_target_cell(self, context, cell_mapping): raised_exc = None - with self._cell_lock.write_lock(): - if cell_mapping is not None: - # This assumes the next local DB access is the same cell that - # was targeted last time. - self._last_ctxt_mgr = desired + def set_last_ctxt_mgr(): + with self._cell_lock.write_lock(): + if cell_mapping is not None: + # This assumes the next local DB access is the same cell + # that was targeted last time. + self._last_ctxt_mgr = desired - with self._cell_lock.read_lock(): - if self._last_ctxt_mgr != desired: - # NOTE(danms): This is unlikely to happen, but it's possible - # another waiting writer changed the state between us letting - # it go and re-acquiring as a reader. If lockutils supported - # upgrading and downgrading locks, this wouldn't be a problem. - # Regardless, assert that it is still as we left it here - # so we don't hit the wrong cell. If this becomes a problem, - # we just need to retry the write section above until we land - # here with the cell we want. - raise RuntimeError('Global DB state changed underneath us') + # Set last context manager to the desired cell's context manager. + set_last_ctxt_mgr() + # Retry setting the last context manager if we detect that a writer + # changed global DB state before we take the read lock. + for retry_time in range(0, 3): try: - with self._real_target_cell(context, cell_mapping) as ccontext: - yield ccontext - except Exception as exc: - raised_exc = exc + with self._cell_lock.read_lock(): + if self._last_ctxt_mgr != desired: + # NOTE(danms): This is unlikely to happen, but it's + # possible another waiting writer changed the state + # between us letting it go and re-acquiring as a + # reader. If lockutils supported upgrading and + # downgrading locks, this wouldn't be a problem. + # Regardless, assert that it is still as we left it + # here so we don't hit the wrong cell. If this becomes + # a problem, we just need to retry the write section + # above until we land here with the cell we want. + raise RuntimeError( + 'Global DB state changed underneath us') + try: + with self._real_target_cell( + context, cell_mapping + ) as ccontext: + yield ccontext + except Exception as exc: + raised_exc = exc + # Leave the retry loop after calling target_cell + break + except RuntimeError: + # Give other threads a chance to make progress, increasing the + # wait time between attempts. + time.sleep(retry_time) + set_last_ctxt_mgr() with self._cell_lock.write_lock(): # Once we have returned from the context, we need @@ -878,6 +904,16 @@ def setUp(self): message='Implicit coercion of SELECT and textual SELECT .*', category=sqla_exc.SADeprecationWarning) + # Enable general SQLAlchemy warnings also to ensure we're not doing + # silly stuff. It's possible that we'll need to filter things out here + # with future SQLAlchemy versions, but that's a good thing + + warnings.filterwarnings( + 'error', + module='nova', + category=sqla_exc.SAWarning, + ) + self.addCleanup(self._reset_warning_filters) def _reset_warning_filters(self): @@ -1006,9 +1042,15 @@ def setUp(self): self.api = client.TestOpenStackClient( 'fake', base_url, project_id=self.project_id, roles=['reader', 'member']) + self.alternative_api = client.TestOpenStackClient( + 'fake', base_url, project_id=self.project_id, + roles=['reader', 'member']) self.admin_api = client.TestOpenStackClient( 'admin', base_url, project_id=self.project_id, roles=['reader', 'member', 'admin']) + self.alternative_admin_api = client.TestOpenStackClient( + 'admin', base_url, project_id=self.project_id, + roles=['reader', 'member', 'admin']) self.reader_api = client.TestOpenStackClient( 'reader', base_url, project_id=self.project_id, roles=['reader']) @@ -1104,9 +1146,9 @@ def evloop(*args, **kwargs): # Don't poison the function if it's already mocked import nova.virt.libvirt.host if not isinstance(nova.virt.libvirt.host.Host._init_events, mock.Mock): - self.useFixture(fixtures.MockPatch( + self.useFixture(fixtures.MonkeyPatch( 'nova.virt.libvirt.host.Host._init_events', - side_effect=evloop)) + evloop)) class IndirectionAPIFixture(fixtures.Fixture): @@ -1314,6 +1356,77 @@ def setUp(self): nova.privsep.sys_admin_pctxt, 'client_mode', False)) +class CGroupsFixture(fixtures.Fixture): + """Mocks checks made for available subsystems on the host's control group. + + The fixture mocks all calls made on the host to verify the capabilities + provided by its kernel. Through this, one can simulate the underlying + system hosts work on top of and have tests react to expected outcomes from + such. + + Use sample: + >>> cgroups = self.useFixture(CGroupsFixture()) + >>> cgroups = self.useFixture(CGroupsFixture(version=2)) + >>> cgroups = self.useFixture(CGroupsFixture()) + ... cgroups.version = 2 + + :attr version: Arranges mocks to simulate the host interact with nova + following the given version of cgroups. + Available values are: + - 0: All checks related to cgroups will return False. + - 1: Checks related to cgroups v1 will return True. + - 2: Checks related to cgroups v2 will return True. + Defaults to 1. + """ + + def __init__(self, version=1): + self._cpuv1 = None + self._cpuv2 = None + + self._version = version + + @property + def version(self): + return self._version + + @version.setter + def version(self, value): + self._version = value + self._update_mocks() + + def setUp(self): + super().setUp() + self._cpuv1 = self.useFixture(fixtures.MockPatch( + 'nova.virt.libvirt.host.Host._has_cgroupsv1_cpu_controller')).mock + self._cpuv2 = self.useFixture(fixtures.MockPatch( + 'nova.virt.libvirt.host.Host._has_cgroupsv2_cpu_controller')).mock + self._update_mocks() + + def _update_mocks(self): + if not self._cpuv1: + return + + if not self._cpuv2: + return + + if self.version == 0: + self._cpuv1.return_value = False + self._cpuv2.return_value = False + return + + if self.version == 1: + self._cpuv1.return_value = True + self._cpuv2.return_value = False + return + + if self.version == 2: + self._cpuv1.return_value = False + self._cpuv2.return_value = True + return + + raise ValueError(f"Unknown cgroups version: '{self.version}'.") + + class NoopQuotaDriverFixture(fixtures.Fixture): """A fixture to run tests using the NoopQuotaDriver. diff --git a/nova/tests/functional/api_sample_tests/test_remote_consoles.py b/nova/tests/functional/api_sample_tests/test_remote_consoles.py index 986826bfee0..e304402ee94 100644 --- a/nova/tests/functional/api_sample_tests/test_remote_consoles.py +++ b/nova/tests/functional/api_sample_tests/test_remote_consoles.py @@ -13,6 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock + +from nova.compute import api as compute +from nova import exception from nova.tests.functional.api_sample_tests import test_servers HTTP_RE = r'(https?://)([\w\d:#@%/;$()~_?\+-=\\.&](#!)?)*' @@ -38,6 +42,22 @@ def test_get_vnc_console(self): self._verify_response('get-vnc-console-post-resp', {'url': HTTP_RE}, response, 200) + @mock.patch.object(compute.API, 'get_vnc_console') + def test_get_vnc_console_instance_invalid_state(self, + mock_get_vnc_console): + uuid = self._post_server() + + def fake_get_vnc_console(*args, **kwargs): + raise exception.InstanceInvalidState( + attr='fake_attr', state='fake_state', method='fake_method', + instance_uuid=uuid) + + mock_get_vnc_console.side_effect = fake_get_vnc_console + response = self._do_post('servers/%s/action' % uuid, + 'get-vnc-console-post-req', + {'action': 'os-getVNCConsole'}) + self.assertEqual(409, response.status_code) + def test_get_spice_console(self): uuid = self._post_server() response = self._do_post('servers/%s/action' % uuid, diff --git a/nova/tests/functional/compute/test_resource_tracker.py b/nova/tests/functional/compute/test_resource_tracker.py index 81b7dfb68cf..758c15f371a 100644 --- a/nova/tests/functional/compute/test_resource_tracker.py +++ b/nova/tests/functional/compute/test_resource_tracker.py @@ -29,7 +29,6 @@ from nova import context from nova import objects from nova import test -from nova.tests import fixtures as nova_fixtures from nova.tests.functional import fixtures as func_fixtures from nova.tests.functional import integrated_helpers from nova.virt import driver as virt_driver @@ -694,15 +693,6 @@ def test_end_to_end(self): feature a vm cannot be spawning using a custom trait and then start a compute service that provides that trait. """ - - self.useFixture(nova_fixtures.NeutronFixture(self)) - self.useFixture(nova_fixtures.GlanceFixture(self)) - - # Start nova services. - self.api = self.useFixture(nova_fixtures.OSAPIFixture( - api_version='v2.1')).admin_api - self.api.microversion = 'latest' - self.start_service('conductor') # start nova-compute that will not have the additional trait. self._start_compute("fake-host-1") diff --git a/nova/tests/functional/integrated_helpers.py b/nova/tests/functional/integrated_helpers.py index 70918bc5f59..bd6244546c8 100644 --- a/nova/tests/functional/integrated_helpers.py +++ b/nova/tests/functional/integrated_helpers.py @@ -540,8 +540,9 @@ def _live_migrate( self.api.post_server_action( server['id'], {'os-migrateLive': {'host': None, 'block_migration': 'auto'}}) - self._wait_for_state_change(server, server_expected_state) + server = self._wait_for_state_change(server, server_expected_state) self._wait_for_migration_status(server, [migration_expected_state]) + return server _live_migrate_server = _live_migrate @@ -606,9 +607,11 @@ def _start_server(self, server): self.api.post_server_action(server['id'], {'os-start': None}) return self._wait_for_state_change(server, 'ACTIVE') - def _stop_server(self, server): + def _stop_server(self, server, wait_for_stop=True): self.api.post_server_action(server['id'], {'os-stop': None}) - return self._wait_for_state_change(server, 'SHUTOFF') + if wait_for_stop: + return self._wait_for_state_change(server, 'SHUTOFF') + return server class PlacementHelperMixin: diff --git a/nova/tests/functional/libvirt/base.py b/nova/tests/functional/libvirt/base.py index 3d8aec8106b..85b884c3ba9 100644 --- a/nova/tests/functional/libvirt/base.py +++ b/nova/tests/functional/libvirt/base.py @@ -42,7 +42,8 @@ def setUp(self): super(ServersTestBase, self).setUp() self.useFixture(nova_fixtures.LibvirtImageBackendFixture()) - self.useFixture(nova_fixtures.LibvirtFixture()) + self.useFixture(nova_fixtures.CGroupsFixture()) + self.libvirt = self.useFixture(nova_fixtures.LibvirtFixture()) self.useFixture(nova_fixtures.OSBrickFixture()) self.useFixture(fixtures.MockPatch( @@ -51,12 +52,12 @@ def setUp(self): self.useFixture(fixtures.MockPatch( 'nova.virt.libvirt.LibvirtDriver._get_local_gb_info', return_value={'total': 128, 'used': 44, 'free': 84})) - self.useFixture(fixtures.MockPatch( + self.mock_is_valid_hostname = self.useFixture(fixtures.MockPatch( 'nova.virt.libvirt.driver.libvirt_utils.is_valid_hostname', - return_value=True)) - self.useFixture(fixtures.MockPatch( + return_value=True)).mock + self.mock_file_open = self.useFixture(fixtures.MockPatch( 'nova.virt.libvirt.driver.libvirt_utils.file_open', - side_effect=lambda *a, **k: io.BytesIO(b''))) + side_effect=lambda *a, **k: io.BytesIO(b''))).mock self.useFixture(fixtures.MockPatch( 'nova.privsep.utils.supports_direct_io', return_value=True)) @@ -114,7 +115,7 @@ def _get_connection( def start_compute( self, hostname='compute1', host_info=None, pci_info=None, mdev_info=None, vdpa_info=None, libvirt_version=None, - qemu_version=None, + qemu_version=None, cell_name=None, connection=None ): """Start a compute service. @@ -124,27 +125,53 @@ def start_compute( :param host_info: A fakelibvirt.HostInfo object for the host. Defaults to a HostInfo with 2 NUMA nodes, 2 cores per node, 2 threads per core, and 16GB of RAM. + :param connection: A fake libvirt connection. You should not provide it + directly. However it is used by restart_compute_service to + implement restart without loosing the hypervisor state. :returns: The hostname of the created service, which can be used to lookup the created service and UUID of the assocaited resource provider. """ + if connection and ( + host_info or + pci_info or + mdev_info or + vdpa_info or + libvirt_version or + qemu_version + ): + raise ValueError( + "Either an existing connection instance can be provided or a " + "list of parameters for a new connection" + ) def _start_compute(hostname, host_info): - fake_connection = self._get_connection( - host_info, pci_info, mdev_info, vdpa_info, libvirt_version, - qemu_version, hostname, - ) + if connection: + fake_connection = connection + else: + fake_connection = self._get_connection( + host_info, pci_info, mdev_info, vdpa_info, libvirt_version, + qemu_version, hostname, + ) + + # If the compute is configured with PCI devices then we need to + # make sure that the stubs around sysfs has the MAC address + # information for the PCI PF devices + if pci_info: + self.libvirt.update_sriov_mac_address_mapping( + pci_info.get_pci_address_mac_mapping()) # This is fun. Firstly we need to do a global'ish mock so we can # actually start the service. - with mock.patch('nova.virt.libvirt.host.Host.get_connection', - return_value=fake_connection): - compute = self.start_service('compute', host=hostname) - # Once that's done, we need to tweak the compute "service" to - # make sure it returns unique objects. We do this inside the - # mock context to avoid a small window between the end of the - # context and the tweaking where get_connection would revert to - # being an autospec mock. - compute.driver._host.get_connection = lambda: fake_connection + orig_con = self.mock_conn.return_value + self.mock_conn.return_value = fake_connection + compute = self.start_service( + 'compute', host=hostname, cell_name=cell_name) + # Once that's done, we need to tweak the compute "service" to + # make sure it returns unique objects. + compute.driver._host.get_connection = lambda: fake_connection + # Then we revert the local mock tweaking so the next compute can + # get its own + self.mock_conn.return_value = orig_con return compute # ensure we haven't already registered services with these hostnames @@ -159,6 +186,74 @@ def _start_compute(hostname, host_info): return hostname + def restart_compute_service( + self, + hostname, + host_info=None, + pci_info=None, + mdev_info=None, + vdpa_info=None, + libvirt_version=None, + qemu_version=None, + keep_hypervisor_state=True, + ): + """Stops the service and starts a new one to have realistic restart + + :param hostname: the hostname of the nova-compute service to be + restarted + :param keep_hypervisor_state: If True then we reuse the fake connection + from the existing driver. If False a new connection will be created + based on the other parameters provided + """ + # We are intentionally not calling super() here. Nova's base test class + # defines starting and restarting compute service with a very + # different signatures and also those calls are cannot be made aware of + # the intricacies of the libvirt fixture. So we simply hide that + # implementation. + + if keep_hypervisor_state and ( + host_info or + pci_info or + mdev_info or + vdpa_info or + libvirt_version or + qemu_version + ): + raise ValueError( + "Either keep_hypervisor_state=True or a list of libvirt " + "parameters can be provided but not both" + ) + + compute = self.computes.pop(hostname) + self.compute_rp_uuids.pop(hostname) + + # NOTE(gibi): The service interface cannot be used to simulate a real + # service restart as the manager object will not be recreated after a + # service.stop() and service.start() therefore the manager state will + # survive. For example the resource tracker will not be recreated after + # a stop start. The service.kill() call cannot help as it deletes + # the service from the DB which is unrealistic and causes that some + # operation that refers to the killed host (e.g. evacuate) fails. + # So this helper method will stop the original service and then starts + # a brand new compute service for the same host and node. This way + # a new ComputeManager instance will be created and initialized during + # the service startup. + compute.stop() + + # this service was running previously, so we have to make sure that + # we restart it in the same cell + cell_name = self.host_mappings[compute.host].cell_mapping.name + + old_connection = compute.manager.driver._get_connection() + + self.start_compute( + hostname, host_info, pci_info, mdev_info, vdpa_info, + libvirt_version, qemu_version, cell_name, + old_connection if keep_hypervisor_state else None + ) + + return self.computes[hostname] + class LibvirtMigrationMixin(object): """A simple mixin to facilliate successful libvirt live migrations @@ -392,6 +487,22 @@ class LibvirtNeutronFixture(nova_fixtures.NeutronFixture): 'binding:vnic_type': 'remote-managed', } + network_4_port_pf = { + 'id': 'c6f51315-9202-416f-9e2f-eb78b3ac36d9', + 'network_id': network_4['id'], + 'status': 'ACTIVE', + 'mac_address': 'b5:bc:2e:e7:51:01', + 'fixed_ips': [ + { + 'ip_address': '192.168.4.8', + 'subnet_id': subnet_4['id'] + } + ], + 'binding:vif_details': {'vlan': 42}, + 'binding:vif_type': 'hostdev_physical', + 'binding:vnic_type': 'direct-physical', + } + def __init__(self, test): super(LibvirtNeutronFixture, self).__init__(test) self._networks = { diff --git a/nova/tests/functional/libvirt/test_device_bus_migration.py b/nova/tests/functional/libvirt/test_device_bus_migration.py index 82a0d4556e2..3852e31c68b 100644 --- a/nova/tests/functional/libvirt/test_device_bus_migration.py +++ b/nova/tests/functional/libvirt/test_device_bus_migration.py @@ -51,7 +51,7 @@ def _assert_stashed_image_properties(self, server_id, properties): def _assert_stashed_image_properties_persist(self, server, properties): # Assert the stashed properties persist across a host reboot - self.restart_compute_service(self.compute) + self.restart_compute_service(self.compute_hostname) self._assert_stashed_image_properties(server['id'], properties) # Assert the stashed properties persist across a guest reboot @@ -173,7 +173,7 @@ def test_default_image_property_persists_across_host_flag_changes(self): self.flags(pointer_model='ps2mouse') # Restart compute to pick up ps2 setting, which means the guest will # not get a prescribed pointer device - self.restart_compute_service(self.compute) + self.restart_compute_service(self.compute_hostname) # Create a server with default image properties default_image_properties1 = { @@ -187,7 +187,7 @@ def test_default_image_property_persists_across_host_flag_changes(self): # Assert the defaults persist across a host flag change self.flags(pointer_model='usbtablet') # Restart compute to pick up usb setting - self.restart_compute_service(self.compute) + self.restart_compute_service(self.compute_hostname) self._assert_stashed_image_properties( server1['id'], default_image_properties1) @@ -216,7 +216,7 @@ def test_default_image_property_persists_across_host_flag_changes(self): # https://bugs.launchpad.net/nova/+bug/1866106 self.flags(pointer_model=None) # Restart compute to pick up None setting - self.restart_compute_service(self.compute) + self.restart_compute_service(self.compute_hostname) self._assert_stashed_image_properties( server1['id'], default_image_properties1) self._assert_stashed_image_properties( diff --git a/nova/tests/functional/libvirt/test_evacuate.py b/nova/tests/functional/libvirt/test_evacuate.py index 531cefc63ca..9da04661afe 100644 --- a/nova/tests/functional/libvirt/test_evacuate.py +++ b/nova/tests/functional/libvirt/test_evacuate.py @@ -427,6 +427,7 @@ def setUp(self): self.useFixture(nova_fixtures.NeutronFixture(self)) self.useFixture(nova_fixtures.GlanceFixture(self)) self.useFixture(func_fixtures.PlacementFixture()) + self.useFixture(nova_fixtures.CGroupsFixture()) fake_network.set_stub_network_methods(self) api_fixture = self.useFixture( diff --git a/nova/tests/functional/libvirt/test_numa_live_migration.py b/nova/tests/functional/libvirt/test_numa_live_migration.py index 2f3897d6b2b..0e504d2df25 100644 --- a/nova/tests/functional/libvirt/test_numa_live_migration.py +++ b/nova/tests/functional/libvirt/test_numa_live_migration.py @@ -206,10 +206,8 @@ def _test(self, pin_dest): # Increase cpu_dedicated_set to 0-3, expecting the live migrated server # to end up on 2,3. self.flags(cpu_dedicated_set='0-3', group='compute') - self.computes['host_a'] = self.restart_compute_service( - self.computes['host_a']) - self.computes['host_b'] = self.restart_compute_service( - self.computes['host_b']) + self.restart_compute_service('host_a') + self.restart_compute_service('host_b') # Live migrate, RPC-pinning the destination host if asked if pin_dest: @@ -333,10 +331,8 @@ def _test(self, pin_dest=False): # Increase cpu_dedicated_set to 0-3, expecting the live migrated server # to end up on 2,3. self.flags(cpu_dedicated_set='0-3', group='compute') - self.computes['host_a'] = self.restart_compute_service( - self.computes['host_a']) - self.computes['host_b'] = self.restart_compute_service( - self.computes['host_b']) + self.restart_compute_service('host_a') + self.restart_compute_service('host_b') # Live migrate, RPC-pinning the destination host if asked. This is a # rollback test, so server_a is expected to remain on host_a. diff --git a/nova/tests/functional/libvirt/test_numa_servers.py b/nova/tests/functional/libvirt/test_numa_servers.py index fd09a11e20a..8fd97294040 100644 --- a/nova/tests/functional/libvirt/test_numa_servers.py +++ b/nova/tests/functional/libvirt/test_numa_servers.py @@ -1187,10 +1187,8 @@ def test_vcpu_to_pcpu_reshape(self): self.flags(cpu_dedicated_set='0-7', group='compute') self.flags(vcpu_pin_set=None) - computes = {} - for host, compute in self.computes.items(): - computes[host] = self.restart_compute_service(compute) - self.computes = computes + for host in list(self.computes.keys()): + self.restart_compute_service(host) # verify that the inventory, usages and allocation are correct after # the reshape diff --git a/nova/tests/functional/libvirt/test_pci_sriov_servers.py b/nova/tests/functional/libvirt/test_pci_sriov_servers.py index a5e52555e05..6e5165134bc 100644 --- a/nova/tests/functional/libvirt/test_pci_sriov_servers.py +++ b/nova/tests/functional/libvirt/test_pci_sriov_servers.py @@ -28,6 +28,7 @@ import nova from nova import context +from nova import exception from nova.network import constants from nova import objects from nova.objects import fields @@ -366,31 +367,66 @@ def _test_move_operation_with_neutron(self, move_operation, expect_fail=False): # The purpose here is to force an observable PCI slot update when # moving from source to dest. This is accomplished by having a single - # PCI device on the source, 2 PCI devices on the test, and relying on - # the fact that our fake HostPCIDevicesInfo creates predictable PCI - # addresses. The PCI device on source and the first PCI device on dest - # will have identical PCI addresses. By sticking a "placeholder" - # instance on that first PCI device on the dest, the incoming instance - # from source will be forced to consume the second dest PCI device, - # with a different PCI address. + # PCI VF device on the source, 2 PCI VF devices on the dest, and + # relying on the fact that our fake HostPCIDevicesInfo creates + # predictable PCI addresses. The PCI VF device on source and the first + # PCI VF device on dest will have identical PCI addresses. By sticking + # a "placeholder" instance on that first PCI VF device on the dest, the + # incoming instance from source will be forced to consume the second + # dest PCI VF device, with a different PCI address. + # We want to test server operations with SRIOV VFs and SRIOV PFs so + # the config of the compute hosts also have one extra PCI PF devices + # without any VF children. But the two compute has different PCI PF + # addresses and MAC so that the test can observe the slot update as + # well as the MAC updated during migration and after revert. + source_pci_info = fakelibvirt.HostPCIDevicesInfo(num_pfs=1, num_vfs=1) + # add an extra PF without VF to be used by direct-physical ports + source_pci_info.add_device( + dev_type='PF', + bus=0x82, # the HostPCIDevicesInfo use the 0x81 by default + slot=0x0, + function=0, + iommu_group=42, + numa_node=0, + vf_ratio=0, + mac_address='b4:96:91:34:f4:aa', + ) self.start_compute( hostname='source', - pci_info=fakelibvirt.HostPCIDevicesInfo( - num_pfs=1, num_vfs=1)) + pci_info=source_pci_info) + + dest_pci_info = fakelibvirt.HostPCIDevicesInfo(num_pfs=1, num_vfs=2) + # add an extra PF without VF to be used by direct-physical ports + dest_pci_info.add_device( + dev_type='PF', + bus=0x82, # the HostPCIDevicesInfo use the 0x81 by default + slot=0x6, # make it different from the source host + function=0, + iommu_group=42, + numa_node=0, + vf_ratio=0, + mac_address='b4:96:91:34:f4:bb', + ) self.start_compute( hostname='dest', - pci_info=fakelibvirt.HostPCIDevicesInfo( - num_pfs=1, num_vfs=2)) + pci_info=dest_pci_info) source_port = self.neutron.create_port( {'port': self.neutron.network_4_port_1}) + source_pf_port = self.neutron.create_port( + {'port': self.neutron.network_4_port_pf}) dest_port1 = self.neutron.create_port( {'port': self.neutron.network_4_port_2}) dest_port2 = self.neutron.create_port( {'port': self.neutron.network_4_port_3}) source_server = self._create_server( - networks=[{'port': source_port['port']['id']}], host='source') + networks=[ + {'port': source_port['port']['id']}, + {'port': source_pf_port['port']['id']} + ], + host='source', + ) dest_server1 = self._create_server( networks=[{'port': dest_port1['port']['id']}], host='dest') dest_server2 = self._create_server( @@ -398,6 +434,7 @@ def _test_move_operation_with_neutron(self, move_operation, # Refresh the ports. source_port = self.neutron.show_port(source_port['port']['id']) + source_pf_port = self.neutron.show_port(source_pf_port['port']['id']) dest_port1 = self.neutron.show_port(dest_port1['port']['id']) dest_port2 = self.neutron.show_port(dest_port2['port']['id']) @@ -413,11 +450,24 @@ def _test_move_operation_with_neutron(self, move_operation, same_slot_port = dest_port2 self._delete_server(dest_server1) - # Before moving, explictly assert that the servers on source and dest + # Before moving, explicitly assert that the servers on source and dest # have the same pci_slot in their port's binding profile self.assertEqual(source_port['port']['binding:profile']['pci_slot'], same_slot_port['port']['binding:profile']['pci_slot']) + # Assert that the direct-physical port got the pci_slot information + # according to the source host PF PCI device. + self.assertEqual( + '0000:82:00.0', # which is in sync with the source host pci_info + source_pf_port['port']['binding:profile']['pci_slot'] + ) + # Assert that the direct-physical port is updated with the MAC address + # of the PF device from the source host + self.assertEqual( + 'b4:96:91:34:f4:aa', + source_pf_port['port']['binding:profile']['device_mac_address'] + ) + # Before moving, assert that the servers on source and dest have the # same PCI source address in their XML for their SRIOV nic. source_conn = self.computes['source'].driver._host.get_connection() @@ -434,14 +484,28 @@ def _test_move_operation_with_neutron(self, move_operation, move_operation(source_server) # Refresh the ports again, keeping in mind the source_port is now bound - # on the dest after unshelving. + # on the dest after the move. source_port = self.neutron.show_port(source_port['port']['id']) same_slot_port = self.neutron.show_port(same_slot_port['port']['id']) + source_pf_port = self.neutron.show_port(source_pf_port['port']['id']) self.assertNotEqual( source_port['port']['binding:profile']['pci_slot'], same_slot_port['port']['binding:profile']['pci_slot']) + # Assert that the direct-physical port got the pci_slot information + # according to the dest host PF PCI device. + self.assertEqual( + '0000:82:06.0', # which is in sync with the dest host pci_info + source_pf_port['port']['binding:profile']['pci_slot'] + ) + # Assert that the direct-physical port is updated with the MAC address + # of the PF device from the dest host + self.assertEqual( + 'b4:96:91:34:f4:bb', + source_pf_port['port']['binding:profile']['device_mac_address'] + ) + conn = self.computes['dest'].driver._host.get_connection() vms = [vm._def for vm in conn._vms.values()] self.assertEqual(2, len(vms)) @@ -469,6 +533,169 @@ def move_operation(source_server): self._confirm_resize(source_server) self._test_move_operation_with_neutron(move_operation) + def test_cold_migrate_and_rever_server_with_neutron(self): + # The purpose here is to force an observable PCI slot update when + # moving from source to dest and the from dest to source after the + # revert. This is accomplished by having a single + # PCI VF device on the source, 2 PCI VF devices on the dest, and + # relying on the fact that our fake HostPCIDevicesInfo creates + # predictable PCI addresses. The PCI VF device on source and the first + # PCI VF device on dest will have identical PCI addresses. By sticking + # a "placeholder" instance on that first PCI VF device on the dest, the + # incoming instance from source will be forced to consume the second + # dest PCI VF device, with a different PCI address. + # We want to test server operations with SRIOV VFs and SRIOV PFs so + # the config of the compute hosts also have one extra PCI PF devices + # without any VF children. But the two compute has different PCI PF + # addresses and MAC so that the test can observe the slot update as + # well as the MAC updated during migration and after revert. + source_pci_info = fakelibvirt.HostPCIDevicesInfo(num_pfs=1, num_vfs=1) + # add an extra PF without VF to be used by direct-physical ports + source_pci_info.add_device( + dev_type='PF', + bus=0x82, # the HostPCIDevicesInfo use the 0x81 by default + slot=0x0, + function=0, + iommu_group=42, + numa_node=0, + vf_ratio=0, + mac_address='b4:96:91:34:f4:aa', + ) + self.start_compute( + hostname='source', + pci_info=source_pci_info) + dest_pci_info = fakelibvirt.HostPCIDevicesInfo(num_pfs=1, num_vfs=2) + # add an extra PF without VF to be used by direct-physical ports + dest_pci_info.add_device( + dev_type='PF', + bus=0x82, # the HostPCIDevicesInfo use the 0x81 by default + slot=0x6, # make it different from the source host + function=0, + iommu_group=42, + numa_node=0, + vf_ratio=0, + mac_address='b4:96:91:34:f4:bb', + ) + self.start_compute( + hostname='dest', + pci_info=dest_pci_info) + source_port = self.neutron.create_port( + {'port': self.neutron.network_4_port_1}) + source_pf_port = self.neutron.create_port( + {'port': self.neutron.network_4_port_pf}) + dest_port1 = self.neutron.create_port( + {'port': self.neutron.network_4_port_2}) + dest_port2 = self.neutron.create_port( + {'port': self.neutron.network_4_port_3}) + source_server = self._create_server( + networks=[ + {'port': source_port['port']['id']}, + {'port': source_pf_port['port']['id']} + ], + host='source', + ) + dest_server1 = self._create_server( + networks=[{'port': dest_port1['port']['id']}], host='dest') + dest_server2 = self._create_server( + networks=[{'port': dest_port2['port']['id']}], host='dest') + # Refresh the ports. + source_port = self.neutron.show_port(source_port['port']['id']) + source_pf_port = self.neutron.show_port(source_pf_port['port']['id']) + dest_port1 = self.neutron.show_port(dest_port1['port']['id']) + dest_port2 = self.neutron.show_port(dest_port2['port']['id']) + # Find the server on the dest compute that's using the same pci_slot as + # the server on the source compute, and delete the other one to make + # room for the incoming server from the source. + source_pci_slot = source_port['port']['binding:profile']['pci_slot'] + dest_pci_slot1 = dest_port1['port']['binding:profile']['pci_slot'] + if dest_pci_slot1 == source_pci_slot: + same_slot_port = dest_port1 + self._delete_server(dest_server2) + else: + same_slot_port = dest_port2 + self._delete_server(dest_server1) + # Before moving, explicitly assert that the servers on source and dest + # have the same pci_slot in their port's binding profile + self.assertEqual(source_port['port']['binding:profile']['pci_slot'], + same_slot_port['port']['binding:profile']['pci_slot']) + # Assert that the direct-physical port got the pci_slot information + # according to the source host PF PCI device. + self.assertEqual( + '0000:82:00.0', # which is in sync with the source host pci_info + source_pf_port['port']['binding:profile']['pci_slot'] + ) + # Assert that the direct-physical port is updated with the MAC address + # of the PF device from the source host + self.assertEqual( + 'b4:96:91:34:f4:aa', + source_pf_port['port']['binding:profile']['device_mac_address'] + ) + # Before moving, assert that the servers on source and dest have the + # same PCI source address in their XML for their SRIOV nic. + source_conn = self.computes['source'].driver._host.get_connection() + dest_conn = self.computes['source'].driver._host.get_connection() + source_vms = [vm._def for vm in source_conn._vms.values()] + dest_vms = [vm._def for vm in dest_conn._vms.values()] + self.assertEqual(1, len(source_vms)) + self.assertEqual(1, len(dest_vms)) + self.assertEqual(1, len(source_vms[0]['devices']['nics'])) + self.assertEqual(1, len(dest_vms[0]['devices']['nics'])) + self.assertEqual(source_vms[0]['devices']['nics'][0]['source'], + dest_vms[0]['devices']['nics'][0]['source']) + + # TODO(stephenfin): The mock of 'migrate_disk_and_power_off' should + # probably be less...dumb + with mock.patch('nova.virt.libvirt.driver.LibvirtDriver' + '.migrate_disk_and_power_off', return_value='{}'): + self._migrate_server(source_server) + + # Refresh the ports again, keeping in mind the ports are now bound + # on the dest after migrating. + source_port = self.neutron.show_port(source_port['port']['id']) + same_slot_port = self.neutron.show_port(same_slot_port['port']['id']) + source_pf_port = self.neutron.show_port(source_pf_port['port']['id']) + self.assertNotEqual( + source_port['port']['binding:profile']['pci_slot'], + same_slot_port['port']['binding:profile']['pci_slot']) + # Assert that the direct-physical port got the pci_slot information + # according to the dest host PF PCI device. + self.assertEqual( + '0000:82:06.0', # which is in sync with the dest host pci_info + source_pf_port['port']['binding:profile']['pci_slot'] + ) + # Assert that the direct-physical port is updated with the MAC address + # of the PF device from the dest host + self.assertEqual( + 'b4:96:91:34:f4:bb', + source_pf_port['port']['binding:profile']['device_mac_address'] + ) + conn = self.computes['dest'].driver._host.get_connection() + vms = [vm._def for vm in conn._vms.values()] + self.assertEqual(2, len(vms)) + for vm in vms: + self.assertEqual(1, len(vm['devices']['nics'])) + self.assertNotEqual(vms[0]['devices']['nics'][0]['source'], + vms[1]['devices']['nics'][0]['source']) + + self._revert_resize(source_server) + + # Refresh the ports again, keeping in mind the ports are now bound + # on the source as the migration is reverted + source_pf_port = self.neutron.show_port(source_pf_port['port']['id']) + + # Assert that the direct-physical port got the pci_slot information + # according to the source host PF PCI device. + self.assertEqual( + '0000:82:00.0', # which is in sync with the source host pci_info + source_pf_port['port']['binding:profile']['pci_slot'] + ) + # Assert that the direct-physical port is updated with the MAC address + # of the PF device from the source host + self.assertEqual( + 'b4:96:91:34:f4:aa', + source_pf_port['port']['binding:profile']['device_mac_address'] + ) + def test_evacuate_server_with_neutron(self): def move_operation(source_server): # Down the source compute to enable the evacuation @@ -486,17 +713,44 @@ def test_live_migrate_server_with_neutron(self): """ # start two compute services with differing PCI device inventory - self.start_compute( - hostname='test_compute0', - pci_info=fakelibvirt.HostPCIDevicesInfo( - num_pfs=2, num_vfs=8, numa_node=0)) - self.start_compute( - hostname='test_compute1', - pci_info=fakelibvirt.HostPCIDevicesInfo( - num_pfs=1, num_vfs=2, numa_node=1)) + source_pci_info = fakelibvirt.HostPCIDevicesInfo( + num_pfs=2, num_vfs=8, numa_node=0) + # add an extra PF without VF to be used by direct-physical ports + source_pci_info.add_device( + dev_type='PF', + bus=0x82, # the HostPCIDevicesInfo use the 0x81 by default + slot=0x0, + function=0, + iommu_group=42, + numa_node=0, + vf_ratio=0, + mac_address='b4:96:91:34:f4:aa', + ) + self.start_compute(hostname='test_compute0', pci_info=source_pci_info) - # create the port - self.neutron.create_port({'port': self.neutron.network_4_port_1}) + dest_pci_info = fakelibvirt.HostPCIDevicesInfo( + num_pfs=1, num_vfs=2, numa_node=1) + # add an extra PF without VF to be used by direct-physical ports + dest_pci_info.add_device( + dev_type='PF', + bus=0x82, # the HostPCIDevicesInfo use the 0x81 by default + slot=0x6, # make it different from the source host + function=0, + iommu_group=42, + # numa node needs to be aligned with the other pci devices in this + # host as the instance needs to fit into a single host numa node + numa_node=1, + vf_ratio=0, + mac_address='b4:96:91:34:f4:bb', + ) + + self.start_compute(hostname='test_compute1', pci_info=dest_pci_info) + + # create the ports + port = self.neutron.create_port( + {'port': self.neutron.network_4_port_1})['port'] + pf_port = self.neutron.create_port( + {'port': self.neutron.network_4_port_pf})['port'] # create a server using the VF via neutron extra_spec = {'hw:cpu_policy': 'dedicated'} @@ -504,7 +758,8 @@ def test_live_migrate_server_with_neutron(self): server = self._create_server( flavor_id=flavor_id, networks=[ - {'port': base.LibvirtNeutronFixture.network_4_port_1['id']}, + {'port': port['id']}, + {'port': pf_port['id']}, ], host='test_compute0', ) @@ -512,8 +767,8 @@ def test_live_migrate_server_with_neutron(self): # our source host should have marked two PCI devices as used, the VF # and the parent PF, while the future destination is currently unused self.assertEqual('test_compute0', server['OS-EXT-SRV-ATTR:host']) - self.assertPCIDeviceCounts('test_compute0', total=10, free=8) - self.assertPCIDeviceCounts('test_compute1', total=3, free=3) + self.assertPCIDeviceCounts('test_compute0', total=11, free=8) + self.assertPCIDeviceCounts('test_compute1', total=4, free=4) # the instance should be on host NUMA node 0, since that's where our # PCI devices are @@ -544,13 +799,26 @@ def test_live_migrate_server_with_neutron(self): port['binding:profile'], ) + # ensure the binding details sent to "neutron" are correct + pf_port = self.neutron.show_port(pf_port['id'],)['port'] + self.assertIn('binding:profile', pf_port) + self.assertEqual( + { + 'pci_vendor_info': '8086:1528', + 'pci_slot': '0000:82:00.0', + 'physical_network': 'physnet4', + 'device_mac_address': 'b4:96:91:34:f4:aa', + }, + pf_port['binding:profile'], + ) + # now live migrate that server self._live_migrate(server, 'completed') # we should now have transitioned our usage to the destination, freeing # up the source in the process - self.assertPCIDeviceCounts('test_compute0', total=10, free=10) - self.assertPCIDeviceCounts('test_compute1', total=3, free=1) + self.assertPCIDeviceCounts('test_compute0', total=11, free=11) + self.assertPCIDeviceCounts('test_compute1', total=4, free=1) # the instance should now be on host NUMA node 1, since that's where # our PCI devices are for this second host @@ -577,6 +845,18 @@ def test_live_migrate_server_with_neutron(self): }, port['binding:profile'], ) + # ensure the binding details sent to "neutron" are correct + pf_port = self.neutron.show_port(pf_port['id'],)['port'] + self.assertIn('binding:profile', pf_port) + self.assertEqual( + { + 'pci_vendor_info': '8086:1528', + 'pci_slot': '0000:82:06.0', + 'physical_network': 'physnet4', + 'device_mac_address': 'b4:96:91:34:f4:bb', + }, + pf_port['binding:profile'], + ) def test_get_server_diagnostics_server_with_VF(self): """Ensure server disagnostics include info on VF-type PCI devices.""" @@ -635,11 +915,8 @@ def test_create_server_after_change_in_nonsriov_pf_to_sriov_pf(self): # Disable SRIOV capabilties in PF and delete the VFs self._disable_sriov_in_pf(pci_info_no_sriov) - fake_connection = self._get_connection(pci_info=pci_info_no_sriov, - hostname='test_compute0') - self.mock_conn.return_value = fake_connection - - self.compute = self.start_service('compute', host='test_compute0') + self.start_compute('test_compute0', pci_info=pci_info_no_sriov) + self.compute = self.computes['test_compute0'] ctxt = context.get_admin_context() pci_devices = objects.PciDeviceList.get_by_compute_node( @@ -651,13 +928,9 @@ def test_create_server_after_change_in_nonsriov_pf_to_sriov_pf(self): self.assertEqual(1, len(pci_devices)) self.assertEqual('type-PCI', pci_devices[0].dev_type) - # Update connection with original pci info with sriov PFs - fake_connection = self._get_connection(pci_info=pci_info, - hostname='test_compute0') - self.mock_conn.return_value = fake_connection - - # Restart the compute service - self.restart_compute_service(self.compute) + # Restart the compute service with sriov PFs + self.restart_compute_service( + self.compute.host, pci_info=pci_info, keep_hypervisor_state=False) # Verify if PCI devices are of type type-PF or type-VF pci_devices = objects.PciDeviceList.get_by_compute_node( @@ -679,6 +952,88 @@ def test_create_server_after_change_in_nonsriov_pf_to_sriov_pf(self): ], ) + def test_change_bound_port_vnic_type_kills_compute_at_restart(self): + """Create a server with a direct port and change the vnic_type of the + bound port to macvtap. Then restart the compute service. + + As the vnic_type is changed on the port but the vif_type is hwveb + instead of macvtap the vif plug logic will try to look up the netdev + of the parent VF. Howvere that VF consumed by the instance so the + netdev does not exists. This causes that the compute service will fail + with an exception during startup + """ + pci_info = fakelibvirt.HostPCIDevicesInfo(num_pfs=1, num_vfs=2) + self.start_compute(pci_info=pci_info) + + # create a direct port + port = self.neutron.network_4_port_1 + self.neutron.create_port({'port': port}) + + # create a server using the VF via neutron + server = self._create_server(networks=[{'port': port['id']}]) + + # update the vnic_type of the port in neutron + port = copy.deepcopy(port) + port['binding:vnic_type'] = 'macvtap' + self.neutron.update_port(port['id'], {"port": port}) + + compute = self.computes['compute1'] + + # Force an update on the instance info cache to ensure nova gets the + # information about the updated port + with context.target_cell( + context.get_admin_context(), + self.host_mappings['compute1'].cell_mapping + ) as cctxt: + compute.manager._heal_instance_info_cache(cctxt) + self.assertIn( + 'The vnic_type of the bound port %s has been changed in ' + 'neutron from "direct" to "macvtap". Changing vnic_type of a ' + 'bound port is not supported by Nova. To avoid breaking the ' + 'connectivity of the instance please change the port ' + 'vnic_type back to "direct".' % port['id'], + self.stdlog.logger.output, + ) + + def fake_get_ifname_by_pci_address(pci_addr: str, pf_interface=False): + # we want to fail the netdev lookup only if the pci_address is + # already consumed by our instance. So we look into the instance + # definition to see if the device is attached to the instance as VF + conn = compute.manager.driver._host.get_connection() + dom = conn.lookupByUUIDString(server['id']) + dev = dom._def['devices']['nics'][0] + lookup_addr = pci_addr.replace(':', '_').replace('.', '_') + if ( + dev['type'] == 'hostdev' and + dev['source'] == 'pci_' + lookup_addr + ): + # nova tried to look up the netdev of an already consumed VF. + # So we have to fail + raise exception.PciDeviceNotFoundById(id=pci_addr) + + # We need to simulate the actual failure manually as in our functional + # environment all the PCI lookup is mocked. In reality nova tries to + # look up the netdev of the pci device on the host used by the port as + # the parent of the macvtap. However, as the originally direct port is + # bound to the instance, the VF pci device is already consumed by the + # instance and therefore there is no netdev for the VF. + with mock.patch( + 'nova.pci.utils.get_ifname_by_pci_address', + side_effect=fake_get_ifname_by_pci_address, + ): + # Nova cannot prevent the vnic_type change on a bound port. Neutron + # should prevent that instead. But the nova-compute should still + # be able to start up and only log an ERROR for this instance in + # inconsistent state. + self.restart_compute_service('compute1') + + self.assertIn( + 'Virtual interface plugging failed for instance. Probably the ' + 'vnic_type of the bound port has been changed. Nova does not ' + 'support such change.', + self.stdlog.logger.output, + ) + class SRIOVAttachDetachTest(_PCIServersTestBase): # no need for aliases as these test will request SRIOV via neutron @@ -742,10 +1097,9 @@ def _test_detach_attach(self, first_port_id, second_port_id): host_info = fakelibvirt.HostInfo(cpu_nodes=2, cpu_sockets=1, cpu_cores=2, cpu_threads=2) pci_info = fakelibvirt.HostPCIDevicesInfo(num_pfs=1, num_vfs=1) - fake_connection = self._get_connection(host_info, pci_info) - self.mock_conn.return_value = fake_connection - - self.compute = self.start_service('compute', host='test_compute0') + self.start_compute( + 'test_compute0', host_info=host_info, pci_info=pci_info) + self.compute = self.computes['test_compute0'] # Create server with a port server = self._create_server(networks=[{'port': first_port_id}]) @@ -834,7 +1188,7 @@ def setUp(self): # fixture already stubbed. self.neutron = self.useFixture(base.LibvirtNeutronFixture(self)) - def start_compute(self): + def start_vdpa_compute(self, hostname='compute-0'): vf_ratio = self.NUM_VFS // self.NUM_PFS pci_info = fakelibvirt.HostPCIDevicesInfo( @@ -872,7 +1226,7 @@ def start_compute(self): driver_name='mlx5_core') vdpa_info.add_device(f'vdpa_vdpa{idx}', idx, vf) - return super().start_compute( + return super().start_compute(hostname=hostname, pci_info=pci_info, vdpa_info=vdpa_info, libvirt_version=self.FAKE_LIBVIRT_VERSION, qemu_version=self.FAKE_QEMU_VERSION) @@ -927,7 +1281,7 @@ def fake_create(cls, xml, host): fake_create, ) - hostname = self.start_compute() + hostname = self.start_vdpa_compute() num_pci = self.NUM_PFS + self.NUM_VFS # both the PF and VF with vDPA capabilities (dev_type=vdpa) should have @@ -960,12 +1314,16 @@ def fake_create(cls, xml, host): port['binding:profile'], ) - def _test_common(self, op, *args, **kwargs): - self.start_compute() - + def _create_port_and_server(self): # create the port and a server, with the port attached to the server vdpa_port = self.create_vdpa_port() server = self._create_server(networks=[{'port': vdpa_port['id']}]) + return vdpa_port, server + + def _test_common(self, op, *args, **kwargs): + self.start_vdpa_compute() + + vdpa_port, server = self._create_port_and_server() # attempt the unsupported action and ensure it fails ex = self.assertRaises( @@ -976,13 +1334,11 @@ def _test_common(self, op, *args, **kwargs): ex.response.text) def test_attach_interface(self): - self.start_compute() - + self.start_vdpa_compute() # create the port and a server, but don't attach the port to the server # yet vdpa_port = self.create_vdpa_port() server = self._create_server(networks='none') - # attempt to attach the port to the server ex = self.assertRaises( client.OpenStackApiException, @@ -994,21 +1350,282 @@ def test_attach_interface(self): def test_detach_interface(self): self._test_common(self._detach_interface, uuids.vdpa_port) - def test_shelve(self): - self._test_common(self._shelve_server) + def test_shelve_offload(self): + hostname = self.start_vdpa_compute() + vdpa_port, server = self._create_port_and_server() + # assert the port is bound to the vm and the compute host + port = self.neutron.show_port(vdpa_port['id'])['port'] + self.assertEqual(server['id'], port['device_id']) + self.assertEqual(hostname, port['binding:host_id']) + num_pci = self.NUM_PFS + self.NUM_VFS + # -2 we claim the vdpa device which make the parent PF unavailable + self.assertPCIDeviceCounts(hostname, total=num_pci, free=num_pci - 2) + server = self._shelve_server(server) + # now that the vm is shelve offloaded it should not be bound + # to any host but should still be owned by the vm + port = self.neutron.show_port(vdpa_port['id'])['port'] + self.assertEqual(server['id'], port['device_id']) + # FIXME(sean-k-mooney): we should be unbinding the port from + # the host when we shelve offload but we don't today. + # This is unrelated to vdpa port and is a general issue. + self.assertEqual(hostname, port['binding:host_id']) + self.assertIn('binding:profile', port) + self.assertIsNone(server['OS-EXT-SRV-ATTR:hypervisor_hostname']) + self.assertIsNone(server['OS-EXT-SRV-ATTR:host']) + self.assertPCIDeviceCounts(hostname, total=num_pci, free=num_pci) - def test_suspend(self): - self._test_common(self._suspend_server) + def test_unshelve_to_same_host(self): + hostname = self.start_vdpa_compute() + num_pci = self.NUM_PFS + self.NUM_VFS + self.assertPCIDeviceCounts(hostname, total=num_pci, free=num_pci) + + vdpa_port, server = self._create_port_and_server() + self.assertPCIDeviceCounts(hostname, total=num_pci, free=num_pci - 2) + self.assertEqual( + hostname, server['OS-EXT-SRV-ATTR:hypervisor_hostname']) + port = self.neutron.show_port(vdpa_port['id'])['port'] + self.assertEqual(hostname, port['binding:host_id']) + + server = self._shelve_server(server) + self.assertPCIDeviceCounts(hostname, total=num_pci, free=num_pci) + self.assertIsNone(server['OS-EXT-SRV-ATTR:hypervisor_hostname']) + port = self.neutron.show_port(vdpa_port['id'])['port'] + # FIXME(sean-k-mooney): shelve offload should unbind the port + # self.assertEqual('', port['binding:host_id']) + self.assertEqual(hostname, port['binding:host_id']) + + server = self._unshelve_server(server) + self.assertPCIDeviceCounts(hostname, total=num_pci, free=num_pci - 2) + self.assertEqual( + hostname, server['OS-EXT-SRV-ATTR:hypervisor_hostname']) + port = self.neutron.show_port(vdpa_port['id'])['port'] + self.assertEqual(hostname, port['binding:host_id']) + + def test_unshelve_to_different_host(self): + source = self.start_vdpa_compute(hostname='source') + dest = self.start_vdpa_compute(hostname='dest') + + num_pci = self.NUM_PFS + self.NUM_VFS + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci) + self.assertPCIDeviceCounts(dest, total=num_pci, free=num_pci) + + # ensure we boot the vm on the "source" compute + self.api.put_service( + self.computes['dest'].service_ref.uuid, {'status': 'disabled'}) + vdpa_port, server = self._create_port_and_server() + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci - 2) + self.assertEqual( + source, server['OS-EXT-SRV-ATTR:hypervisor_hostname']) + port = self.neutron.show_port(vdpa_port['id'])['port'] + self.assertEqual(source, port['binding:host_id']) + + server = self._shelve_server(server) + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci) + self.assertIsNone(server['OS-EXT-SRV-ATTR:hypervisor_hostname']) + port = self.neutron.show_port(vdpa_port['id'])['port'] + # FIXME(sean-k-mooney): shelve should unbind the port + # self.assertEqual('', port['binding:host_id']) + self.assertEqual(source, port['binding:host_id']) + + # force the unshelve to the other host + self.api.put_service( + self.computes['source'].service_ref.uuid, {'status': 'disabled'}) + self.api.put_service( + self.computes['dest'].service_ref.uuid, {'status': 'enabled'}) + self.assertPCIDeviceCounts(dest, total=num_pci, free=num_pci) + server = self._unshelve_server(server) + # the dest devices should be claimed + self.assertPCIDeviceCounts(dest, total=num_pci, free=num_pci - 2) + # and the source host devices should still be free + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci) + self.assertEqual( + dest, server['OS-EXT-SRV-ATTR:hypervisor_hostname']) + port = self.neutron.show_port(vdpa_port['id'])['port'] + self.assertEqual(dest, port['binding:host_id']) def test_evacute(self): - self._test_common(self._evacuate_server) + source = self.start_vdpa_compute(hostname='source') + dest = self.start_vdpa_compute(hostname='dest') - def test_resize(self): - flavor_id = self._create_flavor() - self._test_common(self._resize_server, flavor_id) + num_pci = self.NUM_PFS + self.NUM_VFS + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci) + self.assertPCIDeviceCounts(dest, total=num_pci, free=num_pci) + + # ensure we boot the vm on the "source" compute + self.api.put_service( + self.computes['dest'].service_ref.uuid, {'status': 'disabled'}) + vdpa_port, server = self._create_port_and_server() + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci - 2) + self.assertEqual( + source, server['OS-EXT-SRV-ATTR:hypervisor_hostname']) + port = self.neutron.show_port(vdpa_port['id'])['port'] + self.assertEqual(source, port['binding:host_id']) + + # stop the source compute and enable the dest + self.api.put_service( + self.computes['dest'].service_ref.uuid, {'status': 'enabled'}) + self.computes['source'].stop() + # Down the source compute to enable the evacuation + self.api.put_service( + self.computes['source'].service_ref.uuid, {'forced_down': True}) + + self.assertPCIDeviceCounts(dest, total=num_pci, free=num_pci) + server = self._evacuate_server(server) + self.assertPCIDeviceCounts(dest, total=num_pci, free=num_pci - 2) + self.assertEqual( + dest, server['OS-EXT-SRV-ATTR:hypervisor_hostname']) + port = self.neutron.show_port(vdpa_port['id'])['port'] + self.assertEqual(dest, port['binding:host_id']) + + # as the source compute is offline the pci claims will not be cleaned + # up on the source compute. + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci - 2) + # but if you fix/restart the source node the allocations for evacuated + # instances should be released. + self.restart_compute_service(source) + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci) + + def test_resize_same_host(self): + self.flags(allow_resize_to_same_host=True) + num_pci = self.NUM_PFS + self.NUM_VFS + source = self.start_vdpa_compute() + vdpa_port, server = self._create_port_and_server() + # before we resize the vm should be using 1 VF but that will mark + # the PF as unavailable so we assert 2 devices are in use. + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci - 2) + flavor_id = self._create_flavor(name='new-flavor') + self.assertNotEqual(server['flavor']['original_name'], 'new-flavor') + with mock.patch( + 'nova.virt.libvirt.driver.LibvirtDriver' + '.migrate_disk_and_power_off', return_value='{}', + ): + server = self._resize_server(server, flavor_id) + self.assertEqual( + server['flavor']['original_name'], 'new-flavor') + # in resize verify the VF claims should be doubled even + # for same host resize so assert that 3 are in devices in use + # 1 PF and 2 VFs . + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci - 3) + server = self._confirm_resize(server) + # but once we confrim it should be reduced back to 1 PF and 1 VF + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci - 2) + # assert the hostname has not have changed as part + # of the resize. + self.assertEqual( + source, server['OS-EXT-SRV-ATTR:hypervisor_hostname']) + + def test_resize_different_host(self): + self.flags(allow_resize_to_same_host=False) + source = self.start_vdpa_compute(hostname='source') + dest = self.start_vdpa_compute(hostname='dest') + + num_pci = self.NUM_PFS + self.NUM_VFS + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci) + self.assertPCIDeviceCounts(dest, total=num_pci, free=num_pci) + + # ensure we boot the vm on the "source" compute + self.api.put_service( + self.computes['dest'].service_ref.uuid, {'status': 'disabled'}) + vdpa_port, server = self._create_port_and_server() + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci - 2) + flavor_id = self._create_flavor(name='new-flavor') + self.assertNotEqual(server['flavor']['original_name'], 'new-flavor') + # disable the source compute and enable the dest + self.api.put_service( + self.computes['source'].service_ref.uuid, {'status': 'disabled'}) + self.api.put_service( + self.computes['dest'].service_ref.uuid, {'status': 'enabled'}) + with mock.patch( + 'nova.virt.libvirt.driver.LibvirtDriver' + '.migrate_disk_and_power_off', return_value='{}', + ): + server = self._resize_server(server, flavor_id) + self.assertEqual( + server['flavor']['original_name'], 'new-flavor') + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci - 2) + self.assertPCIDeviceCounts(dest, total=num_pci, free=num_pci - 2) + server = self._confirm_resize(server) + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci) + self.assertPCIDeviceCounts(dest, total=num_pci, free=num_pci - 2) + self.assertEqual( + dest, server['OS-EXT-SRV-ATTR:hypervisor_hostname']) + + def test_resize_revert(self): + self.flags(allow_resize_to_same_host=False) + source = self.start_vdpa_compute(hostname='source') + dest = self.start_vdpa_compute(hostname='dest') + + num_pci = self.NUM_PFS + self.NUM_VFS + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci) + self.assertPCIDeviceCounts(dest, total=num_pci, free=num_pci) + + # ensure we boot the vm on the "source" compute + self.api.put_service( + self.computes['dest'].service_ref.uuid, {'status': 'disabled'}) + vdpa_port, server = self._create_port_and_server() + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci - 2) + flavor_id = self._create_flavor(name='new-flavor') + self.assertNotEqual(server['flavor']['original_name'], 'new-flavor') + # disable the source compute and enable the dest + self.api.put_service( + self.computes['source'].service_ref.uuid, {'status': 'disabled'}) + self.api.put_service( + self.computes['dest'].service_ref.uuid, {'status': 'enabled'}) + with mock.patch( + 'nova.virt.libvirt.driver.LibvirtDriver' + '.migrate_disk_and_power_off', return_value='{}', + ): + server = self._resize_server(server, flavor_id) + self.assertEqual( + server['flavor']['original_name'], 'new-flavor') + # in resize verify both the dest and source pci claims should be + # present. + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci - 2) + self.assertPCIDeviceCounts(dest, total=num_pci, free=num_pci - 2) + server = self._revert_resize(server) + # but once we revert the dest claims should be freed. + self.assertPCIDeviceCounts(dest, total=num_pci, free=num_pci) + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci - 2) + self.assertEqual( + source, server['OS-EXT-SRV-ATTR:hypervisor_hostname']) def test_cold_migrate(self): - self._test_common(self._migrate_server) + source = self.start_vdpa_compute(hostname='source') + dest = self.start_vdpa_compute(hostname='dest') + + num_pci = self.NUM_PFS + self.NUM_VFS + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci) + self.assertPCIDeviceCounts(dest, total=num_pci, free=num_pci) + + # ensure we boot the vm on the "source" compute + self.api.put_service( + self.computes['dest'].service_ref.uuid, {'status': 'disabled'}) + vdpa_port, server = self._create_port_and_server() + self.assertEqual( + source, server['OS-EXT-SRV-ATTR:hypervisor_hostname']) + + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci - 2) + # enable the dest we do not need to disable the source since cold + # migrate wont happen to the same host in the libvirt driver + self.api.put_service( + self.computes['dest'].service_ref.uuid, {'status': 'enabled'}) + with mock.patch( + 'nova.virt.libvirt.driver.LibvirtDriver' + '.migrate_disk_and_power_off', return_value='{}', + ): + server = self._migrate_server(server) + self.assertEqual( + dest, server['OS-EXT-SRV-ATTR:hypervisor_hostname']) + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci - 2) + self.assertPCIDeviceCounts(dest, total=num_pci, free=num_pci - 2) + server = self._confirm_resize(server) + self.assertPCIDeviceCounts(source, total=num_pci, free=num_pci) + self.assertPCIDeviceCounts(dest, total=num_pci, free=num_pci - 2) + self.assertEqual( + dest, server['OS-EXT-SRV-ATTR:hypervisor_hostname']) + + def test_suspend(self): + self._test_common(self._suspend_server) class PCIServersTest(_PCIServersTestBase): @@ -1116,13 +1733,16 @@ def test_resize_pci_to_vanilla(self): # Resize it to a flavor without PCI devices. We expect this to work, as # test_compute1 is available. - # FIXME(artom) This is bug 1941005. flavor_id = self._create_flavor() - ex = self.assertRaises(client.OpenStackApiException, - self._resize_server, server, flavor_id) - self.assertEqual(500, ex.response.status_code) - self.assertIn('NoValidHost', str(ex)) - # self._confirm_resize(server) + with mock.patch( + 'nova.virt.libvirt.driver.LibvirtDriver' + '.migrate_disk_and_power_off', + return_value='{}', + ): + self._resize_server(server, flavor_id) + self._confirm_resize(server) + self.assertPCIDeviceCounts('test_compute0', total=1, free=1) + self.assertPCIDeviceCounts('test_compute1', total=0, free=0) def _confirm_resize(self, server, host='host1'): # NOTE(sbauza): Unfortunately, _cleanup_resize() in libvirt checks the diff --git a/nova/tests/functional/libvirt/test_reshape.py b/nova/tests/functional/libvirt/test_reshape.py index 5c73ffbf5f7..d0102f12476 100644 --- a/nova/tests/functional/libvirt/test_reshape.py +++ b/nova/tests/functional/libvirt/test_reshape.py @@ -30,17 +30,7 @@ class VGPUReshapeTests(base.ServersTestBase): - @mock.patch('nova.virt.libvirt.LibvirtDriver._get_local_gb_info', - return_value={'total': 128, - 'used': 44, - 'free': 84}) - @mock.patch('nova.virt.libvirt.driver.libvirt_utils.is_valid_hostname', - return_value=True) - @mock.patch('nova.virt.libvirt.driver.libvirt_utils.file_open', - side_effect=[io.BytesIO(b''), io.BytesIO(b''), - io.BytesIO(b'')]) - def test_create_servers_with_vgpu( - self, mock_file_open, mock_valid_hostname, mock_get_fs_info): + def test_create_servers_with_vgpu(self): """Verify that vgpu reshape works with libvirt driver 1) create two servers with an old tree where the VGPU resource is on @@ -49,7 +39,8 @@ def test_create_servers_with_vgpu( 3) check that the allocations of the servers are still valid 4) create another server now against the new tree """ - + self.mock_file_open.side_effect = [ + io.BytesIO(b''), io.BytesIO(b''), io.BytesIO(b'')] # NOTE(gibi): We cannot simply ask the virt driver to create an old # RP tree with vgpu on the root RP as that code path does not exist # any more. So we have to hack a "bit". We will create a compute @@ -81,11 +72,11 @@ def test_create_servers_with_vgpu( # ignore the content of the above HostMdevDeviceInfo self.flags(enabled_mdev_types='', group='devices') - hostname = self.start_compute( + self.hostname = self.start_compute( hostname='compute1', mdev_info=fakelibvirt.HostMdevDevicesInfo(devices=mdevs), ) - self.compute = self.computes[hostname] + self.compute = self.computes[self.hostname] # create the VGPU resource in placement manually compute_rp_uuid = self.placement.get( @@ -167,7 +158,7 @@ def test_create_servers_with_vgpu( allocations[compute_rp_uuid]['resources']) # restart compute which will trigger a reshape - self.compute = self.restart_compute_service(self.compute) + self.compute = self.restart_compute_service(self.hostname) # verify that the inventory, usages and allocation are correct after # the reshape diff --git a/nova/tests/functional/libvirt/test_vgpu.py b/nova/tests/functional/libvirt/test_vgpu.py index f25ce442214..686582120ad 100644 --- a/nova/tests/functional/libvirt/test_vgpu.py +++ b/nova/tests/functional/libvirt/test_vgpu.py @@ -49,11 +49,11 @@ class VGPUTestBase(base.ServersTestBase): def setUp(self): super(VGPUTestBase, self).setUp() - self.useFixture(fixtures.MockPatch( - 'nova.virt.libvirt.LibvirtDriver._get_local_gb_info', - return_value={'total': 128, - 'used': 44, - 'free': 84})) + libvirt_driver.LibvirtDriver._get_local_gb_info.return_value = { + 'total': 128, + 'used': 44, + 'free': 84, + } self.useFixture(fixtures.MockPatch( 'nova.privsep.libvirt.create_mdev', side_effect=self._create_mdev)) @@ -113,8 +113,8 @@ def _create_mdev(self, physical_device, mdev_type, uuid=None): parent=libvirt_parent)}) return uuid - def start_compute(self, hostname): - hostname = super().start_compute( + def start_compute_with_vgpu(self, hostname): + hostname = self.start_compute( pci_info=fakelibvirt.HostPCIDevicesInfo( num_pci=0, num_pfs=0, num_vfs=0, num_mdevcap=2, ), @@ -197,7 +197,7 @@ def setUp(self): enabled_mdev_types=fakelibvirt.NVIDIA_11_VGPU_TYPE, group='devices') - self.compute1 = self.start_compute('host1') + self.compute1 = self.start_compute_with_vgpu('host1') def assert_vgpu_usage_for_compute(self, compute, expected): self.assert_mdev_usage(compute, expected_amount=expected) @@ -211,7 +211,7 @@ def test_create_servers_with_vgpu(self): def test_resize_servers_with_vgpu(self): # Add another compute for the sake of resizing - self.compute2 = self.start_compute('host2') + self.compute2 = self.start_compute_with_vgpu('host2') server = self._create_server( image_uuid='155d900f-4e14-4e4c-a73d-069cbf4541e6', flavor_id=self.flavor, host=self.compute1.host, @@ -337,7 +337,7 @@ def setUp(self): # Prepare traits for later on self._create_trait('CUSTOM_NVIDIA_11') self._create_trait('CUSTOM_NVIDIA_12') - self.compute1 = self.start_compute('host1') + self.compute1 = self.start_compute_with_vgpu('host1') def test_create_servers_with_vgpu(self): self._create_server( @@ -369,13 +369,12 @@ def test_create_servers_with_vgpu(self): def test_create_servers_with_specific_type(self): # Regenerate the PCI addresses so both pGPUs now support nvidia-12 - connection = self.computes[ - self.compute1.host].driver._host.get_connection() - connection.pci_info = fakelibvirt.HostPCIDevicesInfo( + pci_info = fakelibvirt.HostPCIDevicesInfo( num_pci=0, num_pfs=0, num_vfs=0, num_mdevcap=2, multiple_gpu_types=True) # Make a restart to update the Resource Providers - self.compute1 = self.restart_compute_service(self.compute1) + self.compute1 = self.restart_compute_service( + self.compute1.host, pci_info=pci_info, keep_hypervisor_state=False) pgpu1_rp_uuid = self._get_provider_uuid_by_name( self.compute1.host + '_' + fakelibvirt.MDEVCAP_DEV1_PCI_ADDR) pgpu2_rp_uuid = self._get_provider_uuid_by_name( @@ -451,7 +450,7 @@ def setUp(self): group='mdev_nvidia-12') self.flags(mdev_class='CUSTOM_NOTVGPU', group='mdev_mlx5_core') - self.compute1 = self.start_compute('host1') + self.compute1 = self.start_compute_with_vgpu('host1') # Regenerate the PCI addresses so they can support both mlx5 and # nvidia-12 types connection = self.computes[ @@ -460,7 +459,7 @@ def setUp(self): num_pci=0, num_pfs=0, num_vfs=0, num_mdevcap=2, generic_types=True) # Make a restart to update the Resource Providers - self.compute1 = self.restart_compute_service(self.compute1) + self.compute1 = self.restart_compute_service('host1') def test_create_servers_with_different_mdev_classes(self): physdev1_rp_uuid = self._get_provider_uuid_by_name( @@ -498,7 +497,7 @@ def test_create_servers_with_different_mdev_classes(self): def test_resize_servers_with_mlx5(self): # Add another compute for the sake of resizing - self.compute2 = self.start_compute('host2') + self.compute2 = self.start_compute_with_vgpu('host2') # Regenerate the PCI addresses so they can support both mlx5 and # nvidia-12 types connection = self.computes[ @@ -507,7 +506,7 @@ def test_resize_servers_with_mlx5(self): num_pci=0, num_pfs=0, num_vfs=0, num_mdevcap=2, generic_types=True) # Make a restart to update the Resource Providers - self.compute2 = self.restart_compute_service(self.compute2) + self.compute2 = self.restart_compute_service('host2') # Use the new flavor for booting server = self._create_server( diff --git a/nova/tests/functional/libvirt/test_vpmem.py b/nova/tests/functional/libvirt/test_vpmem.py index d1cad0e376c..b76e154997c 100644 --- a/nova/tests/functional/libvirt/test_vpmem.py +++ b/nova/tests/functional/libvirt/test_vpmem.py @@ -75,6 +75,7 @@ def setUp(self): 'nova.privsep.libvirt.get_pmem_namespaces', return_value=self.fake_pmem_namespaces)) self.useFixture(nova_fixtures.LibvirtImageBackendFixture()) + self.useFixture(nova_fixtures.CGroupsFixture()) self.useFixture(fixtures.MockPatch( 'nova.virt.libvirt.LibvirtDriver._get_local_gb_info', return_value={'total': 128, diff --git a/nova/tests/functional/libvirt/test_vtpm.py b/nova/tests/functional/libvirt/test_vtpm.py index c07c38f02d9..4e9c705052e 100644 --- a/nova/tests/functional/libvirt/test_vtpm.py +++ b/nova/tests/functional/libvirt/test_vtpm.py @@ -128,7 +128,7 @@ def setUp(self): # the presence of users on the host, none of which makes sense here _p = mock.patch( 'nova.virt.libvirt.driver.LibvirtDriver._check_vtpm_support') - self.mock_conn = _p.start() + _p.start() self.addCleanup(_p.stop) self.key_mgr = crypto._get_key_manager() diff --git a/nova/tests/functional/regressions/test_bug_1595962.py b/nova/tests/functional/regressions/test_bug_1595962.py index ebdf82f21a3..78916d09b71 100644 --- a/nova/tests/functional/regressions/test_bug_1595962.py +++ b/nova/tests/functional/regressions/test_bug_1595962.py @@ -47,6 +47,7 @@ def setUp(self): 'nova.virt.libvirt.guest.libvirt', fakelibvirt)) self.useFixture(nova_fixtures.LibvirtFixture()) + self.useFixture(nova_fixtures.CGroupsFixture()) self.admin_api = api_fixture.admin_api self.api = api_fixture.api diff --git a/nova/tests/functional/regressions/test_bug_1628606.py b/nova/tests/functional/regressions/test_bug_1628606.py new file mode 100644 index 00000000000..0fccd78ccec --- /dev/null +++ b/nova/tests/functional/regressions/test_bug_1628606.py @@ -0,0 +1,60 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova import test +from nova.tests import fixtures as nova_fixtures +from nova.tests.functional.api import client +from nova.tests.functional import fixtures as func_fixtures +from nova.tests.functional import integrated_helpers +from unittest import mock + + +class PostLiveMigrationFail( + test.TestCase, integrated_helpers.InstanceHelperMixin): + """Regression test for bug 1628606 + """ + + def setUp(self): + super().setUp() + self.useFixture(nova_fixtures.NeutronFixture(self)) + self.glance = self.useFixture(nova_fixtures.GlanceFixture(self)) + self.useFixture(func_fixtures.PlacementFixture()) + self.useFixture(nova_fixtures.HostNameWeigherFixture()) + + self.start_service('conductor') + self.start_service('scheduler') + + api_fixture = self.useFixture(nova_fixtures.OSAPIFixture( + api_version='v2.1')) + + self.api = api_fixture.admin_api + self.api.microversion = 'latest' + + self.src = self._start_compute(host='host1') + self.dest = self._start_compute(host='host2') + + @mock.patch( + 'nova.compute.manager.ComputeManager' + '._post_live_migration_remove_source_vol_connections') + def test_post_live_migration(self, mock_migration): + server = self._create_server(networks=[]) + self.assertEqual(self.src.host, server['OS-EXT-SRV-ATTR:host']) + + error = client.OpenStackApiException( + "Failed to remove source vol connection post live migration") + mock_migration.side_effect = error + + server = self._live_migrate( + server, migration_expected_state='error', + server_expected_state='ERROR') + + self.assertEqual(self.dest.host, server['OS-EXT-SRV-ATTR:host']) diff --git a/nova/tests/functional/regressions/test_bug_1781286.py b/nova/tests/functional/regressions/test_bug_1781286.py index 7b2d603092d..bb47eb0ea8a 100644 --- a/nova/tests/functional/regressions/test_bug_1781286.py +++ b/nova/tests/functional/regressions/test_bug_1781286.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import fixtures import mock from oslo_db import exception as oslo_db_exc @@ -67,11 +66,11 @@ def test_server_create_reschedule_blocked_az_up_call(self): def wrap_bari(*args, **kwargs): # Poison the AZ query to blow up as if the cell conductor does not # have access to the API DB. - self.useFixture( - fixtures.MockPatch( - 'nova.objects.AggregateList.get_by_host', - side_effect=oslo_db_exc.CantStartEngineError)) - return original_bari(*args, **kwargs) + with mock.patch( + 'nova.objects.AggregateList.get_by_host', + side_effect=oslo_db_exc.CantStartEngineError + ): + return original_bari(*args, **kwargs) self.stub_out('nova.compute.manager.ComputeManager.' 'build_and_run_instance', wrap_bari) @@ -81,10 +80,6 @@ def wrap_bari(*args, **kwargs): # compute service we have to wait for the notification that the build # is complete and then stop the mock so we can use the API again. self.notifier.wait_for_versioned_notifications('instance.create.end') - # Note that we use stopall here because we actually called - # build_and_run_instance twice so we have more than one instance of - # the mock that needs to be stopped. - mock.patch.stopall() server = self._wait_for_state_change(server, 'ACTIVE') # We should have rescheduled and the instance AZ should be set from the # Selection object. Since neither compute host is in an AZ, the server @@ -128,19 +123,20 @@ def test_migrate_reschedule_blocked_az_up_call(self): self.rescheduled = None def wrap_prep_resize(_self, *args, **kwargs): - # Poison the AZ query to blow up as if the cell conductor does not - # have access to the API DB. - self.agg_mock = self.useFixture( - fixtures.MockPatch( - 'nova.objects.AggregateList.get_by_host', - side_effect=oslo_db_exc.CantStartEngineError)).mock if self.rescheduled is None: # Track the first host that we rescheduled from. self.rescheduled = _self.host # Trigger a reschedule. raise exception.ComputeResourcesUnavailable( reason='test_migrate_reschedule_blocked_az_up_call') - return original_prep_resize(_self, *args, **kwargs) + # Poison the AZ query to blow up as if the cell conductor does not + # have access to the API DB. + with mock.patch( + 'nova.objects.AggregateList.get_by_host', + side_effect=oslo_db_exc.CantStartEngineError, + ) as agg_mock: + self.agg_mock = agg_mock + return original_prep_resize(_self, *args, **kwargs) self.stub_out('nova.compute.manager.ComputeManager._prep_resize', wrap_prep_resize) diff --git a/nova/tests/functional/regressions/test_bug_1888395.py b/nova/tests/functional/regressions/test_bug_1888395.py index e582ad3e851..c50b78e2f66 100644 --- a/nova/tests/functional/regressions/test_bug_1888395.py +++ b/nova/tests/functional/regressions/test_bug_1888395.py @@ -23,14 +23,8 @@ from nova.tests.functional.libvirt import base as libvirt_base -class TestLiveMigrationWithoutMultiplePortBindings( +class TestLiveMigrationWithoutMultiplePortBindingsBase( libvirt_base.ServersTestBase): - """Regression test for bug 1888395. - - This regression test asserts that Live migration works when - neutron does not support the binding-extended api extension - and the legacy single port binding workflow is used. - """ ADMIN_API = True microversion = 'latest' @@ -72,6 +66,16 @@ def setUp(self): 'nova.tests.fixtures.libvirt.Domain.migrateToURI3', self._migrate_stub)) + +class TestLiveMigrationWithoutMultiplePortBindings( + TestLiveMigrationWithoutMultiplePortBindingsBase): + """Regression test for bug 1888395. + + This regression test asserts that Live migration works when + neutron does not support the binding-extended api extension + and the legacy single port binding workflow is used. + """ + def _migrate_stub(self, domain, destination, params, flags): """Stub out migrateToURI3.""" @@ -124,3 +128,25 @@ def test_live_migrate(self): server, {'OS-EXT-SRV-ATTR:host': 'end_host', 'status': 'ACTIVE'}) msg = "NotImplementedError: Cannot load 'vif_type' in the base class" self.assertNotIn(msg, self.stdlog.logger.output) + + +class TestLiveMigrationRollbackWithoutMultiplePortBindings( + TestLiveMigrationWithoutMultiplePortBindingsBase): + + def _migrate_stub(self, domain, destination, params, flags): + source = self.computes['start_host'] + conn = source.driver._host.get_connection() + dom = conn.lookupByUUIDString(self.server['id']) + dom.fail_job() + + def test_live_migration_rollback(self): + self.server = self._create_server( + host='start_host', + networks=[{'port': self.neutron.port_1['id']}]) + + self.assertFalse( + self.neutron_api.has_port_binding_extension(self.ctxt)) + # NOTE(artom) The live migration will still fail (we fail it in + # _migrate_stub()), but the server should correctly rollback to ACTIVE. + self._live_migrate(self.server, migration_expected_state='failed', + server_expected_state='ACTIVE') diff --git a/nova/tests/functional/regressions/test_bug_1890244.py b/nova/tests/functional/regressions/test_bug_1890244.py new file mode 100644 index 00000000000..bf969eebe77 --- /dev/null +++ b/nova/tests/functional/regressions/test_bug_1890244.py @@ -0,0 +1,96 @@ +# Copyright 2017 Ericsson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from nova import context +from nova import objects +from nova import test +from nova.tests import fixtures as nova_fixtures +from nova.tests.functional import fixtures as func_fixtures +from nova.tests.functional import integrated_helpers + + +class IgnoreDeletedServerGroupsTest( + test.TestCase, integrated_helpers.InstanceHelperMixin, +): + """Regression test for bug 1890244 + + If instance are created as member of server groups it + should be possibel to evacuate them if the server groups are + deleted prior to the host failure. + """ + + def setUp(self): + super().setUp() + # Stub out external dependencies. + self.useFixture(nova_fixtures.NeutronFixture(self)) + self.useFixture(nova_fixtures.GlanceFixture(self)) + self.useFixture(func_fixtures.PlacementFixture()) + # Start nova controller services. + api_fixture = self.useFixture(nova_fixtures.OSAPIFixture( + api_version='v2.1')) + self.api = api_fixture.admin_api + self.start_service('conductor') + # Use a custom weigher to make sure that we have a predictable + # scheduling sort order. + self.useFixture(nova_fixtures.HostNameWeigherFixture()) + self.start_service('scheduler') + # Start two computes, one where the server will be created and another + # where we'll evacuate it to. + self.src = self._start_compute('host1') + self.dest = self._start_compute('host2') + self.notifier = self.useFixture( + nova_fixtures.NotificationFixture(self) + ) + + def test_evacuate_after_group_delete(self): + # Create an anti-affinity group for the server. + body = { + 'server_group': { + 'name': 'test-group', + 'policies': ['anti-affinity'] + } + } + group_id = self.api.api_post( + '/os-server-groups', body).body['server_group']['id'] + + # Create a server in the group which should land on host1 due to our + # custom weigher. + body = {'server': self._build_server()} + body['os:scheduler_hints'] = {'group': group_id} + server = self.api.post_server(body) + server = self._wait_for_state_change(server, 'ACTIVE') + self.assertEqual('host1', server['OS-EXT-SRV-ATTR:host']) + + # Down the source compute to enable the evacuation + self.api.microversion = '2.11' # Cap for the force-down call. + self.api.force_down_service('host1', 'nova-compute', True) + self.api.microversion = 'latest' + self.src.stop() + + # assert the server currently has a server group + reqspec = objects.RequestSpec.get_by_instance_uuid( + context.get_admin_context(), server['id']) + self.assertIsNotNone(reqspec.instance_group) + self.assertIn('group', reqspec.scheduler_hints) + # then delete it so that we need to clean it up on evac + self.api.api_delete(f'/os-server-groups/{group_id}') + + # Initiate evacuation + server = self._evacuate_server( + server, expected_host='host2', expected_migration_status='done' + ) + reqspec = objects.RequestSpec.get_by_instance_uuid( + context.get_admin_context(), server['id']) + self.assertIsNone(reqspec.instance_group) + self.assertNotIn('group', reqspec.scheduler_hints) diff --git a/nova/tests/functional/regressions/test_bug_1896463.py b/nova/tests/functional/regressions/test_bug_1896463.py index 6663ebe8cd3..dc74791e0e5 100644 --- a/nova/tests/functional/regressions/test_bug_1896463.py +++ b/nova/tests/functional/regressions/test_bug_1896463.py @@ -51,14 +51,6 @@ def setUp(self): self.api_fixture = self.useFixture(nova_fixtures.OSAPIFixture( api_version='v2.1')) - self.useFixture(fixtures.MockPatch( - 'nova.pci.utils.get_mac_by_pci_address', - return_value='52:54:00:1e:59:c6')) - - self.useFixture(fixtures.MockPatch( - 'nova.pci.utils.get_vf_num_by_pci_address', - return_value=1)) - self.admin_api = self.api_fixture.admin_api self.admin_api.microversion = 'latest' self.api = self.admin_api diff --git a/nova/tests/functional/regressions/test_bug_1944619.py b/nova/tests/functional/regressions/test_bug_1944619.py new file mode 100644 index 00000000000..82b7475dca8 --- /dev/null +++ b/nova/tests/functional/regressions/test_bug_1944619.py @@ -0,0 +1,76 @@ +# Copyright 2021, Canonical, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from nova import exception as nova_exceptions +from nova.tests.fixtures import libvirt as fakelibvirt +from nova.tests.functional import integrated_helpers +from nova.tests.functional.libvirt import base + + +class TestRollbackWithHWOffloadedOVS( + base.LibvirtMigrationMixin, + base.ServersTestBase, + integrated_helpers.InstanceHelperMixin +): + """Regression test for bug LP#1944619 + + Assert the behaviour observed in bug LP#1944619 caused by the live + migration cleanup code being used to cleanup pre-live migration failures. + When SRIOV devices are in use on a VM, that will cause the source host to + try to re-attach a VIF not actually de-attached causing a failure. + + The exception mocked in pre_live_migration reproduce an arbitrary error + that might cause the pre-live migration process to fail and + rollback_live_migration_at_source reproduce the device re-attach failure. + """ + + api_major_version = 'v2.1' + microversion = 'latest' + ADMIN_API = True + + def setUp(self): + super().setUp() + + self.start_compute( + hostname='src', + host_info=fakelibvirt.HostInfo( + cpu_nodes=1, cpu_sockets=1, cpu_cores=4, cpu_threads=1)) + self.start_compute( + hostname='dest', + host_info=fakelibvirt.HostInfo( + cpu_nodes=1, cpu_sockets=1, cpu_cores=4, cpu_threads=1)) + + self.src = self.computes['src'] + self.dest = self.computes['dest'] + + def test_rollback_pre_live_migration(self): + self.server = self._create_server(host='src', networks='none') + + lib_path = "nova.virt.libvirt.driver.LibvirtDriver" + funtion_path = "pre_live_migration" + mock_lib_path_prelive = "%s.%s" % (lib_path, funtion_path) + with mock.patch(mock_lib_path_prelive, + side_effect=nova_exceptions.DestinationDiskExists( + path='/var/non/existent')) as mlpp: + funtion_path = "rollback_live_migration_at_source" + mock_lib_path_rollback = "%s.%s" % (lib_path, funtion_path) + with mock.patch(mock_lib_path_rollback) as mlpr: + # Live migrate the instance to another host + self._live_migrate(self.server, + migration_expected_state='failed', + server_expected_state='MIGRATING') + mlpr.assert_not_called() + mlpp.assert_called_once() diff --git a/nova/tests/functional/regressions/test_bug_1951656.py b/nova/tests/functional/regressions/test_bug_1951656.py new file mode 100644 index 00000000000..d705ff6fe31 --- /dev/null +++ b/nova/tests/functional/regressions/test_bug_1951656.py @@ -0,0 +1,73 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_utils import uuidutils + + +from nova.tests.fixtures import libvirt as fakelibvirt +from nova.tests.functional.libvirt import test_vgpu +from nova.virt.libvirt import utils as libvirt_utils + + +class VGPUTestsLibvirt7_7(test_vgpu.VGPUTestBase): + + def _create_mdev(self, physical_device, mdev_type, uuid=None): + # We need to fake the newly created sysfs object by adding a new + # FakeMdevDevice in the existing persisted Connection object so + # when asking to get the existing mdevs, we would see it. + if not uuid: + uuid = uuidutils.generate_uuid() + mdev_name = libvirt_utils.mdev_uuid2name(uuid) + libvirt_parent = self.pci2libvirt_address(physical_device) + + # Libvirt 7.7 now creates mdevs with a parent_addr suffix. + new_mdev_name = '_'.join([mdev_name, libvirt_parent]) + + # Here, we get the right compute thanks by the self.current_host that + # was modified just before + connection = self.computes[ + self._current_host].driver._host.get_connection() + connection.mdev_info.devices.update( + {mdev_name: fakelibvirt.FakeMdevDevice(dev_name=new_mdev_name, + type_id=mdev_type, + parent=libvirt_parent)}) + return uuid + + def setUp(self): + super(VGPUTestsLibvirt7_7, self).setUp() + extra_spec = {"resources:VGPU": "1"} + self.flavor = self._create_flavor(extra_spec=extra_spec) + + # Start compute1 supporting only nvidia-11 + self.flags( + enabled_mdev_types=fakelibvirt.NVIDIA_11_VGPU_TYPE, + group='devices') + + self.compute1 = self.start_compute_with_vgpu('host1') + + def test_create_servers_with_vgpu(self): + + # Create a single instance against a specific compute node. + self._create_server( + image_uuid='155d900f-4e14-4e4c-a73d-069cbf4541e6', + flavor_id=self.flavor, host=self.compute1.host, + networks='auto', expected_state='ACTIVE') + + self.assert_mdev_usage(self.compute1, expected_amount=1) + + self._create_server( + image_uuid='155d900f-4e14-4e4c-a73d-069cbf4541e6', + flavor_id=self.flavor, host=self.compute1.host, + networks='auto', expected_state='ACTIVE') + + self.assert_mdev_usage(self.compute1, expected_amount=2) diff --git a/nova/tests/functional/regressions/test_bug_1978983.py b/nova/tests/functional/regressions/test_bug_1978983.py new file mode 100644 index 00000000000..51465900da0 --- /dev/null +++ b/nova/tests/functional/regressions/test_bug_1978983.py @@ -0,0 +1,71 @@ +# Copyright 2022 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova import test +from nova.tests import fixtures as nova_fixtures +from nova.tests.functional import fixtures as func_fixtures +from nova.tests.functional import integrated_helpers + + +class EvacuateServerWithTaskState( + test.TestCase, integrated_helpers.InstanceHelperMixin, +): + """Regression test for bug 1978983 + If instance task state is powering-off or not None + instance should be allowed to evacuate. + """ + + def setUp(self): + super().setUp() + # Stub out external dependencies. + self.useFixture(nova_fixtures.NeutronFixture(self)) + self.useFixture(nova_fixtures.GlanceFixture(self)) + self.useFixture(func_fixtures.PlacementFixture()) + self.useFixture(nova_fixtures.HostNameWeigherFixture()) + + # Start nova controller services. + self.start_service('conductor') + self.start_service('scheduler') + + api_fixture = self.useFixture(nova_fixtures.OSAPIFixture( + api_version='v2.1')) + self.api = api_fixture.admin_api + self.api.microversion = 'latest' + + self.src = self._start_compute(host='host1') + self.dest = self._start_compute(host='host2') + + def test_evacuate_instance(self): + """Evacuating a server + """ + server = self._create_server(networks=[]) + + server = self._wait_for_state_change(server, 'ACTIVE') + self.assertEqual(self.src.host, server['OS-EXT-SRV-ATTR:host']) + + # stop host1 compute service + self.src.stop() + self.api.put_service_force_down(self.src.service_ref.uuid, True) + + # poweroff instance + self._stop_server(server, wait_for_stop=False) + server = self._wait_for_server_parameter( + server, {'OS-EXT-STS:task_state': 'powering-off'}) + + # evacuate instance + server = self._evacuate_server( + server, expected_host=self.dest.host + ) + self.assertEqual(self.dest.host, server['OS-EXT-SRV-ATTR:host']) diff --git a/nova/tests/functional/regressions/test_bug_1983753.py b/nova/tests/functional/regressions/test_bug_1983753.py new file mode 100644 index 00000000000..78499335ec9 --- /dev/null +++ b/nova/tests/functional/regressions/test_bug_1983753.py @@ -0,0 +1,177 @@ +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import fixtures + +from oslo_serialization import jsonutils + +from nova.tests.fixtures import libvirt as fakelibvirt +from nova.tests.functional.libvirt import test_pci_sriov_servers + + +class TestPciResize(test_pci_sriov_servers._PCIServersTestBase): + # these tests use multiple different configs so the whitelist is set by + # each testcase individually + PCI_PASSTHROUGH_WHITELIST = [] + PCI_ALIAS = [ + jsonutils.dumps(x) + for x in [ + { + "vendor_id": fakelibvirt.PCI_VEND_ID, + "product_id": fakelibvirt.PCI_PROD_ID, + "name": "a-pci-dev", + }, + { + "vendor_id": fakelibvirt.PCI_VEND_ID, + "product_id": fakelibvirt.PF_PROD_ID, + "device_type": "type-PF", + "name": "a-pf", + }, + { + "vendor_id": fakelibvirt.PCI_VEND_ID, + "product_id": fakelibvirt.VF_PROD_ID, + "device_type": "type-VF", + "name": "a-vf", + }, + ] + ] + + def setUp(self): + super().setUp() + self.useFixture( + fixtures.MockPatch( + 'nova.virt.libvirt.driver.LibvirtDriver.' + 'migrate_disk_and_power_off', + return_value='{}' + ) + ) + # These tests should not depend on the host's sysfs + self.useFixture( + fixtures.MockPatch('nova.pci.utils.is_physical_function')) + self.useFixture( + fixtures.MockPatch( + 'nova.pci.utils.get_function_by_ifname', + return_value=(None, False) + ) + ) + + def _test_resize_from_two_devs_to_one_dev(self, num_pci_on_dest): + # The fake libvirt will emulate on the host: + # * two type-PCI in slot 0, 1 + compute1_pci_info = fakelibvirt.HostPCIDevicesInfo(num_pci=2) + # the config matches the PCI dev + compute1_device_spec = [ + jsonutils.dumps(x) + for x in [ + { + "vendor_id": fakelibvirt.PCI_VEND_ID, + "product_id": fakelibvirt.PCI_PROD_ID, + }, + ] + ] + self.flags(group='pci', passthrough_whitelist=compute1_device_spec) + self.start_compute(hostname="compute1", pci_info=compute1_pci_info) + self.assertPCIDeviceCounts("compute1", total=2, free=2) + + # create a server that requests two PCI devs + extra_spec = {"pci_passthrough:alias": "a-pci-dev:2"} + flavor_id = self._create_flavor(extra_spec=extra_spec) + server = self._create_server(flavor_id=flavor_id, networks=[]) + self.assertPCIDeviceCounts("compute1", total=2, free=0) + + # start another compute with a different amount of PCI dev available + compute2_pci_info = fakelibvirt.HostPCIDevicesInfo( + num_pci=num_pci_on_dest) + # the config matches the PCI dev + compute2_device_spec = [ + jsonutils.dumps(x) + for x in [ + { + "vendor_id": fakelibvirt.PCI_VEND_ID, + "product_id": fakelibvirt.PCI_PROD_ID, + }, + ] + ] + self.flags(group='pci', passthrough_whitelist=compute2_device_spec) + self.start_compute(hostname="compute2", pci_info=compute2_pci_info) + self.assertPCIDeviceCounts( + "compute2", total=num_pci_on_dest, free=num_pci_on_dest) + + # resize the server to request only one PCI dev instead of the current + # two. This should fit to compute2 having at least one dev + extra_spec = {"pci_passthrough:alias": "a-pci-dev:1"} + flavor_id = self._create_flavor(extra_spec=extra_spec) + self._resize_server(server, flavor_id=flavor_id) + self._confirm_resize(server) + self.assertPCIDeviceCounts("compute1", total=2, free=2) + self.assertPCIDeviceCounts( + "compute2", total=num_pci_on_dest, free=num_pci_on_dest - 1) + + def test_resize_from_two_devs_to_one_dev_dest_has_two_devs(self): + self._test_resize_from_two_devs_to_one_dev(num_pci_on_dest=2) + + def test_resize_from_two_devs_to_one_dev_dest_has_one_dev(self): + self._test_resize_from_two_devs_to_one_dev(num_pci_on_dest=1) + + def test_resize_from_vf_to_pf(self): + # The fake libvirt will emulate on the host: + # * one type-PF in slot 0 with one VF + compute1_pci_info = fakelibvirt.HostPCIDevicesInfo( + num_pfs=1, num_vfs=1) + # the config matches only the VF + compute1_device_spec = [ + jsonutils.dumps(x) + for x in [ + { + "vendor_id": fakelibvirt.PCI_VEND_ID, + "product_id": fakelibvirt.VF_PROD_ID, + }, + ] + ] + self.flags(group='pci', passthrough_whitelist=compute1_device_spec) + self.start_compute(hostname="compute1", pci_info=compute1_pci_info) + self.assertPCIDeviceCounts("compute1", total=1, free=1) + + # create a server that requests one Vf + extra_spec = {"pci_passthrough:alias": "a-vf:1"} + flavor_id = self._create_flavor(extra_spec=extra_spec) + server = self._create_server(flavor_id=flavor_id, networks=[]) + self.assertPCIDeviceCounts("compute1", total=1, free=0) + + # start another compute with a single PF dev available + # The fake libvirt will emulate on the host: + # * one type-PF in slot 0 with 1 VF + compute2_pci_info = fakelibvirt.HostPCIDevicesInfo( + num_pfs=1, num_vfs=1) + # the config matches the PF dev but not the VF + compute2_device_spec = [ + jsonutils.dumps(x) + for x in [ + { + "vendor_id": fakelibvirt.PCI_VEND_ID, + "product_id": fakelibvirt.PF_PROD_ID, + }, + ] + ] + self.flags(group='pci', passthrough_whitelist=compute2_device_spec) + self.start_compute(hostname="compute2", pci_info=compute2_pci_info) + self.assertPCIDeviceCounts("compute2", total=1, free=1) + + # resize the server to request on PF dev instead of the current VF + # dev. This should fit to compute2 having exactly one PF dev. + extra_spec = {"pci_passthrough:alias": "a-pf:1"} + flavor_id = self._create_flavor(extra_spec=extra_spec) + self._resize_server(server, flavor_id=flavor_id) + self._confirm_resize(server) + self.assertPCIDeviceCounts("compute1", total=1, free=1) + self.assertPCIDeviceCounts("compute2", total=1, free=0) diff --git a/nova/tests/functional/regressions/test_bug_2025480.py b/nova/tests/functional/regressions/test_bug_2025480.py new file mode 100644 index 00000000000..c707a40a846 --- /dev/null +++ b/nova/tests/functional/regressions/test_bug_2025480.py @@ -0,0 +1,86 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from unittest import mock + +from nova import context +from nova.objects import compute_node +from nova import test +from nova.tests import fixtures as nova_fixtures +from nova.tests.functional import fixtures as func_fixtures +from nova.tests.functional import integrated_helpers + + +class UnshelveUpdateAvailableResourcesPeriodicRace( + test.TestCase, integrated_helpers.InstanceHelperMixin): + def setUp(self): + super(UnshelveUpdateAvailableResourcesPeriodicRace, self).setUp() + + placement = func_fixtures.PlacementFixture() + self.useFixture(placement) + self.placement = placement.api + self.neutron = nova_fixtures.NeutronFixture(self) + self.useFixture(self.neutron) + self.useFixture(nova_fixtures.GlanceFixture(self)) + # Start nova services. + self.api = self.useFixture(nova_fixtures.OSAPIFixture( + api_version='v2.1')).admin_api + self.api.microversion = 'latest' + self.notifier = self.useFixture( + nova_fixtures.NotificationFixture(self)) + + self.start_service('conductor') + self.start_service('scheduler') + + def test_unshelve_spawning_update_available_resources(self): + compute = self._start_compute('compute1') + + server = self._create_server( + networks=[{'port': self.neutron.port_1['id']}]) + + node = compute_node.ComputeNode.get_by_nodename( + context.get_admin_context(), 'compute1') + self.assertEqual(1, node.vcpus_used) + + # with default config shelve means immediate offload as well + req = { + 'shelve': {} + } + self.api.post_server_action(server['id'], req) + self._wait_for_server_parameter( + server, {'status': 'SHELVED_OFFLOADED', + 'OS-EXT-SRV-ATTR:host': None}) + + node = compute_node.ComputeNode.get_by_nodename( + context.get_admin_context(), 'compute1') + self.assertEqual(0, node.vcpus_used) + + def fake_spawn(*args, **kwargs): + self._run_periodics() + + with mock.patch.object( + compute.driver, 'spawn', side_effect=fake_spawn): + req = {'unshelve': None} + self.api.post_server_action(server['id'], req) + self.notifier.wait_for_versioned_notifications( + 'instance.unshelve.start') + self._wait_for_server_parameter( + server, + { + 'status': 'ACTIVE', + 'OS-EXT-STS:task_state': None, + 'OS-EXT-SRV-ATTR:host': 'compute1', + }) + + node = compute_node.ComputeNode.get_by_nodename( + context.get_admin_context(), 'compute1') + # After the fix, the instance should have resources claimed + self.assertEqual(1, node.vcpus_used) diff --git a/nova/tests/functional/test_aggregates.py b/nova/tests/functional/test_aggregates.py index 8dfb3455782..1ffa3ada92c 100644 --- a/nova/tests/functional/test_aggregates.py +++ b/nova/tests/functional/test_aggregates.py @@ -935,11 +935,11 @@ def setUp(self): # Start nova services. self.start_service('conductor') - self.admin_api = self.useFixture( - nova_fixtures.OSAPIFixture(api_version='v2.1')).admin_api - self.api = self.useFixture( - nova_fixtures.OSAPIFixture(api_version='v2.1', - project_id=uuids.non_admin)).api + api_fixture = self.useFixture( + nova_fixtures.OSAPIFixture(api_version='v2.1')) + self.admin_api = api_fixture.admin_api + self.api = api_fixture.api + self.api.project_id = uuids.non_admin # Add the AggregateMultiTenancyIsolation to the list of enabled # filters since it is not enabled by default. enabled_filters = CONF.filter_scheduler.enabled_filters @@ -1037,15 +1037,15 @@ def setUp(self): self.glance = self.useFixture(nova_fixtures.GlanceFixture(self)) self.useFixture(nova_fixtures.NeutronFixture(self)) self.useFixture(func_fixtures.PlacementFixture()) - # Intentionally keep these separate since we want to create the - # server with the non-admin user in a different project. - admin_api_fixture = self.useFixture(nova_fixtures.OSAPIFixture( + # Intentionally define different project id for the two client since + # we want to create the server with the non-admin user in a different + # project. + api_fixture = self.useFixture(nova_fixtures.OSAPIFixture( api_version='v2.1', project_id=uuids.admin_project)) - self.admin_api = admin_api_fixture.admin_api + self.admin_api = api_fixture.admin_api self.admin_api.microversion = 'latest' - user_api_fixture = self.useFixture(nova_fixtures.OSAPIFixture( - api_version='v2.1', project_id=uuids.user_project)) - self.api = user_api_fixture.api + self.api = api_fixture.api + self.api.project_id = uuids.user_project self.api.microversion = 'latest' self.start_service('conductor') diff --git a/nova/tests/functional/test_images.py b/nova/tests/functional/test_images.py index 340e883da96..e7e9f2a6c94 100644 --- a/nova/tests/functional/test_images.py +++ b/nova/tests/functional/test_images.py @@ -12,7 +12,6 @@ from oslo_utils.fixture import uuidsentinel as uuids -from nova.tests import fixtures as nova_fixtures from nova.tests.functional.api import client from nova.tests.functional import integrated_helpers @@ -70,10 +69,9 @@ def test_admin_snapshot_user_image_access_member(self): server = self.api.post_server({"server": server}) server = self._wait_for_state_change(server, 'ACTIVE') - # Create an admin API fixture with a unique project ID. - admin_api = self.useFixture( - nova_fixtures.OSAPIFixture( - project_id=uuids.admin_project)).admin_api + # use an admin API with a unique project ID. + admin_api = self.api_fixture.alternative_admin_api + admin_api.project_id = uuids.admin_project # Create a snapshot of the server using the admin project. name = 'admin-created-snapshot' diff --git a/nova/tests/functional/test_server_group.py b/nova/tests/functional/test_server_group.py index 08e47b3971a..a562df84078 100644 --- a/nova/tests/functional/test_server_group.py +++ b/nova/tests/functional/test_server_group.py @@ -19,6 +19,7 @@ from nova.compute import instance_actions from nova import context from nova.db.main import api as db +from nova import objects from nova import test from nova.tests import fixtures as nova_fixtures from nova.tests.functional.api import client @@ -64,12 +65,12 @@ def setUp(self): self.useFixture(nova_fixtures.NeutronFixture(self)) self.useFixture(func_fixtures.PlacementFixture()) - api_fixture = self.useFixture(nova_fixtures.OSAPIFixture( + self.api_fixture = self.useFixture(nova_fixtures.OSAPIFixture( api_version='v2.1')) - self.api = api_fixture.api + self.api = self.api_fixture.api self.api.microversion = self.microversion - self.admin_api = api_fixture.admin_api + self.admin_api = self.api_fixture.admin_api self.admin_api.microversion = self.microversion self.start_service('conductor') @@ -174,13 +175,8 @@ def test_get_groups_all_projects(self): # Create an API using project 'openstack1'. # This is a non-admin API. - # - # NOTE(sdague): this is actually very much *not* how this - # fixture should be used. This actually spawns a whole - # additional API server. Should be addressed in the future. - api_openstack1 = self.useFixture(nova_fixtures.OSAPIFixture( - api_version=self.api_major_version, - project_id=PROJECT_ID_ALT)).api + api_openstack1 = self.api_fixture.alternative_api + api_openstack1.project_id = PROJECT_ID_ALT api_openstack1.microversion = self.microversion # Create a server group in project 'openstack' @@ -499,6 +495,85 @@ def test_soft_affinity_not_supported(self): self.assertIn('Invalid input', ex.response.text) self.assertIn('soft-affinity', ex.response.text) + @mock.patch('nova.scheduler.filters.affinity_filter.' + 'ServerGroupAffinityFilter.host_passes', return_value=True) + def test_failed_count_with_affinity_violation(self, mock_host_passes): + """Check failed count not incremented after violation of the late + affinity check. https://bugs.launchpad.net/nova/+bug/1996732 + """ + + created_group = self.api.post_server_groups(self.affinity) + flavor = self.api.get_flavors()[2] + + # Ensure the first instance is on compute1 + with utils.temporary_mutation(self.admin_api, microversion='2.53'): + compute2_service_id = self.admin_api.get_services( + host=self.compute2.host, binary='nova-compute')[0]['id'] + self.admin_api.put_service(compute2_service_id, + {'status': 'disabled'}) + + self._boot_a_server_to_group(created_group, flavor=flavor) + + # Ensure the second instance is on compute2 + with utils.temporary_mutation(self.admin_api, microversion='2.53'): + self.admin_api.put_service(compute2_service_id, + {'status': 'enabled'}) + compute1_service_id = self.admin_api.get_services( + host=self.compute.host, binary='nova-compute')[0]['id'] + self.admin_api.put_service(compute1_service_id, + {'status': 'disabled'}) + + # Expects GroupAffinityViolation exception + failed_server = self._boot_a_server_to_group(created_group, + flavor=flavor, + expected_status='ERROR') + + self.assertEqual('Exceeded maximum number of retries. Exhausted all ' + 'hosts available for retrying build failures for ' + 'instance %s.' % failed_server['id'], + failed_server['fault']['message']) + + ctxt = context.get_admin_context() + computes = objects.ComputeNodeList.get_all(ctxt) + + for node in computes: + self.assertEqual(node.stats.get('failed_builds'), '0') + + @mock.patch('nova.scheduler.filters.affinity_filter.' + 'ServerGroupAntiAffinityFilter.host_passes', return_value=True) + def test_failed_count_with_anti_affinity_violation(self, mock_host_passes): + """Check failed count after violation of the late affinity check. + https://bugs.launchpad.net/nova/+bug/1996732 + """ + + created_group = self.api.post_server_groups(self.anti_affinity) + flavor = self.api.get_flavors()[2] + + # Ensure two instances are scheduled on the same host + with utils.temporary_mutation(self.admin_api, microversion='2.53'): + compute2_service_id = self.admin_api.get_services( + host=self.compute2.host, binary='nova-compute')[0]['id'] + self.admin_api.put_service(compute2_service_id, + {'status': 'disabled'}) + + self._boot_a_server_to_group(created_group, flavor=flavor) + + # Expects GroupAffinityViolation exception + failed_server = self._boot_a_server_to_group(created_group, + flavor=flavor, + expected_status='ERROR') + + self.assertEqual('Exceeded maximum number of retries. Exhausted all ' + 'hosts available for retrying build failures for ' + 'instance %s.' % failed_server['id'], + failed_server['fault']['message']) + + ctxt = context.get_admin_context() + computes = objects.ComputeNodeList.get_all(ctxt) + + for node in computes: + self.assertEqual(node.stats.get('failed_builds'), '0') + class ServerGroupAffinityConfTest(ServerGroupTestBase): api_major_version = 'v2.1' diff --git a/nova/tests/functional/test_server_rescue.py b/nova/tests/functional/test_server_rescue.py index fa96c10344a..8f5b9129437 100644 --- a/nova/tests/functional/test_server_rescue.py +++ b/nova/tests/functional/test_server_rescue.py @@ -10,6 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime + +from oslo_utils.fixture import uuidsentinel as uuids + from nova.tests import fixtures as nova_fixtures from nova.tests.functional.api import client from nova.tests.functional import integrated_helpers @@ -23,7 +27,37 @@ def setUp(self): self.useFixture(nova_fixtures.CinderFixture(self)) self._start_compute(host='host1') - def _create_bfv_server(self): + def _create_image(self, metadata=None): + image = { + 'id': uuids.stable_rescue_image, + 'name': 'fake-image-rescue-property', + 'created_at': datetime.datetime(2011, 1, 1, 1, 2, 3), + 'updated_at': datetime.datetime(2011, 1, 1, 1, 2, 3), + 'deleted_at': None, + 'deleted': False, + 'status': 'active', + 'is_public': False, + 'container_format': 'raw', + 'disk_format': 'raw', + 'size': '25165824', + 'min_ram': 0, + 'min_disk': 0, + 'protected': False, + 'visibility': 'public', + 'tags': ['tag1', 'tag2'], + 'properties': { + 'kernel_id': 'nokernel', + 'ramdisk_id': 'nokernel', + 'hw_rescue_device': 'disk', + 'hw_rescue_bus': 'scsi', + }, + } + if metadata: + image['properties'].update(metadata) + return self.glance.create(None, image) + + def _create_bfv_server(self, metadata=None): + image = self._create_image(metadata=metadata) server_request = self._build_server(networks=[]) server_request.pop('imageRef') server_request['block_device_mapping_v2'] = [{ @@ -33,7 +67,7 @@ def _create_bfv_server(self): 'destination_type': 'volume'}] server = self.api.post_server({'server': server_request}) self._wait_for_state_change(server, 'ACTIVE') - return server + return server, image class DisallowBFVRescuev286(BFVRescue): @@ -43,10 +77,10 @@ class DisallowBFVRescuev286(BFVRescue): microversion = '2.86' def test_bfv_rescue_not_supported(self): - server = self._create_bfv_server() + server, image = self._create_bfv_server() ex = self.assertRaises(client.OpenStackApiException, self.api.post_server_action, server['id'], {'rescue': { - 'rescue_image_ref': '155d900f-4e14-4e4c-a73d-069cbf4541e6'}}) + 'rescue_image_ref': image['id']}}) self.assertEqual(400, ex.response.status_code) self.assertIn('Cannot rescue a volume-backed instance', ex.response.text) @@ -60,10 +94,10 @@ class DisallowBFVRescuev286WithTrait(BFVRescue): microversion = '2.86' def test_bfv_rescue_not_supported(self): - server = self._create_bfv_server() + server, image = self._create_bfv_server() ex = self.assertRaises(client.OpenStackApiException, self.api.post_server_action, server['id'], {'rescue': { - 'rescue_image_ref': '155d900f-4e14-4e4c-a73d-069cbf4541e6'}}) + 'rescue_image_ref': image['id']}}) self.assertEqual(400, ex.response.status_code) self.assertIn('Cannot rescue a volume-backed instance', ex.response.text) @@ -77,10 +111,10 @@ class DisallowBFVRescuev287WithoutTrait(BFVRescue): microversion = '2.87' def test_bfv_rescue_not_supported(self): - server = self._create_bfv_server() + server, image = self._create_bfv_server() ex = self.assertRaises(client.OpenStackApiException, self.api.post_server_action, server['id'], {'rescue': { - 'rescue_image_ref': '155d900f-4e14-4e4c-a73d-069cbf4541e6'}}) + 'rescue_image_ref': image['id']}}) self.assertEqual(400, ex.response.status_code) self.assertIn('Host unable to rescue a volume-backed instance', ex.response.text) @@ -94,7 +128,41 @@ class AllowBFVRescuev287WithTrait(BFVRescue): microversion = '2.87' def test_bfv_rescue_supported(self): - server = self._create_bfv_server() + server, image = self._create_bfv_server() self.api.post_server_action(server['id'], {'rescue': { + 'rescue_image_ref': image['id']}}) + self._wait_for_state_change(server, 'RESCUE') + + +class DisallowBFVRescuev287WithoutRescueImageProperties(BFVRescue): + """Asserts that BFV rescue requests fail with microversion 2.87 (or later) + when the required hw_rescue_device and hw_rescue_bus image properties + are not set on the image. + """ + compute_driver = 'fake.MediumFakeDriver' + microversion = '2.87' + + def test_bfv_rescue_failed(self): + server, image = self._create_bfv_server() + # try rescue without hw_rescue_device and hw_rescue_bus properties set + ex = self.assertRaises(client.OpenStackApiException, + self.api.post_server_action, server['id'], {'rescue': { 'rescue_image_ref': '155d900f-4e14-4e4c-a73d-069cbf4541e6'}}) + self.assertEqual(400, ex.response.status_code) + self.assertIn('Cannot rescue a volume-backed instance', + ex.response.text) + + +class AllowBFVRescuev287WithRescueImageProperties(BFVRescue): + """Asserts that BFV rescue requests pass with microversion 2.87 (or later) + when the required hw_rescue_device and hw_rescue_bus image properties + are set on the image. + """ + compute_driver = 'fake.RescueBFVDriver' + microversion = '2.87' + + def test_bfv_rescue_done(self): + server, image = self._create_bfv_server() + self.api.post_server_action(server['id'], {'rescue': { + 'rescue_image_ref': image['id']}}) self._wait_for_state_change(server, 'RESCUE') diff --git a/nova/tests/functional/test_servers.py b/nova/tests/functional/test_servers.py index e77d4bf1ea2..440195cd196 100644 --- a/nova/tests/functional/test_servers.py +++ b/nova/tests/functional/test_servers.py @@ -1253,9 +1253,7 @@ def test_get_servers_detail_non_admin_with_deleted_flag(self): def test_get_servers_detail_filters(self): # We get the results only from the up cells, this ignoring the down # cells if list_records_by_skipping_down_cells config option is True. - api_fixture = self.useFixture(nova_fixtures.OSAPIFixture( - api_version='v2.1')) - self.admin_api = api_fixture.admin_api + self.admin_api = self.api_fixture.admin_api self.admin_api.microversion = '2.69' servers = self.admin_api.get_servers( search_opts={'hostname': "cell3-inst0"}) @@ -1263,9 +1261,7 @@ def test_get_servers_detail_filters(self): self.assertEqual(self.up_cell_insts[2], servers[0]['id']) def test_get_servers_detail_all_tenants_with_down_cells(self): - api_fixture = self.useFixture(nova_fixtures.OSAPIFixture( - api_version='v2.1')) - self.admin_api = api_fixture.admin_api + self.admin_api = self.api_fixture.admin_api self.admin_api.microversion = '2.69' servers = self.admin_api.get_servers(search_opts={'all_tenants': True}) # 4 servers from the up cells and 4 servers from the down cells @@ -1523,10 +1519,8 @@ class ServersTestV280(integrated_helpers._IntegratedTestBase): def setUp(self): super(ServersTestV280, self).setUp() - api_fixture = self.useFixture(nova_fixtures.OSAPIFixture( - api_version='v2.1')) - self.api = api_fixture.api - self.admin_api = api_fixture.admin_api + self.api = self.api_fixture.api + self.admin_api = self.api_fixture.admin_api self.api.microversion = '2.80' self.admin_api.microversion = '2.80' @@ -1585,9 +1579,8 @@ def test_get_migrations_after_live_migrate_server_in_different_project( project_id_1 = '4906260553374bf0a5d566543b320516' project_id_2 = 'c850298c1b6b4796a8f197ac310b2469' - new_api_fixture = self.useFixture(nova_fixtures.OSAPIFixture( - api_version=self.api_major_version, project_id=project_id_1)) - new_admin_api = new_api_fixture.admin_api + new_admin_api = self.api_fixture.alternative_admin_api + new_admin_api.project_id = project_id_1 new_admin_api.microversion = '2.80' post = { diff --git a/nova/tests/unit/api/openstack/compute/test_create_backup.py b/nova/tests/unit/api/openstack/compute/test_create_backup.py index f7280a5a370..70978d11dea 100644 --- a/nova/tests/unit/api/openstack/compute/test_create_backup.py +++ b/nova/tests/unit/api/openstack/compute/test_create_backup.py @@ -40,10 +40,6 @@ def setUp(self): self.controller = getattr(self.create_backup, self.controller_name)() self.compute_api = self.controller.compute_api - patch_get = mock.patch.object(self.compute_api, 'get') - self.mock_get = patch_get.start() - self.addCleanup(patch_get.stop) - @mock.patch.object(common, 'check_img_metadata_properties_quota') @mock.patch.object(api.API, 'backup') def test_create_backup_with_metadata(self, mock_backup, mock_check_image): diff --git a/nova/tests/unit/api/openstack/compute/test_flavor_access.py b/nova/tests/unit/api/openstack/compute/test_flavor_access.py index 8c25a2efc27..1c5c34e758d 100644 --- a/nova/tests/unit/api/openstack/compute/test_flavor_access.py +++ b/nova/tests/unit/api/openstack/compute/test_flavor_access.py @@ -353,14 +353,37 @@ def test_add_tenant_access_with_invalid_tenant(self, mock_verify): mock_verify.assert_called_once_with( req.environ['nova.context'], 'proj2') + @mock.patch('nova.objects.Flavor.remove_access') @mock.patch('nova.api.openstack.identity.verify_project_id', side_effect=exc.HTTPBadRequest( explanation="Project ID proj2 is not a valid project.")) - def test_remove_tenant_access_with_invalid_tenant(self, mock_verify): + def test_remove_tenant_access_with_invalid_tenant(self, + mock_verify, + mock_remove_access): """Tests the case that the tenant does not exist in Keystone.""" req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action', use_admin_context=True) body = {'removeTenantAccess': {'tenant': 'proj2'}} + + self.flavor_action_controller._remove_tenant_access( + req, '2', body=body) + mock_verify.assert_called_once_with( + req.environ['nova.context'], 'proj2') + mock_remove_access.assert_called_once_with('proj2') + + @mock.patch('nova.api.openstack.identity.verify_project_id', + side_effect=exc.HTTPBadRequest( + explanation="Nova was unable to find Keystone " + "service endpoint.")) + def test_remove_tenant_access_missing_keystone_endpoint(self, + mock_verify): + """Tests the case that Keystone identity service endpoint + version 3.0 was not found. + """ + req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action', + use_admin_context=True) + body = {'removeTenantAccess': {'tenant': 'proj2'}} + self.assertRaises(exc.HTTPBadRequest, self.flavor_action_controller._remove_tenant_access, req, '2', body=body) diff --git a/nova/tests/unit/api/openstack/compute/test_hypervisors.py b/nova/tests/unit/api/openstack/compute/test_hypervisors.py index facc5389be3..6545031a0ba 100644 --- a/nova/tests/unit/api/openstack/compute/test_hypervisors.py +++ b/nova/tests/unit/api/openstack/compute/test_hypervisors.py @@ -368,25 +368,23 @@ def fake_service_get_by_compute_host(context, host): return TEST_SERVICES[0] raise exception.ComputeHostNotFound(host=host) - @mock.patch.object(self.controller.host_api, 'compute_node_get_all', - return_value=compute_nodes) - @mock.patch.object(self.controller.host_api, - 'service_get_by_compute_host', - fake_service_get_by_compute_host) - def _test(self, compute_node_get_all): - req = self._get_request(True) - result = self.controller.index(req) - self.assertEqual(1, len(result['hypervisors'])) - expected = { - 'id': compute_nodes[0].uuid if self.expect_uuid_for_id - else compute_nodes[0].id, - 'hypervisor_hostname': compute_nodes[0].hypervisor_hostname, - 'state': 'up', - 'status': 'enabled', - } - self.assertDictEqual(expected, result['hypervisors'][0]) + m_get = self.controller.host_api.compute_node_get_all + m_get.side_effect = None + m_get.return_value = compute_nodes + self.controller.host_api.service_get_by_compute_host.side_effect = ( + fake_service_get_by_compute_host) - _test(self) + req = self._get_request(True) + result = self.controller.index(req) + self.assertEqual(1, len(result['hypervisors'])) + expected = { + 'id': compute_nodes[0].uuid if self.expect_uuid_for_id + else compute_nodes[0].id, + 'hypervisor_hostname': compute_nodes[0].hypervisor_hostname, + 'state': 'up', + 'status': 'enabled', + } + self.assertDictEqual(expected, result['hypervisors'][0]) def test_index_compute_host_not_mapped(self): """Tests that we don't fail index if a host is not mapped.""" @@ -402,25 +400,22 @@ def fake_service_get_by_compute_host(context, host): return TEST_SERVICES[0] raise exception.HostMappingNotFound(name=host) - @mock.patch.object(self.controller.host_api, 'compute_node_get_all', - return_value=compute_nodes) - @mock.patch.object(self.controller.host_api, - 'service_get_by_compute_host', - fake_service_get_by_compute_host) - def _test(self, compute_node_get_all): - req = self._get_request(True) - result = self.controller.index(req) - self.assertEqual(1, len(result['hypervisors'])) - expected = { - 'id': compute_nodes[0].uuid if self.expect_uuid_for_id - else compute_nodes[0].id, - 'hypervisor_hostname': compute_nodes[0].hypervisor_hostname, - 'state': 'up', - 'status': 'enabled', - } - self.assertDictEqual(expected, result['hypervisors'][0]) + self.controller.host_api.compute_node_get_all.return_value = ( + compute_nodes) + self.controller.host_api.service_get_by_compute_host = ( + fake_service_get_by_compute_host) - _test(self) + req = self._get_request(True) + result = self.controller.index(req) + self.assertEqual(1, len(result['hypervisors'])) + expected = { + 'id': compute_nodes[0].uuid if self.expect_uuid_for_id + else compute_nodes[0].id, + 'hypervisor_hostname': compute_nodes[0].hypervisor_hostname, + 'state': 'up', + 'status': 'enabled', + } + self.assertDictEqual(expected, result['hypervisors'][0]) def test_detail(self): req = self._get_request(True) @@ -444,32 +439,30 @@ def fake_service_get_by_compute_host(context, host): return TEST_SERVICES[0] raise exception.ComputeHostNotFound(host=host) - @mock.patch.object(self.controller.host_api, 'compute_node_get_all', - return_value=compute_nodes) - @mock.patch.object(self.controller.host_api, - 'service_get_by_compute_host', - fake_service_get_by_compute_host) - def _test(self, compute_node_get_all): - req = self._get_request(True) - result = self.controller.detail(req) - self.assertEqual(1, len(result['hypervisors'])) - expected = { - 'id': compute_nodes[0].id, - 'hypervisor_hostname': compute_nodes[0].hypervisor_hostname, - 'state': 'up', - 'status': 'enabled', - } - # we don't care about all of the details, just make sure we get - # the subset we care about and there are more keys than what index - # would return - hypervisor = result['hypervisors'][0] - self.assertTrue( - set(expected.keys()).issubset(set(hypervisor.keys()))) - self.assertGreater(len(hypervisor.keys()), len(expected.keys())) - self.assertEqual(compute_nodes[0].hypervisor_hostname, - hypervisor['hypervisor_hostname']) - - _test(self) + m_get = self.controller.host_api.compute_node_get_all + m_get.side_effect = None + m_get.return_value = compute_nodes + self.controller.host_api.service_get_by_compute_host.side_effect = ( + fake_service_get_by_compute_host) + + req = self._get_request(True) + result = self.controller.detail(req) + self.assertEqual(1, len(result['hypervisors'])) + expected = { + 'id': compute_nodes[0].id, + 'hypervisor_hostname': compute_nodes[0].hypervisor_hostname, + 'state': 'up', + 'status': 'enabled', + } + # we don't care about all of the details, just make sure we get + # the subset we care about and there are more keys than what index + # would return + hypervisor = result['hypervisors'][0] + self.assertTrue( + set(expected.keys()).issubset(set(hypervisor.keys()))) + self.assertGreater(len(hypervisor.keys()), len(expected.keys())) + self.assertEqual(compute_nodes[0].hypervisor_hostname, + hypervisor['hypervisor_hostname']) def test_detail_compute_host_not_mapped(self): """Tests that if a service is deleted but the compute node is not we @@ -487,32 +480,28 @@ def fake_service_get_by_compute_host(context, host): return TEST_SERVICES[0] raise exception.HostMappingNotFound(name=host) - @mock.patch.object(self.controller.host_api, 'compute_node_get_all', - return_value=compute_nodes) - @mock.patch.object(self.controller.host_api, - 'service_get_by_compute_host', - fake_service_get_by_compute_host) - def _test(self, compute_node_get_all): - req = self._get_request(True) - result = self.controller.detail(req) - self.assertEqual(1, len(result['hypervisors'])) - expected = { - 'id': compute_nodes[0].id, - 'hypervisor_hostname': compute_nodes[0].hypervisor_hostname, - 'state': 'up', - 'status': 'enabled', - } - # we don't care about all of the details, just make sure we get - # the subset we care about and there are more keys than what index - # would return - hypervisor = result['hypervisors'][0] - self.assertTrue( - set(expected.keys()).issubset(set(hypervisor.keys()))) - self.assertGreater(len(hypervisor.keys()), len(expected.keys())) - self.assertEqual(compute_nodes[0].hypervisor_hostname, - hypervisor['hypervisor_hostname']) - - _test(self) + self.controller.host_api.service_get_by_compute_host.side_effect = ( + fake_service_get_by_compute_host) + self.controller.host_api.compute_node_get_all.return_value = ( + compute_nodes) + req = self._get_request(True) + result = self.controller.detail(req) + self.assertEqual(1, len(result['hypervisors'])) + expected = { + 'id': compute_nodes[0].id, + 'hypervisor_hostname': compute_nodes[0].hypervisor_hostname, + 'state': 'up', + 'status': 'enabled', + } + # we don't care about all of the details, just make sure we get + # the subset we care about and there are more keys than what index + # would return + hypervisor = result['hypervisors'][0] + self.assertTrue( + set(expected.keys()).issubset(set(hypervisor.keys()))) + self.assertGreater(len(hypervisor.keys()), len(expected.keys())) + self.assertEqual(compute_nodes[0].hypervisor_hostname, + hypervisor['hypervisor_hostname']) def test_show(self): req = self._get_request(True) @@ -525,21 +514,16 @@ def test_show_compute_host_not_mapped(self): """Tests that if a service is deleted but the compute node is not we don't fail when listing hypervisors. """ - - @mock.patch.object(self.controller.host_api, 'compute_node_get', - return_value=self.TEST_HYPERS_OBJ[0]) - @mock.patch.object(self.controller.host_api, - 'service_get_by_compute_host') - def _test(self, mock_service, mock_compute_node_get): - req = self._get_request(True) - mock_service.side_effect = exception.HostMappingNotFound( - name='foo') - hyper_id = self._get_hyper_id() - self.assertRaises(exc.HTTPNotFound, self.controller.show, - req, hyper_id) - self.assertTrue(mock_service.called) - mock_compute_node_get.assert_called_once_with(mock.ANY, hyper_id) - _test(self) + self.controller.host_api.service_get_by_compute_host.side_effect = ( + exception.HostMappingNotFound(name='foo')) + req = self._get_request(True) + hyper_id = self._get_hyper_id() + self.assertRaises( + exc.HTTPNotFound, self.controller.show, req, hyper_id) + self.assertTrue( + self.controller.host_api.service_get_by_compute_host.called) + self.controller.host_api.compute_node_get.assert_called_once_with( + mock.ANY, hyper_id) def test_show_noid(self): req = self._get_request(True) @@ -611,20 +595,15 @@ def test_uptime_hypervisor_down(self): mock.ANY, self.TEST_HYPERS_OBJ[0].host) def test_uptime_hypervisor_not_mapped_service_get(self): - @mock.patch.object(self.controller.host_api, 'compute_node_get') - @mock.patch.object(self.controller.host_api, 'get_host_uptime') - @mock.patch.object(self.controller.host_api, - 'service_get_by_compute_host', - side_effect=exception.HostMappingNotFound( - name='dummy')) - def _test(mock_get, _, __): - req = self._get_request(True) - hyper_id = self._get_hyper_id() - self.assertRaises(exc.HTTPNotFound, - self.controller.uptime, req, hyper_id) - self.assertTrue(mock_get.called) + self.controller.host_api.service_get_by_compute_host.side_effect = ( + exception.HostMappingNotFound(name='dummy')) - _test() + req = self._get_request(True) + hyper_id = self._get_hyper_id() + self.assertRaises(exc.HTTPNotFound, + self.controller.uptime, req, hyper_id) + self.assertTrue( + self.controller.host_api.service_get_by_compute_host.called) def test_uptime_hypervisor_not_mapped(self): with mock.patch.object(self.controller.host_api, 'get_host_uptime', @@ -644,30 +623,26 @@ def test_search(self): self.assertEqual(dict(hypervisors=self.INDEX_HYPER_DICTS), result) def test_search_non_exist(self): - with mock.patch.object(self.controller.host_api, - 'compute_node_search_by_hypervisor', - return_value=[]) as mock_node_search: - req = self._get_request(True) - self.assertRaises(exc.HTTPNotFound, self.controller.search, - req, 'a') - self.assertEqual(1, mock_node_search.call_count) + m_search = self.controller.host_api.compute_node_search_by_hypervisor + m_search.side_effect = None + m_search.return_value = [] + + req = self._get_request(True) + self.assertRaises(exc.HTTPNotFound, self.controller.search, req, 'a') + self.assertEqual(1, m_search.call_count) def test_search_unmapped(self): + m_search = self.controller.host_api.compute_node_search_by_hypervisor + m_search.side_effect = None + m_search.return_value = [mock.MagicMock()] - @mock.patch.object(self.controller.host_api, - 'compute_node_search_by_hypervisor') - @mock.patch.object(self.controller.host_api, - 'service_get_by_compute_host') - def _test(mock_service, mock_search): - mock_search.return_value = [mock.MagicMock()] - mock_service.side_effect = exception.HostMappingNotFound( - name='foo') - req = self._get_request(True) - self.assertRaises(exc.HTTPNotFound, self.controller.search, - req, 'a') - self.assertTrue(mock_service.called) + self.controller.host_api.service_get_by_compute_host.side_effect = ( + exception.HostMappingNotFound(name='foo')) - _test() + req = self._get_request(True) + self.assertRaises(exc.HTTPNotFound, self.controller.search, req, 'a') + self.assertTrue( + self.controller.host_api.service_get_by_compute_host.called) @mock.patch.object(objects.InstanceList, 'get_by_host', side_effect=fake_instance_get_all_by_host) @@ -702,15 +677,12 @@ def test_servers_not_mapped(self): def test_servers_compute_host_not_found(self): req = self._get_request(True) - with test.nested( - mock.patch.object( - self.controller.host_api, 'instance_get_all_by_host', - side_effect=fake_instance_get_all_by_host, - ), - mock.patch.object( - self.controller.host_api, 'service_get_by_compute_host', - side_effect=exception.ComputeHostNotFound(host='foo'), - ), + self.controller.host_api.service_get_by_compute_host.side_effect = ( + exception.ComputeHostNotFound(host='foo')) + with mock.patch.object( + self.controller.host_api, + 'instance_get_all_by_host', + side_effect=fake_instance_get_all_by_host, ): # The result should be empty since every attempt to fetch the # service for a hypervisor "failed" @@ -718,24 +690,25 @@ def test_servers_compute_host_not_found(self): self.assertEqual({'hypervisors': []}, result) def test_servers_non_id(self): - with mock.patch.object(self.controller.host_api, - 'compute_node_search_by_hypervisor', - return_value=[]) as mock_node_search: - req = self._get_request(True) - self.assertRaises(exc.HTTPNotFound, - self.controller.servers, - req, '115') - self.assertEqual(1, mock_node_search.call_count) + m_search = self.controller.host_api.compute_node_search_by_hypervisor + m_search.side_effect = None + m_search.return_value = [] + + req = self._get_request(True) + self.assertRaises(exc.HTTPNotFound, + self.controller.servers, + req, '115') + self.assertEqual(1, m_search.call_count) def test_servers_with_non_integer_hypervisor_id(self): - with mock.patch.object(self.controller.host_api, - 'compute_node_search_by_hypervisor', - return_value=[]) as mock_node_search: + m_search = self.controller.host_api.compute_node_search_by_hypervisor + m_search.side_effect = None + m_search.return_value = [] - req = self._get_request(True) - self.assertRaises(exc.HTTPNotFound, - self.controller.servers, req, 'abc') - self.assertEqual(1, mock_node_search.call_count) + req = self._get_request(True) + self.assertRaises( + exc.HTTPNotFound, self.controller.servers, req, 'abc') + self.assertEqual(1, m_search.call_count) def test_servers_with_no_servers(self): with mock.patch.object(self.controller.host_api, @@ -1089,15 +1062,13 @@ def test_index_with_servers_compute_host_not_found(self): use_admin_context=True, url='/os-hypervisors?with_servers=1') - with test.nested( - mock.patch.object( - self.controller.host_api, 'instance_get_all_by_host', - side_effect=fake_instance_get_all_by_host, - ), - mock.patch.object( - self.controller.host_api, 'service_get_by_compute_host', - side_effect=exception.ComputeHostNotFound(host='foo'), - ), + self.controller.host_api.service_get_by_compute_host.side_effect = ( + exception.ComputeHostNotFound(host='foo')) + + with mock.patch.object( + self.controller.host_api, + "instance_get_all_by_host", + side_effect=fake_instance_get_all_by_host, ): # The result should be empty since every attempt to fetch the # service for a hypervisor "failed" @@ -1157,11 +1128,13 @@ def test_index_with_hostname_pattern_no_match(self): use_admin_context=True, url='/os-hypervisors?with_servers=yes&' 'hypervisor_hostname_pattern=shenzhen') - with mock.patch.object(self.controller.host_api, - 'compute_node_search_by_hypervisor', - return_value=objects.ComputeNodeList()) as s: - self.assertRaises(exc.HTTPNotFound, self.controller.index, req) - s.assert_called_once_with(req.environ['nova.context'], 'shenzhen') + m_search = self.controller.host_api.compute_node_search_by_hypervisor + m_search.side_effect = None + m_search.return_value = objects.ComputeNodeList() + + self.assertRaises(exc.HTTPNotFound, self.controller.index, req) + m_search.assert_called_once_with( + req.environ['nova.context'], 'shenzhen') def test_detail_with_hostname_pattern(self): """Test listing hypervisors with details and using the @@ -1170,13 +1143,14 @@ def test_detail_with_hostname_pattern(self): req = self._get_request( use_admin_context=True, url='/os-hypervisors?hypervisor_hostname_pattern=shenzhen') - with mock.patch.object( - self.controller.host_api, - 'compute_node_search_by_hypervisor', - return_value=objects.ComputeNodeList(objects=[TEST_HYPERS_OBJ[0]]) - ) as s: - result = self.controller.detail(req) - s.assert_called_once_with(req.environ['nova.context'], 'shenzhen') + m_search = self.controller.host_api.compute_node_search_by_hypervisor + m_search.side_effect = None + m_search.return_value = objects.ComputeNodeList( + objects=[TEST_HYPERS_OBJ[0]]) + + result = self.controller.detail(req) + m_search.assert_called_once_with( + req.environ['nova.context'], 'shenzhen') expected = {'hypervisors': [self.DETAIL_HYPERS_DICTS[0]]} @@ -1483,15 +1457,11 @@ def test_uptime(self): self.controller.uptime, req) def test_uptime_old_version(self): - with mock.patch.object( - self.controller.host_api, 'get_host_uptime', - return_value='fake uptime', - ): - req = self._get_request(use_admin_context=True, version='2.87') - hyper_id = self._get_hyper_id() + req = self._get_request(use_admin_context=True, version='2.87') + hyper_id = self._get_hyper_id() - # no exception == pass - self.controller.uptime(req, hyper_id) + # no exception == pass + self.controller.uptime(req, hyper_id) def test_uptime_noid(self): # the separate 'uptime' API has been removed, so skip this test @@ -1526,34 +1496,36 @@ def test_uptime_hypervisor_not_mapped(self): pass def test_show_with_uptime_notimplemented(self): - with mock.patch.object( - self.controller.host_api, 'get_host_uptime', - side_effect=NotImplementedError, - ) as mock_get_uptime: - req = self._get_request(use_admin_context=True) - hyper_id = self._get_hyper_id() + self.controller.host_api.get_host_uptime.side_effect = ( + NotImplementedError()) - result = self.controller.show(req, hyper_id) + req = self._get_request(use_admin_context=True) + hyper_id = self._get_hyper_id() - expected_dict = copy.deepcopy(self.DETAIL_HYPERS_DICTS[0]) - expected_dict.update({'uptime': None}) - self.assertEqual({'hypervisor': expected_dict}, result) - self.assertEqual(1, mock_get_uptime.call_count) + result = self.controller.show(req, hyper_id) + + expected_dict = copy.deepcopy(self.DETAIL_HYPERS_DICTS[0]) + expected_dict.update({'uptime': None}) + self.assertEqual({'hypervisor': expected_dict}, result) + self.assertEqual( + 1, self.controller.host_api.get_host_uptime.call_count) def test_show_with_uptime_hypervisor_down(self): - with mock.patch.object( - self.controller.host_api, 'get_host_uptime', - side_effect=exception.ComputeServiceUnavailable(host='dummy') - ) as mock_get_uptime: - req = self._get_request(use_admin_context=True) - hyper_id = self._get_hyper_id() + self.controller.host_api.get_host_uptime.side_effect = ( + exception.ComputeServiceUnavailable(host='dummy')) - result = self.controller.show(req, hyper_id) + req = self._get_request(use_admin_context=True) + hyper_id = self._get_hyper_id() - expected_dict = copy.deepcopy(self.DETAIL_HYPERS_DICTS[0]) - expected_dict.update({'uptime': None}) - self.assertEqual({'hypervisor': expected_dict}, result) - self.assertEqual(1, mock_get_uptime.call_count) + result = self.controller.show(req, hyper_id) + + expected_dict = copy.deepcopy(self.DETAIL_HYPERS_DICTS[0]) + expected_dict.update({'uptime': None}) + self.assertEqual({'hypervisor': expected_dict}, result) + self.assertEqual( + 1, + self.controller.host_api.get_host_uptime.call_count + ) def test_show_old_version(self): # ensure things still work as expected here diff --git a/nova/tests/unit/api/openstack/compute/test_limits.py b/nova/tests/unit/api/openstack/compute/test_limits.py index a5ac0bca24c..69676e28acf 100644 --- a/nova/tests/unit/api/openstack/compute/test_limits.py +++ b/nova/tests/unit/api/openstack/compute/test_limits.py @@ -34,7 +34,6 @@ from nova.limit import placement as placement_limit from nova import objects from nova.policies import limits as l_policies -from nova import quota from nova import test from nova.tests.unit.api.openstack import fakes from nova.tests.unit import matchers @@ -52,12 +51,12 @@ def stub_get_project_quotas(context, project_id, usages=True): return {k: dict(limit=v, in_use=v // 2) for k, v in self.absolute_limits.items()} - mock_get_project_quotas = mock.patch.object( + patcher_get_project_quotas = mock.patch.object( nova.quota.QUOTAS, "get_project_quotas", - side_effect = stub_get_project_quotas) - mock_get_project_quotas.start() - self.addCleanup(mock_get_project_quotas.stop) + side_effect=stub_get_project_quotas) + self.mock_get_project_quotas = patcher_get_project_quotas.start() + self.addCleanup(patcher_get_project_quotas.stop) patcher = self.mock_can = mock.patch('nova.context.RequestContext.can') self.mock_can = patcher.start() self.addCleanup(patcher.stop) @@ -154,16 +153,14 @@ def _get_project_quotas(context, project_id, usages=True): return {k: dict(limit=v, in_use=v // 2) for k, v in self.absolute_limits.items()} - with mock.patch('nova.quota.QUOTAS.get_project_quotas') as \ - get_project_quotas: - get_project_quotas.side_effect = _get_project_quotas + self.mock_get_project_quotas.side_effect = _get_project_quotas - response = request.get_response(self.controller) + response = request.get_response(self.controller) - body = jsonutils.loads(response.body) - self.assertEqual(expected, body) - get_project_quotas.assert_called_once_with(context, tenant_id, - usages=True) + body = jsonutils.loads(response.body) + self.assertEqual(expected, body) + self.mock_get_project_quotas.assert_called_once_with( + context, tenant_id, usages=True) def _do_test_used_limits(self, reserved): request = self._get_index_request(tenant_id=None) @@ -186,8 +183,7 @@ def _do_test_used_limits(self, reserved): def stub_get_project_quotas(context, project_id, usages=True): return limits - self.stub_out('nova.quota.QUOTAS.get_project_quotas', - stub_get_project_quotas) + self.mock_get_project_quotas.side_effect = stub_get_project_quotas res = request.get_response(self.controller) body = jsonutils.loads(res.body) @@ -211,14 +207,15 @@ def test_admin_can_fetch_limits_for_a_given_tenant_id(self): user_id=user_id, project_id=project_id) context = fake_req.environ["nova.context"] - with mock.patch.object(quota.QUOTAS, 'get_project_quotas', - return_value={}) as mock_get_quotas: - fake_req.get_response(self.controller) - self.assertEqual(2, self.mock_can.call_count) - self.mock_can.assert_called_with( - l_policies.OTHER_PROJECT_LIMIT_POLICY_NAME) - mock_get_quotas.assert_called_once_with(context, - tenant_id, usages=True) + self.mock_get_project_quotas.side_effect = None + self.mock_get_project_quotas.return_value = {} + + fake_req.get_response(self.controller) + self.assertEqual(2, self.mock_can.call_count) + self.mock_can.assert_called_with( + l_policies.OTHER_PROJECT_LIMIT_POLICY_NAME) + self.mock_get_project_quotas.assert_called_once_with(context, + tenant_id, usages=True) def _test_admin_can_fetch_used_limits_for_own_project(self, req_get): project_id = "123456" @@ -230,11 +227,12 @@ def _test_admin_can_fetch_used_limits_for_own_project(self, req_get): project_id=project_id) context = fake_req.environ["nova.context"] - with mock.patch.object(quota.QUOTAS, 'get_project_quotas', - return_value={}) as mock_get_quotas: - fake_req.get_response(self.controller) - mock_get_quotas.assert_called_once_with(context, - project_id, usages=True) + self.mock_get_project_quotas.side_effect = None + self.mock_get_project_quotas.return_value = {} + + fake_req.get_response(self.controller) + self.mock_get_project_quotas.assert_called_once_with( + context, project_id, usages=True) def test_admin_can_fetch_used_limits_for_own_project(self): req_get = {} @@ -262,12 +260,13 @@ def test_used_limits_fetched_for_context_project_id(self): project_id = "123456" fake_req = self._get_index_request(project_id=project_id) context = fake_req.environ["nova.context"] - with mock.patch.object(quota.QUOTAS, 'get_project_quotas', - return_value={}) as mock_get_quotas: - fake_req.get_response(self.controller) + self.mock_get_project_quotas.side_effect = None + self.mock_get_project_quotas.return_value = {} - mock_get_quotas.assert_called_once_with(context, - project_id, usages=True) + fake_req.get_response(self.controller) + + self.mock_get_project_quotas.assert_called_once_with( + context, project_id, usages=True) def test_used_ram_added(self): fake_req = self._get_index_request() @@ -275,28 +274,26 @@ def test_used_ram_added(self): def stub_get_project_quotas(context, project_id, usages=True): return {'ram': {'limit': 512, 'in_use': 256}} - with mock.patch.object(quota.QUOTAS, 'get_project_quotas', - side_effect=stub_get_project_quotas - ) as mock_get_quotas: + self.mock_get_project_quotas.side_effect = stub_get_project_quotas - res = fake_req.get_response(self.controller) - body = jsonutils.loads(res.body) - abs_limits = body['limits']['absolute'] - self.assertIn('totalRAMUsed', abs_limits) - self.assertEqual(256, abs_limits['totalRAMUsed']) - self.assertEqual(1, mock_get_quotas.call_count) + res = fake_req.get_response(self.controller) + body = jsonutils.loads(res.body) + abs_limits = body['limits']['absolute'] + self.assertIn('totalRAMUsed', abs_limits) + self.assertEqual(256, abs_limits['totalRAMUsed']) + self.assertEqual(1, self.mock_get_project_quotas.call_count) def test_no_ram_quota(self): fake_req = self._get_index_request() - with mock.patch.object(quota.QUOTAS, 'get_project_quotas', - return_value={}) as mock_get_quotas: + self.mock_get_project_quotas.side_effect = None + self.mock_get_project_quotas.return_value = {} - res = fake_req.get_response(self.controller) - body = jsonutils.loads(res.body) - abs_limits = body['limits']['absolute'] - self.assertNotIn('totalRAMUsed', abs_limits) - self.assertEqual(1, mock_get_quotas.call_count) + res = fake_req.get_response(self.controller) + body = jsonutils.loads(res.body) + abs_limits = body['limits']['absolute'] + self.assertNotIn('totalRAMUsed', abs_limits) + self.assertEqual(1, self.mock_get_project_quotas.call_count) class FakeHttplibSocket(object): @@ -398,25 +395,24 @@ def _get_project_quotas(context, project_id, usages=True): return {k: dict(limit=v, in_use=v // 2) for k, v in absolute_limits.items()} - with mock.patch('nova.quota.QUOTAS.get_project_quotas') as \ - get_project_quotas: - get_project_quotas.side_effect = _get_project_quotas - response = self.controller.index(self.req) - expected_response = { - "limits": { - "rate": [], - "absolute": { - "maxTotalRAMSize": 512, - "maxTotalInstances": 5, - "maxTotalCores": 21, - "maxTotalKeypairs": 10, - "totalRAMUsed": 256, - "totalCoresUsed": 10, - "totalInstancesUsed": 2, - }, + self.mock_get_project_quotas.side_effect = _get_project_quotas + + response = self.controller.index(self.req) + expected_response = { + "limits": { + "rate": [], + "absolute": { + "maxTotalRAMSize": 512, + "maxTotalInstances": 5, + "maxTotalCores": 21, + "maxTotalKeypairs": 10, + "totalRAMUsed": 256, + "totalCoresUsed": 10, + "totalInstancesUsed": 2, }, - } - self.assertEqual(expected_response, response) + }, + } + self.assertEqual(expected_response, response) class LimitsControllerTestV239(BaseLimitTestSuite): @@ -436,21 +432,20 @@ def _get_project_quotas(context, project_id, usages=True): return {k: dict(limit=v, in_use=v // 2) for k, v in absolute_limits.items()} - with mock.patch('nova.quota.QUOTAS.get_project_quotas') as \ - get_project_quotas: - get_project_quotas.side_effect = _get_project_quotas - response = self.controller.index(self.req) - # staring from version 2.39 there is no 'maxImageMeta' field - # in response after removing 'image-metadata' proxy API - expected_response = { - "limits": { - "rate": [], - "absolute": { - "maxServerMeta": 1, - }, + self.mock_get_project_quotas.side_effect = _get_project_quotas + + response = self.controller.index(self.req) + # starting from version 2.39 there is no 'maxImageMeta' field + # in response after removing 'image-metadata' proxy API + expected_response = { + "limits": { + "rate": [], + "absolute": { + "maxServerMeta": 1, }, - } - self.assertEqual(expected_response, response) + }, + } + self.assertEqual(expected_response, response) class LimitsControllerTestV275(BaseLimitTestSuite): @@ -469,10 +464,9 @@ def _get_project_quotas(context, project_id, usages=True): return {k: dict(limit=v, in_use=v // 2) for k, v in absolute_limits.items()} - with mock.patch('nova.quota.QUOTAS.get_project_quotas') as \ - get_project_quotas: - get_project_quotas.side_effect = _get_project_quotas - self.controller.index(req) + self.mock_get_project_quotas.side_effect = _get_project_quotas + self.controller.index(req) + self.controller.index(req) def test_index_additional_query_param(self): req = fakes.HTTPRequest.blank("/?unkown=fake", diff --git a/nova/tests/unit/api/openstack/compute/test_migrate_server.py b/nova/tests/unit/api/openstack/compute/test_migrate_server.py index 683759eccc5..325b4927b30 100644 --- a/nova/tests/unit/api/openstack/compute/test_migrate_server.py +++ b/nova/tests/unit/api/openstack/compute/test_migrate_server.py @@ -530,9 +530,8 @@ def _test_migrate_validation_error(self, body): self.req, fakes.FAKE_UUID, body=body) def _test_migrate_exception(self, exc_info, expected_result): - @mock.patch.object(self.compute_api, 'get') @mock.patch.object(self.compute_api, 'resize', side_effect=exc_info) - def _test(mock_resize, mock_get): + def _test(mock_resize): instance = objects.Instance(uuid=uuids.instance) self.assertRaises(expected_result, self.controller._migrate, diff --git a/nova/tests/unit/api/openstack/compute/test_quotas.py b/nova/tests/unit/api/openstack/compute/test_quotas.py index 6cb8d9c7adb..7e4f9d13747 100644 --- a/nova/tests/unit/api/openstack/compute/test_quotas.py +++ b/nova/tests/unit/api/openstack/compute/test_quotas.py @@ -882,7 +882,8 @@ def setUp(self): local_limit.KEY_PAIRS: 100, local_limit.SERVER_GROUPS: 12, local_limit.SERVER_GROUP_MEMBERS: 10} - self.useFixture(limit_fixture.LimitFixture(reglimits, {})) + self.limit_fixture = self.useFixture( + limit_fixture.LimitFixture(reglimits, {})) @mock.patch.object(placement_limit, "get_legacy_project_limits") def test_show_v21(self, mock_proj): @@ -1098,7 +1099,7 @@ def test_defaults_v21_different_limit_values(self): local_limit.KEY_PAIRS: 1, local_limit.SERVER_GROUPS: 3, local_limit.SERVER_GROUP_MEMBERS: 2} - self.useFixture(limit_fixture.LimitFixture(reglimits, {})) + self.limit_fixture.reglimits = reglimits req = fakes.HTTPRequest.blank("") response = self.controller.defaults(req, uuids.project_id) diff --git a/nova/tests/unit/api/openstack/compute/test_remote_consoles.py b/nova/tests/unit/api/openstack/compute/test_remote_consoles.py index 6427b1abf03..f62093bbb79 100644 --- a/nova/tests/unit/api/openstack/compute/test_remote_consoles.py +++ b/nova/tests/unit/api/openstack/compute/test_remote_consoles.py @@ -103,6 +103,18 @@ def test_get_vnc_console_no_instance_on_console_get(self): 'get_vnc_console', exception.InstanceNotFound(instance_id=fakes.FAKE_UUID)) + def test_get_vnc_console_instance_invalid_state(self): + body = {'os-getVNCConsole': {'type': 'novnc'}} + self._check_console_failure( + self.controller.get_vnc_console, + webob.exc.HTTPConflict, + body, + 'get_vnc_console', + exception.InstanceInvalidState( + attr='fake-attr', state='fake-state', method='fake-method', + instance_uuid=fakes.FAKE_UUID) + ) + def test_get_vnc_console_invalid_type(self): body = {'os-getVNCConsole': {'type': 'invalid'}} self._check_console_failure( diff --git a/nova/tests/unit/api/openstack/compute/test_server_actions.py b/nova/tests/unit/api/openstack/compute/test_server_actions.py index d07924abe84..b4daad1286a 100644 --- a/nova/tests/unit/api/openstack/compute/test_server_actions.py +++ b/nova/tests/unit/api/openstack/compute/test_server_actions.py @@ -66,11 +66,11 @@ def setUp(self): self.controller = self._get_controller() self.compute_api = self.controller.compute_api - # We don't care about anything getting as far as hitting the compute - # RPC API so we just mock it out here. - mock_rpcapi = mock.patch.object(self.compute_api, 'compute_rpcapi') - mock_rpcapi.start() - self.addCleanup(mock_rpcapi.stop) + # In most of the cases we don't care about anything getting as far as + # hitting the compute RPC API so we just mock it out here. + patcher_rpcapi = mock.patch.object(self.compute_api, 'compute_rpcapi') + self.mock_rpcapi = patcher_rpcapi.start() + self.addCleanup(patcher_rpcapi.stop) # The project_id here matches what is used by default in # fake_compute_get which need to match for policy checks. self.req = fakes.HTTPRequest.blank('', @@ -1079,21 +1079,23 @@ def fake_block_device_mapping_get_all_by_instance(context, inst_id, snapshot = dict(id=_fake_id('d')) + self.mock_rpcapi.quiesce_instance.side_effect = ( + exception.InstanceQuiesceNotSupported( + instance_id="fake", reason="test" + ) + ) + with test.nested( mock.patch.object( self.controller.compute_api.volume_api, 'get_absolute_limits', return_value={'totalSnapshotsUsed': 0, 'maxTotalSnapshots': 10}), - mock.patch.object(self.controller.compute_api.compute_rpcapi, - 'quiesce_instance', - side_effect=exception.InstanceQuiesceNotSupported( - instance_id='fake', reason='test')), mock.patch.object(self.controller.compute_api.volume_api, 'get', return_value=volume), mock.patch.object(self.controller.compute_api.volume_api, 'create_snapshot_force', return_value=snapshot), - ) as (mock_get_limits, mock_quiesce, mock_vol_get, mock_vol_create): + ) as (mock_get_limits, mock_vol_get, mock_vol_create): if mock_vol_create_side_effect: mock_vol_create.side_effect = mock_vol_create_side_effect @@ -1125,7 +1127,7 @@ def fake_block_device_mapping_get_all_by_instance(context, inst_id, for k in extra_properties.keys(): self.assertEqual(properties[k], extra_properties[k]) - mock_quiesce.assert_called_once_with(mock.ANY, mock.ANY) + self.mock_rpcapi.quiesce_instance.assert_called_once() mock_vol_get.assert_called_once_with(mock.ANY, volume['id']) mock_vol_create.assert_called_once_with(mock.ANY, volume['id'], mock.ANY, mock.ANY) @@ -1189,21 +1191,23 @@ def fake_block_device_mapping_get_all_by_instance(context, inst_id, snapshot = dict(id=_fake_id('d')) + self.mock_rpcapi.quiesce_instance.side_effect = ( + exception.InstanceQuiesceNotSupported( + instance_id="fake", reason="test" + ) + ) + with test.nested( mock.patch.object( self.controller.compute_api.volume_api, 'get_absolute_limits', return_value={'totalSnapshotsUsed': 0, 'maxTotalSnapshots': 10}), - mock.patch.object(self.controller.compute_api.compute_rpcapi, - 'quiesce_instance', - side_effect=exception.InstanceQuiesceNotSupported( - instance_id='fake', reason='test')), mock.patch.object(self.controller.compute_api.volume_api, 'get', return_value=volume), mock.patch.object(self.controller.compute_api.volume_api, 'create_snapshot_force', return_value=snapshot), - ) as (mock_get_limits, mock_quiesce, mock_vol_get, mock_vol_create): + ) as (mock_get_limits, mock_vol_get, mock_vol_create): response = self.controller._action_create_image(self.req, FAKE_UUID, body=body) @@ -1218,7 +1222,7 @@ def fake_block_device_mapping_get_all_by_instance(context, inst_id, for key, val in extra_metadata.items(): self.assertEqual(properties[key], val) - mock_quiesce.assert_called_once_with(mock.ANY, mock.ANY) + self.mock_rpcapi.quiesce_instance.assert_called_once() mock_vol_get.assert_called_once_with(mock.ANY, volume['id']) mock_vol_create.assert_called_once_with(mock.ANY, volume['id'], mock.ANY, mock.ANY) diff --git a/nova/tests/unit/api/openstack/compute/test_server_group_quotas.py b/nova/tests/unit/api/openstack/compute/test_server_group_quotas.py index a0404baffcf..81d1939e716 100644 --- a/nova/tests/unit/api/openstack/compute/test_server_group_quotas.py +++ b/nova/tests/unit/api/openstack/compute/test_server_group_quotas.py @@ -209,7 +209,8 @@ def setUp(self): self.flags(driver='nova.quota.UnifiedLimitsDriver', group='quota') self.req = fakes.HTTPRequest.blank('') self.controller = sg_v21.ServerGroupController() - self.useFixture(limit_fixture.LimitFixture({'server_groups': 10}, {})) + self.limit_fixture = self.useFixture( + limit_fixture.LimitFixture({'server_groups': 10}, {})) @mock.patch('nova.limit.local.enforce_db_limit') def test_create_server_group_during_recheck(self, mock_enforce): @@ -236,7 +237,7 @@ def test_create_server_group_recheck_disabled(self, mock_enforce): delta=1) def test_create_group_fails_with_zero_quota(self): - self.useFixture(limit_fixture.LimitFixture({'server_groups': 0}, {})) + self.limit_fixture.reglimits = {'server_groups': 0} sgroup = {'name': 'test', 'policies': ['anti-affinity']} exc = self.assertRaises(webob.exc.HTTPForbidden, self.controller.create, @@ -245,7 +246,7 @@ def test_create_group_fails_with_zero_quota(self): self.assertIn(msg, str(exc)) def test_create_only_one_group_when_limit_is_one(self): - self.useFixture(limit_fixture.LimitFixture({'server_groups': 1}, {})) + self.limit_fixture.reglimits = {'server_groups': 1} policies = ['anti-affinity'] sgroup = {'name': 'test', 'policies': policies} res_dict = self.controller.create( diff --git a/nova/tests/unit/api/openstack/compute/test_servers.py b/nova/tests/unit/api/openstack/compute/test_servers.py index 31739ed7ab2..4e2a694e15f 100644 --- a/nova/tests/unit/api/openstack/compute/test_servers.py +++ b/nova/tests/unit/api/openstack/compute/test_servers.py @@ -2087,10 +2087,10 @@ def _get_server_data_dict(self, uuid, image_bookmark, flavor_bookmark, return server_dict - @mock.patch('nova.compute.api.API.get_instance_host_status') - def _verify_host_status_policy_behavior(self, func, mock_get_host_status): + def _verify_host_status_policy_behavior(self, func): # Set policy to disallow both host_status cases and verify we don't # call the get_instance_host_status compute RPC API. + self.mock_get_instance_host_status.reset_mock() rules = { 'os_compute_api:servers:show:host_status': '!', 'os_compute_api:servers:show:host_status:unknown-only': '!', @@ -2098,7 +2098,7 @@ def _verify_host_status_policy_behavior(self, func, mock_get_host_status): orig_rules = policy.get_rules() policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=False) func() - mock_get_host_status.assert_not_called() + self.mock_get_instance_host_status.assert_not_called() # Restore the original rules. policy.set_rules(orig_rules) @@ -2638,15 +2638,13 @@ class ServersControllerTestV275(ControllerTest): microversion = '2.75' - @mock.patch('nova.compute.api.API.get_all') - def test_get_servers_additional_query_param_old_version(self, mock_get): + def test_get_servers_additional_query_param_old_version(self): req = fakes.HTTPRequest.blank(self.path_with_query % 'unknown=1', use_admin_context=True, version='2.74') self.controller.index(req) - @mock.patch('nova.compute.api.API.get_all') - def test_get_servers_ignore_sort_key_old_version(self, mock_get): + def test_get_servers_ignore_sort_key_old_version(self): req = fakes.HTTPRequest.blank( self.path_with_query % 'sort_key=deleted', use_admin_context=True, version='2.74') @@ -3584,13 +3582,13 @@ def setUp(self): }, } - @mock.patch('nova.compute.api.API.get') - def _rebuild_server(self, mock_get, certs=None, - conf_enabled=True, conf_certs=None): + def _rebuild_server(self, certs=None, conf_enabled=True, conf_certs=None): ctx = self.req.environ['nova.context'] - mock_get.return_value = fakes.stub_instance_obj(ctx, - vm_state=vm_states.ACTIVE, trusted_certs=certs, - project_id=self.req_project_id, user_id=self.req_user_id) + self.mock_get.side_effect = None + self.mock_get.return_value = fakes.stub_instance_obj( + ctx, vm_state=vm_states.ACTIVE, trusted_certs=certs, + project_id=self.req_project_id, user_id=self.req_user_id + ) self.flags(default_trusted_certificate_ids=conf_certs, group='glance') @@ -3743,10 +3741,10 @@ def setUp(self): } } - @mock.patch('nova.compute.api.API.get') - def _rebuild_server(self, mock_get): + def _rebuild_server(self): ctx = self.req.environ['nova.context'] - mock_get.return_value = fakes.stub_instance_obj(ctx, + self.mock_get.side_effect = None + self.mock_get.return_value = fakes.stub_instance_obj(ctx, vm_state=vm_states.ACTIVE, project_id=self.req_project_id, user_id=self.req_user_id) server = self.controller._action_rebuild( diff --git a/nova/tests/unit/api/openstack/compute/test_volumes.py b/nova/tests/unit/api/openstack/compute/test_volumes.py index a24c104c933..14d27d85460 100644 --- a/nova/tests/unit/api/openstack/compute/test_volumes.py +++ b/nova/tests/unit/api/openstack/compute/test_volumes.py @@ -1889,8 +1889,7 @@ def test_assisted_delete_missing_delete_info(self): req, '5') def _test_assisted_delete_instance_conflict(self, api_error): - # unset the stub on volume_snapshot_delete from setUp - self.mock_volume_snapshot_delete.stop() + self.mock_volume_snapshot_delete.side_effect = api_error params = { 'delete_info': jsonutils.dumps({'volume_id': '1'}), } @@ -1899,10 +1898,9 @@ def _test_assisted_delete_instance_conflict(self, api_error): urllib.parse.urlencode(params), version=self.microversion) req.method = 'DELETE' - with mock.patch.object(compute_api.API, 'volume_snapshot_delete', - side_effect=api_error): - self.assertRaises( - webob.exc.HTTPBadRequest, self.controller.delete, req, '5') + + self.assertRaises( + webob.exc.HTTPBadRequest, self.controller.delete, req, '5') def test_assisted_delete_instance_invalid_state(self): api_error = exception.InstanceInvalidState( diff --git a/nova/tests/unit/cmd/test_manage.py b/nova/tests/unit/cmd/test_manage.py index 82c3d3c84ad..3775a0b2c96 100644 --- a/nova/tests/unit/cmd/test_manage.py +++ b/nova/tests/unit/cmd/test_manage.py @@ -4052,6 +4052,8 @@ def test_show_image_properties_unknown_failure( image_property='hw_disk_bus') self.assertEqual(1, ret, 'return code') + @mock.patch('nova.objects.RequestSpec.save') + @mock.patch('nova.objects.RequestSpec.get_by_instance_uuid') @mock.patch('nova.objects.Instance.get_by_uuid') @mock.patch('nova.context.target_cell') @mock.patch('nova.objects.Instance.save') @@ -4060,7 +4062,8 @@ def test_show_image_properties_unknown_failure( @mock.patch('nova.context.get_admin_context', new=mock.Mock(return_value=mock.sentinel.ctxt)) def test_set_image_properties( - self, mock_instance_save, mock_target_cell, mock_get_instance + self, mock_instance_save, mock_target_cell, mock_get_instance, + mock_get_request_spec, mock_request_spec_save ): mock_target_cell.return_value.__enter__.return_value = \ mock.sentinel.cctxt @@ -4069,9 +4072,11 @@ def test_set_image_properties( vm_state=obj_fields.InstanceState.STOPPED, system_metadata={ 'image_hw_disk_bus': 'virtio', - } + }, + image_ref='' ) mock_get_instance.return_value = instance + mock_get_request_spec.return_value = objects.RequestSpec() ret = self.commands.set( instance_uuid=uuidsentinel.instance, image_properties=['hw_cdrom_bus=sata'] @@ -4088,7 +4093,12 @@ def test_set_image_properties( instance.system_metadata.get('image_hw_disk_bus'), 'image_hw_disk_bus' ) + image_props = mock_get_request_spec.return_value.image.properties + self.assertEqual('sata', image_props.get('hw_cdrom_bus')) + self.assertEqual('virtio', image_props.get('hw_disk_bus')) + mock_instance_save.assert_called_once() + mock_request_spec_save.assert_called_once() @mock.patch('nova.objects.Instance.get_by_uuid') @mock.patch('nova.objects.InstanceMapping.get_by_instance_uuid', diff --git a/nova/tests/unit/cmd/test_status.py b/nova/tests/unit/cmd/test_status.py index ba85590697e..2d33c890b77 100644 --- a/nova/tests/unit/cmd/test_status.py +++ b/nova/tests/unit/cmd/test_status.py @@ -502,3 +502,19 @@ def test_instances_not_found_without_hw_machine_type(self): upgradecheck.Code.SUCCESS, result.code ) + + +class TestUpgradeCheckServiceUserToken(test.NoDBTestCase): + + def setUp(self): + super().setUp() + self.cmd = status.UpgradeCommands() + + def test_service_user_token_not_configured(self): + result = self.cmd._check_service_user_token() + self.assertEqual(upgradecheck.Code.FAILURE, result.code) + + def test_service_user_token_configured(self): + self.flags(send_service_user_token=True, group='service_user') + result = self.cmd._check_service_user_token() + self.assertEqual(upgradecheck.Code.SUCCESS, result.code) diff --git a/nova/tests/unit/compute/test_api.py b/nova/tests/unit/compute/test_api.py index 9e85ef633d3..390dece66d3 100644 --- a/nova/tests/unit/compute/test_api.py +++ b/nova/tests/unit/compute/test_api.py @@ -967,6 +967,31 @@ def _set_delete_shelved_part(self, inst, mock_image_delete): return snapshot_id + def _test_delete(self, delete_type, **attrs): + delete_time = datetime.datetime( + 1955, 11, 5, 9, 30, tzinfo=iso8601.UTC) + timeutils.set_time_override(delete_time) + self.addCleanup(timeutils.clear_time_override) + + with test.nested( + mock.patch.object( + self.compute_api.compute_rpcapi, 'confirm_resize'), + mock.patch.object( + self.compute_api.compute_rpcapi, 'terminate_instance'), + mock.patch.object( + self.compute_api.compute_rpcapi, 'soft_delete_instance'), + ) as ( + mock_confirm, mock_terminate, mock_soft_delete + ): + self._do_delete( + delete_type, + mock_confirm, + mock_terminate, + mock_soft_delete, + delete_time, + **attrs + ) + @mock.patch.object(compute_utils, 'notify_about_instance_action') @mock.patch.object(objects.Migration, 'get_by_instance_and_status') @@ -986,12 +1011,13 @@ def _set_delete_shelved_part(self, inst, mock_image_delete): @mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid', return_value=[]) @mock.patch.object(objects.Instance, 'save') - def _test_delete(self, delete_type, mock_save, mock_bdm_get, mock_elevated, - mock_get_cn, mock_up, mock_record, mock_inst_update, - mock_deallocate, mock_inst_meta, mock_inst_destroy, - mock_notify_legacy, mock_get_inst, - mock_save_im, mock_image_delete, mock_mig_get, - mock_notify, **attrs): + def _do_delete( + self, delete_type, mock_confirm, mock_terminate, mock_soft_delete, + delete_time, mock_save, mock_bdm_get, mock_elevated, mock_get_cn, + mock_up, mock_record, mock_inst_update, mock_deallocate, + mock_inst_meta, mock_inst_destroy, mock_notify_legacy, mock_get_inst, + mock_save_im, mock_image_delete, mock_mig_get, mock_notify, **attrs + ): expected_save_calls = [mock.call()] expected_record_calls = [] expected_elevated_calls = [] @@ -1001,17 +1027,11 @@ def _test_delete(self, delete_type, mock_save, mock_bdm_get, mock_elevated, deltas = {'instances': -1, 'cores': -inst.flavor.vcpus, 'ram': -inst.flavor.memory_mb} - delete_time = datetime.datetime(1955, 11, 5, 9, 30, - tzinfo=iso8601.UTC) - self.useFixture(utils_fixture.TimeFixture(delete_time)) task_state = (delete_type == 'soft_delete' and task_states.SOFT_DELETING or task_states.DELETING) updates = {'progress': 0, 'task_state': task_state} if delete_type == 'soft_delete': updates['deleted_at'] = delete_time - rpcapi = self.compute_api.compute_rpcapi - mock_confirm = self.useFixture( - fixtures.MockPatchObject(rpcapi, 'confirm_resize')).mock def _reset_task_state(context, instance, migration, src_host, cast=False): @@ -1026,11 +1046,6 @@ def _reset_task_state(context, instance, migration, src_host, snapshot_id = self._set_delete_shelved_part(inst, mock_image_delete) - mock_terminate = self.useFixture( - fixtures.MockPatchObject(rpcapi, 'terminate_instance')).mock - mock_soft_delete = self.useFixture( - fixtures.MockPatchObject(rpcapi, 'soft_delete_instance')).mock - if inst.task_state == task_states.RESIZE_FINISH: self._test_delete_resizing_part(inst, deltas) @@ -2073,7 +2088,8 @@ def _check_state(expected_task_state=None): filter_properties = {'ignore_hosts': [fake_inst['host']]} if request_spec: - fake_spec = objects.RequestSpec() + fake_spec = objects.RequestSpec( + pci_requests=objects.InstancePCIRequests(requests=[])) if requested_destination: cell1 = objects.CellMapping(uuid=uuids.cell1, name='cell1') fake_spec.requested_destination = objects.Destination( @@ -2636,9 +2652,6 @@ def test_pause(self, mock_save, mock_record): rpcapi = self.compute_api.compute_rpcapi - mock_pause = self.useFixture( - fixtures.MockPatchObject(rpcapi, 'pause_instance')).mock - with mock.patch.object(rpcapi, 'pause_instance') as mock_pause: self.compute_api.pause(self.context, instance) @@ -5623,7 +5636,10 @@ def test_rescue_bfv_with_required_trait(self, mock_get_bdms, destination_type='volume', volume_type=None, snapshot_id=None, volume_id=uuids.volume_id, volume_size=None)]) - rescue_image_meta_obj = image_meta_obj.ImageMeta.from_dict({}) + rescue_image_meta_obj = image_meta_obj.ImageMeta.from_dict({ + 'properties': {'hw_rescue_device': 'disk', + 'hw_rescue_bus': 'scsi'} + }) with test.nested( mock.patch.object(self.compute_api.placementclient, @@ -5675,6 +5691,7 @@ def test_rescue_bfv_with_required_trait(self, mock_get_bdms, # Assert that the instance task state as set in the compute API self.assertEqual(task_states.RESCUING, instance.task_state) + @mock.patch('nova.objects.instance.Instance.image_meta') @mock.patch('nova.objects.compute_node.ComputeNode' '.get_by_host_and_nodename') @mock.patch('nova.compute.utils.is_volume_backed_instance', @@ -5683,7 +5700,8 @@ def test_rescue_bfv_with_required_trait(self, mock_get_bdms, '.get_by_instance_uuid') def test_rescue_bfv_without_required_trait(self, mock_get_bdms, mock_is_volume_backed, - mock_get_cn): + mock_get_cn, + mock_image_meta): instance = self._create_instance_obj() bdms = objects.BlockDeviceMappingList(objects=[ objects.BlockDeviceMapping( @@ -5691,6 +5709,12 @@ def test_rescue_bfv_without_required_trait(self, mock_get_bdms, destination_type='volume', volume_type=None, snapshot_id=None, volume_id=uuids.volume_id, volume_size=None)]) + + instance.image_meta = image_meta_obj.ImageMeta.from_dict({ + 'properties': {'hw_rescue_device': 'disk', + 'hw_rescue_bus': 'scsi'} + }) + with test.nested( mock.patch.object(self.compute_api.placementclient, 'get_provider_traits'), @@ -5728,6 +5752,124 @@ def test_rescue_bfv_without_required_trait(self, mock_get_bdms, mock_get_traits.assert_called_once_with( self.context, uuids.cn) + @mock.patch('nova.objects.image_meta.ImageMeta.from_image_ref') + @mock.patch('nova.objects.compute_node.ComputeNode' + '.get_by_host_and_nodename') + @mock.patch('nova.compute.utils.is_volume_backed_instance', + return_value=True) + @mock.patch('nova.objects.block_device.BlockDeviceMappingList' + '.get_by_instance_uuid') + def test_rescue_bfv_with_required_image_properties( + self, mock_get_bdms, mock_is_volume_backed, mock_get_cn, + mock_image_meta_obj_from_ref): + instance = self._create_instance_obj() + bdms = objects.BlockDeviceMappingList(objects=[ + objects.BlockDeviceMapping( + boot_index=0, image_id=uuids.image_id, source_type='image', + destination_type='volume', volume_type=None, + snapshot_id=None, volume_id=uuids.volume_id, + volume_size=None)]) + rescue_image_meta_obj = image_meta_obj.ImageMeta.from_dict({ + 'properties': {'hw_rescue_device': 'disk', + 'hw_rescue_bus': 'scsi'} + }) + + with test.nested( + mock.patch.object(self.compute_api.placementclient, + 'get_provider_traits'), + mock.patch.object(self.compute_api.volume_api, 'get'), + mock.patch.object(self.compute_api.volume_api, 'check_attached'), + mock.patch.object(instance, 'save'), + mock.patch.object(self.compute_api, '_record_action_start'), + mock.patch.object(self.compute_api.compute_rpcapi, + 'rescue_instance') + ) as ( + mock_get_traits, mock_get_volume, mock_check_attached, + mock_instance_save, mock_record_start, mock_rpcapi_rescue + ): + # Mock out the returned compute node, image_meta, bdms and volume + mock_image_meta_obj_from_ref.return_value = rescue_image_meta_obj + mock_get_bdms.return_value = bdms + mock_get_volume.return_value = mock.sentinel.volume + mock_get_cn.return_value = mock.Mock(uuid=uuids.cn) + + # Ensure the required trait is returned, allowing BFV rescue + mock_trait_info = mock.Mock(traits=[ot.COMPUTE_RESCUE_BFV]) + mock_get_traits.return_value = mock_trait_info + + # Try to rescue the instance + self.compute_api.rescue(self.context, instance, + rescue_image_ref=uuids.rescue_image_id, + allow_bfv_rescue=True) + + # Assert all of the calls made in the compute API + mock_get_bdms.assert_called_once_with(self.context, instance.uuid) + mock_get_volume.assert_called_once_with( + self.context, uuids.volume_id) + mock_check_attached.assert_called_once_with( + self.context, mock.sentinel.volume) + mock_is_volume_backed.assert_called_once_with( + self.context, instance, bdms) + mock_get_cn.assert_called_once_with( + self.context, instance.host, instance.node) + mock_get_traits.assert_called_once_with(self.context, uuids.cn) + mock_instance_save.assert_called_once_with( + expected_task_state=[None]) + mock_record_start.assert_called_once_with( + self.context, instance, instance_actions.RESCUE) + mock_rpcapi_rescue.assert_called_once_with( + self.context, instance=instance, rescue_password=None, + rescue_image_ref=uuids.rescue_image_id, clean_shutdown=True) + + # Assert that the instance task state as set in the compute API + self.assertEqual(task_states.RESCUING, instance.task_state) + + @mock.patch('nova.objects.image_meta.ImageMeta.from_image_ref') + @mock.patch('nova.compute.utils.is_volume_backed_instance', + return_value=True) + @mock.patch('nova.objects.block_device.BlockDeviceMappingList' + '.get_by_instance_uuid') + def test_rescue_bfv_without_required_image_properties( + self, mock_get_bdms, mock_is_volume_backed, + mock_image_meta_obj_from_ref): + instance = self._create_instance_obj() + bdms = objects.BlockDeviceMappingList(objects=[ + objects.BlockDeviceMapping( + boot_index=0, image_id=uuids.image_id, source_type='image', + destination_type='volume', volume_type=None, + snapshot_id=None, volume_id=uuids.volume_id, + volume_size=None)]) + rescue_image_meta_obj = image_meta_obj.ImageMeta.from_dict({ + 'properties': {} + }) + + with test.nested( + mock.patch.object(self.compute_api.volume_api, 'get'), + mock.patch.object(self.compute_api.volume_api, 'check_attached'), + ) as ( + mock_get_volume, mock_check_attached + ): + # Mock out the returned bdms, volume and image_meta + mock_get_bdms.return_value = bdms + mock_get_volume.return_value = mock.sentinel.volume + mock_image_meta_obj_from_ref.return_value = rescue_image_meta_obj + + # Assert that any attempt to rescue a bfv instance on a compute + # node that does not report the COMPUTE_RESCUE_BFV trait fails and + # raises InstanceNotRescuable + self.assertRaises(exception.InstanceNotRescuable, + self.compute_api.rescue, self.context, instance, + rescue_image_ref=None, allow_bfv_rescue=True) + + # Assert the calls made in the compute API prior to the failure + mock_get_bdms.assert_called_once_with(self.context, instance.uuid) + mock_get_volume.assert_called_once_with( + self.context, uuids.volume_id) + mock_check_attached.assert_called_once_with( + self.context, mock.sentinel.volume) + mock_is_volume_backed.assert_called_once_with( + self.context, instance, bdms) + @mock.patch('nova.compute.utils.is_volume_backed_instance', return_value=True) @mock.patch('nova.objects.block_device.BlockDeviceMappingList' @@ -7740,16 +7882,13 @@ def test_compute_api_host(self): self.assertTrue(hasattr(self.compute_api, 'host')) self.assertEqual(CONF.host, self.compute_api.host) - @mock.patch('nova.scheduler.client.report.SchedulerReportClient') + @mock.patch('nova.scheduler.client.report.report_client_singleton') def test_placement_client_init(self, mock_report_client): """Tests to make sure that the construction of the placement client - only happens once per API class instance. + uses the singleton helper, and happens only when needed. """ - self.assertIsNone(self.compute_api._placementclient) - # Access the property twice to make sure SchedulerReportClient is - # only loaded once. - for x in range(2): - self.compute_api.placementclient + self.assertFalse(mock_report_client.called) + self.compute_api.placementclient mock_report_client.assert_called_once_with() def test_validate_host_for_cold_migrate_same_host_fails(self): diff --git a/nova/tests/unit/compute/test_compute.py b/nova/tests/unit/compute/test_compute.py index d8f443843f3..f2ea9c3c009 100644 --- a/nova/tests/unit/compute/test_compute.py +++ b/nova/tests/unit/compute/test_compute.py @@ -5714,13 +5714,15 @@ def _test_resize_with_pci(self, method, expected_pci_addr): objects=[objects.PciDevice(vendor_id='1377', product_id='0047', address='0000:0a:00.1', - request_id=uuids.req1)]) + request_id=uuids.req1, + compute_node_id=1)]) new_pci_devices = objects.PciDeviceList( objects=[objects.PciDevice(vendor_id='1377', product_id='0047', address='0000:0b:00.1', - request_id=uuids.req2)]) + request_id=uuids.req2, + compute_node_id=2)]) if expected_pci_addr == old_pci_devices[0].address: expected_pci_device = old_pci_devices[0] @@ -8618,16 +8620,13 @@ def test_create_instance_defaults_display_name(self): def test_create_instance_sets_system_metadata(self): # Make sure image properties are copied into system metadata. - with mock.patch.object( - self.compute_api.compute_task_api, 'schedule_and_build_instances', - ) as mock_sbi: - ref, resv_id = self.compute_api.create( - self.context, - flavor=self.default_flavor, - image_href='f5000000-0000-0000-0000-000000000000') + ref, resv_id = self.compute_api.create( + self.context, + flavor=self.default_flavor, + image_href='f5000000-0000-0000-0000-000000000000') - build_call = mock_sbi.call_args_list[0] - instance = build_call[1]['build_requests'][0].instance + build_call = self.schedule_and_build_instances_mock.call_args_list[0] + instance = build_call[1]['build_requests'][0].instance image_props = {'image_kernel_id': uuids.kernel_id, 'image_ramdisk_id': uuids.ramdisk_id, @@ -8637,16 +8636,14 @@ def test_create_instance_sets_system_metadata(self): self.assertEqual(value, instance.system_metadata[key]) def test_create_saves_flavor(self): - with mock.patch.object( - self.compute_api.compute_task_api, 'schedule_and_build_instances', - ) as mock_sbi: - ref, resv_id = self.compute_api.create( - self.context, - flavor=self.default_flavor, - image_href=uuids.image_href_id) + ref, resv_id = self.compute_api.create( + self.context, + flavor=self.default_flavor, + image_href=uuids.image_href_id) + + build_call = self.schedule_and_build_instances_mock.call_args_list[0] + instance = build_call[1]['build_requests'][0].instance - build_call = mock_sbi.call_args_list[0] - instance = build_call[1]['build_requests'][0].instance self.assertIn('flavor', instance) self.assertEqual(self.default_flavor.flavorid, instance.flavor.flavorid) @@ -8654,19 +8651,18 @@ def test_create_saves_flavor(self): def test_create_instance_associates_security_groups(self): # Make sure create associates security groups. - with test.nested( - mock.patch.object(self.compute_api.compute_task_api, - 'schedule_and_build_instances'), - mock.patch('nova.network.security_group_api.validate_name', - return_value=uuids.secgroup_id), - ) as (mock_sbi, mock_secgroups): + with mock.patch( + "nova.network.security_group_api.validate_name", + return_value=uuids.secgroup_id, + ) as mock_secgroups: self.compute_api.create( self.context, flavor=self.default_flavor, image_href=uuids.image_href_id, security_groups=['testgroup']) - build_call = mock_sbi.call_args_list[0] + build_call = ( + self.schedule_and_build_instances_mock.call_args_list[0]) reqspec = build_call[1]['request_spec'][0] self.assertEqual(1, len(reqspec.security_groups)) @@ -8701,22 +8697,19 @@ def test_create_instance_associates_requested_networks(self): requested_networks = objects.NetworkRequestList( objects=[objects.NetworkRequest(port_id=uuids.port_instance)]) - with test.nested( - mock.patch.object( - self.compute_api.compute_task_api, - 'schedule_and_build_instances'), - mock.patch.object( - self.compute_api.network_api, - 'create_resource_requests', - return_value=(None, [], objects.RequestLevelParams())), - ) as (mock_sbi, _mock_create_resreqs): + with mock.patch.object( + self.compute_api.network_api, + "create_resource_requests", + return_value=(None, [], objects.RequestLevelParams()), + ): self.compute_api.create( self.context, flavor=self.default_flavor, image_href=uuids.image_href_id, requested_networks=requested_networks) - build_call = mock_sbi.call_args_list[0] + build_call = ( + self.schedule_and_build_instances_mock.call_args_list[0]) reqspec = build_call[1]['request_spec'][0] self.assertEqual(1, len(reqspec.requested_networks)) @@ -10216,8 +10209,7 @@ def test_console_output_no_host(self): self.compute_api.get_console_output, self.context, instance) - @mock.patch.object(compute_utils, 'notify_about_instance_action') - def test_attach_interface(self, mock_notify): + def test_attach_interface(self): instance = self._create_fake_instance_obj() nwinfo = [fake_network_cache_model.new_vif()] network_id = nwinfo[0]['network']['id'] @@ -10237,8 +10229,12 @@ def test_attach_interface(self, mock_notify): mock.patch.object( self.compute, "_claim_pci_device_for_interface_attach", - return_value=None) - ) as (cap, mock_lock, mock_create_resource_req, mock_claim_pci): + return_value=None), + mock.patch.object(compute_utils, 'notify_about_instance_action'), + ) as ( + cap, mock_lock, mock_create_resource_req, mock_claim_pci, + mock_notify + ): mock_create_resource_req.return_value = ( None, [], mock.sentinel.req_lvl_params) vif = self.compute.attach_interface(self.context, @@ -11056,8 +11052,7 @@ def test__allocate_port_resource_for_instance_fails_to_update_pci(self): mock_remove_res.assert_called_once_with( self.context, instance.uuid, mock.sentinel.resources) - @mock.patch.object(compute_utils, 'notify_about_instance_action') - def test_detach_interface(self, mock_notify): + def test_detach_interface(self): nwinfo, port_id = self.test_attach_interface() instance = self._create_fake_instance_obj() instance.info_cache = objects.InstanceInfoCache.new( @@ -11090,10 +11085,13 @@ def test_detach_interface(self, mock_notify): mock.patch('nova.pci.request.get_instance_pci_request_from_vif', return_value=pci_req), mock.patch.object(self.compute.rt, 'unclaim_pci_devices'), - mock.patch.object(instance, 'save') + mock.patch.object(instance, 'save'), + mock.patch.object(compute_utils, 'notify_about_instance_action'), ) as ( - mock_remove_alloc, mock_deallocate, mock_lock, - mock_get_pci_req, mock_unclaim_pci, mock_instance_save): + mock_remove_alloc, mock_deallocate, mock_lock, + mock_get_pci_req, mock_unclaim_pci, mock_instance_save, + mock_notify + ): self.compute.detach_interface(self.context, instance, port_id) mock_deallocate.assert_called_once_with( @@ -11900,17 +11898,16 @@ def fake_rebuild_instance(*args, **kwargs): instance.save() @mock.patch.object(objects.Service, 'get_by_compute_host') - @mock.patch.object(self.compute_api.compute_task_api, - 'rebuild_instance') @mock.patch.object(objects.ComputeNodeList, 'get_all_by_host') @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') @mock.patch.object(self.compute_api.servicegroup_api, 'service_is_up') - def do_test(service_is_up, get_by_instance_uuid, get_all_by_host, - rebuild_instance, get_service): + def do_test( + service_is_up, get_by_instance_uuid, get_all_by_host, get_service + ): service_is_up.return_value = False get_by_instance_uuid.return_value = fake_spec - rebuild_instance.side_effect = fake_rebuild_instance + self.rebuild_instance_mock.side_effect = fake_rebuild_instance get_all_by_host.return_value = objects.ComputeNodeList( objects=[objects.ComputeNode( host='fake_dest_host', @@ -11928,7 +11925,7 @@ def do_test(service_is_up, get_by_instance_uuid, get_all_by_host, host = None else: host = 'fake_dest_host' - rebuild_instance.assert_called_once_with( + self.rebuild_instance_mock.assert_called_once_with( ctxt, instance=instance, new_pass=None, @@ -13046,16 +13043,13 @@ def test_aggregate_list_with_hosts(self, mock_add_host, hosts = aggregate.hosts if 'hosts' in aggregate else None self.assertIn(values[0][1][0], hosts) - @mock.patch('nova.scheduler.client.report.SchedulerReportClient') + @mock.patch('nova.scheduler.client.report.report_client_singleton') def test_placement_client_init(self, mock_report_client): """Tests to make sure that the construction of the placement client - only happens once per AggregateAPI class instance. + uses the singleton helper, and happens only when needed. """ - self.assertIsNone(self.api._placement_client) - # Access the property twice to make sure SchedulerReportClient is - # only loaded once. - for x in range(2): - self.api.placement_client + self.assertFalse(mock_report_client.called) + self.api.placement_client mock_report_client.assert_called_once_with() diff --git a/nova/tests/unit/compute/test_compute_mgr.py b/nova/tests/unit/compute/test_compute_mgr.py index cd1a9369c4a..0d39a570c02 100644 --- a/nova/tests/unit/compute/test_compute_mgr.py +++ b/nova/tests/unit/compute/test_compute_mgr.py @@ -1306,6 +1306,36 @@ def test_init_instance_with_binding_failed_vif_type(self): self.compute._init_instance(self.context, instance) set_error_state.assert_called_once_with(instance) + def test_init_instance_vif_plug_fails_missing_pci(self): + instance = fake_instance.fake_instance_obj( + self.context, + uuid=uuids.instance, + info_cache=None, + power_state=power_state.RUNNING, + vm_state=vm_states.ACTIVE, + task_state=None, + host=self.compute.host, + expected_attrs=['info_cache']) + + with test.nested( + mock.patch.object(context, 'get_admin_context', + return_value=self.context), + mock.patch.object(objects.Instance, 'get_network_info', + return_value=network_model.NetworkInfo()), + mock.patch.object(self.compute.driver, 'plug_vifs', + side_effect=exception.PciDeviceNotFoundById("pci-addr")), + mock.patch("nova.compute.manager.LOG.exception"), + ) as (get_admin_context, get_nw_info, plug_vifs, log_exception): + # as this does not raise, we are sure that the compute service + # continues initializing the rest of the instances + self.compute._init_instance(self.context, instance) + log_exception.assert_called_once_with( + "Virtual interface plugging failed for instance. Probably the " + "vnic_type of the bound port has been changed. Nova does not " + "support such change.", + instance=instance + ) + def _test__validate_pinning_configuration(self, supports_pcpus=True): instance_1 = fake_instance.fake_instance_obj( self.context, uuid=uuids.instance_1) @@ -6516,13 +6546,14 @@ def test_build_and_run_instance_with_unlimited_max_concurrent_builds(self): self.compute = manager.ComputeManager() self._test_build_and_run_instance() + @mock.patch.object(manager.ComputeManager, '_build_succeeded') @mock.patch.object(objects.InstanceActionEvent, 'event_finish_with_failure') @mock.patch.object(objects.InstanceActionEvent, 'event_start') @mock.patch.object(objects.Instance, 'save') @mock.patch.object(manager.ComputeManager, '_build_and_run_instance') def _test_build_and_run_instance(self, mock_build, mock_save, - mock_start, mock_finish): + mock_start, mock_finish, mock_succeeded): self._do_build_instance_update(mock_save) orig_do_build_and_run = self.compute._do_build_and_run_instance @@ -6555,6 +6586,7 @@ def _wrapped_do_build_and_run_instance(*args, **kwargs): self.requested_networks, self.security_groups, self.block_device_mapping, self.node, self.limits, self.filter_properties, {}, self.accel_uuids) + mock_succeeded.assert_called_once_with(self.node) # This test when sending an icehouse compatible rpc call to juno compute # node, NetworkRequest object can load from three items tuple. @@ -6582,6 +6614,7 @@ def test_build_and_run_instance_with_icehouse_requested_network( self.assertEqual('10.0.0.1', str(requested_network.address)) self.assertEqual(uuids.port_instance, requested_network.port_id) + @mock.patch.object(manager.ComputeManager, '_build_failed') @mock.patch.object(objects.InstanceActionEvent, 'event_finish_with_failure') @mock.patch.object(objects.InstanceActionEvent, 'event_start') @@ -6597,7 +6630,7 @@ def test_build_and_run_instance_with_icehouse_requested_network( def test_build_abort_exception(self, mock_build_run, mock_build, mock_set, mock_nil, mock_add, mock_clean_vol, mock_clean_net, mock_save, - mock_start, mock_finish): + mock_start, mock_finish, mock_failed): self._do_build_instance_update(mock_save) mock_build_run.side_effect = exception.BuildAbortException(reason='', instance_uuid=self.instance.uuid) @@ -6640,7 +6673,9 @@ def _wrapped_do_build_and_run_instance(*args, **kwargs): mock.ANY, mock.ANY) mock_nil.assert_called_once_with(self.instance) mock_set.assert_called_once_with(self.instance, clean_task_state=True) + mock_failed.assert_called_once_with(self.node) + @mock.patch.object(manager.ComputeManager, '_build_failed') @mock.patch.object(objects.InstanceActionEvent, 'event_finish_with_failure') @mock.patch.object(objects.InstanceActionEvent, 'event_start') @@ -6651,8 +6686,8 @@ def _wrapped_do_build_and_run_instance(*args, **kwargs): @mock.patch.object(conductor_api.ComputeTaskAPI, 'build_instances') @mock.patch.object(manager.ComputeManager, '_build_and_run_instance') def test_rescheduled_exception(self, mock_build_run, - mock_build, mock_set, mock_nil, - mock_save, mock_start, mock_finish): + mock_build, mock_set, mock_nil, mock_save, + mock_start, mock_finish, mock_failed): self._do_build_instance_update(mock_save, reschedule_update=True) mock_build_run.side_effect = exception.RescheduledException(reason='', instance_uuid=self.instance.uuid) @@ -6699,6 +6734,7 @@ def _wrapped_do_build_and_run_instance(*args, **kwargs): self.admin_pass, self.injected_files, self.requested_networks, self.security_groups, self.block_device_mapping, request_spec={}, host_lists=[fake_host_list]) + mock_failed.assert_called_once_with(self.node) @mock.patch.object(manager.ComputeManager, '_shutdown_instance') @mock.patch.object(manager.ComputeManager, '_build_networks_for_instance') @@ -7052,6 +7088,139 @@ def _wrapped_do_build_and_run_instance(*args, **kwargs): self.security_groups, self.block_device_mapping, request_spec={}, host_lists=[fake_host_list]) + @mock.patch('nova.compute.resource_tracker.ResourceTracker.instance_claim', + new=mock.MagicMock()) + @mock.patch.object(objects.InstanceActionEvent, + 'event_finish_with_failure') + @mock.patch.object(objects.InstanceActionEvent, 'event_start') + @mock.patch.object(objects.Instance, 'save') + @mock.patch.object(manager.ComputeManager, + '_nil_out_instance_obj_host_and_node') + @mock.patch.object(conductor_api.ComputeTaskAPI, 'build_instances') + @mock.patch.object(manager.ComputeManager, '_build_failed') + @mock.patch.object(manager.ComputeManager, '_build_succeeded') + @mock.patch.object(manager.ComputeManager, + '_validate_instance_group_policy') + def test_group_affinity_violation_exception_with_retry( + self, mock_validate_policy, mock_succeeded, mock_failed, mock_build, + mock_nil, mock_save, mock_start, mock_finish, + ): + """Test retry by affinity or anti-affinity validation check doesn't + increase failed build + """ + + self._do_build_instance_update(mock_save, reschedule_update=True) + mock_validate_policy.side_effect = \ + exception.GroupAffinityViolation( + instance_uuid=self.instance.uuid, policy="Affinity") + + orig_do_build_and_run = self.compute._do_build_and_run_instance + + def _wrapped_do_build_and_run_instance(*args, **kwargs): + ret = orig_do_build_and_run(*args, **kwargs) + self.assertEqual(build_results.RESCHEDULED_BY_POLICY, ret) + return ret + + with test.nested( + mock.patch.object( + self.compute, '_do_build_and_run_instance', + side_effect=_wrapped_do_build_and_run_instance, + ), + mock.patch.object( + self.compute.network_api, 'get_instance_nw_info', + ), + ): + self.compute.build_and_run_instance( + self.context, self.instance, + self.image, request_spec={}, + filter_properties=self.filter_properties, + accel_uuids=self.accel_uuids, + injected_files=self.injected_files, + admin_password=self.admin_pass, + requested_networks=self.requested_networks, + security_groups=self.security_groups, + block_device_mapping=self.block_device_mapping, node=self.node, + limits=self.limits, host_list=fake_host_list) + + mock_succeeded.assert_not_called() + mock_failed.assert_not_called() + + self._instance_action_events(mock_start, mock_finish) + self._assert_build_instance_update(mock_save, reschedule_update=True) + mock_nil.assert_called_once_with(self.instance) + mock_build.assert_called_once_with(self.context, + [self.instance], self.image, self.filter_properties, + self.admin_pass, self.injected_files, self.requested_networks, + self.security_groups, self.block_device_mapping, + request_spec={}, host_lists=[fake_host_list]) + + @mock.patch('nova.compute.resource_tracker.ResourceTracker.instance_claim', + new=mock.MagicMock()) + @mock.patch.object(objects.InstanceActionEvent, + 'event_finish_with_failure') + @mock.patch.object(objects.InstanceActionEvent, 'event_start') + @mock.patch.object(objects.Instance, 'save') + @mock.patch.object(manager.ComputeManager, + '_nil_out_instance_obj_host_and_node') + @mock.patch.object(manager.ComputeManager, '_cleanup_allocated_networks') + @mock.patch.object(manager.ComputeManager, '_set_instance_obj_error_state') + @mock.patch.object(compute_utils, 'add_instance_fault_from_exc') + @mock.patch.object(conductor_api.ComputeTaskAPI, 'build_instances') + @mock.patch.object(manager.ComputeManager, '_build_failed') + @mock.patch.object(manager.ComputeManager, '_build_succeeded') + @mock.patch.object(manager.ComputeManager, + '_validate_instance_group_policy') + def test_group_affinity_violation_exception_without_retry( + self, mock_validate_policy, mock_succeeded, mock_failed, mock_build, + mock_add, mock_set_state, mock_clean_net, mock_nil, mock_save, + mock_start, mock_finish, + ): + """Test failure by affinity or anti-affinity validation check doesn't + increase failed build + """ + + self._do_build_instance_update(mock_save) + mock_validate_policy.side_effect = \ + exception.GroupAffinityViolation( + instance_uuid=self.instance.uuid, policy="Affinity") + + orig_do_build_and_run = self.compute._do_build_and_run_instance + + def _wrapped_do_build_and_run_instance(*args, **kwargs): + ret = orig_do_build_and_run(*args, **kwargs) + self.assertEqual(build_results.FAILED_BY_POLICY, ret) + return ret + + with mock.patch.object( + self.compute, '_do_build_and_run_instance', + side_effect=_wrapped_do_build_and_run_instance, + ): + self.compute.build_and_run_instance( + self.context, self.instance, + self.image, request_spec={}, + filter_properties={}, + accel_uuids=[], + injected_files=self.injected_files, + admin_password=self.admin_pass, + requested_networks=self.requested_networks, + security_groups=self.security_groups, + block_device_mapping=self.block_device_mapping, node=self.node, + limits=self.limits, host_list=fake_host_list) + + mock_succeeded.assert_not_called() + mock_failed.assert_not_called() + + self._instance_action_events(mock_start, mock_finish) + self._assert_build_instance_update(mock_save) + mock_clean_net.assert_called_once_with(self.context, self.instance, + self.requested_networks) + mock_add.assert_called_once_with(self.context, self.instance, + mock.ANY, mock.ANY, fault_message=mock.ANY) + mock_nil.assert_called_once_with(self.instance) + mock_build.assert_not_called() + mock_set_state.assert_called_once_with(self.instance, + clean_task_state=True) + @mock.patch.object(objects.InstanceActionEvent, 'event_finish_with_failure') @mock.patch.object(objects.InstanceActionEvent, 'event_start') @@ -7585,6 +7754,27 @@ def test_validate_instance_group_policy_handles_hint_list(self, mock_get): instance, hints) mock_get.assert_called_once_with(self.context, uuids.group_hint) + @mock.patch('nova.objects.InstanceGroup.get_by_hint') + def test_validate_instance_group_policy_deleted_group(self, mock_get): + """Tests that _validate_instance_group_policy handles the case + where the scheduler hint has a group but that group has been deleted. + This tests is a reproducer for bug: #1890244 + """ + instance = objects.Instance(uuid=uuids.instance) + hints = {'group': [uuids.group_hint]} + mock_get.side_effect = exception.InstanceGroupNotFound( + group_uuid=uuids.group_hint + ) + # This implicitly asserts that no exception is raised since + # uncaught exceptions would be treated as a test failure. + self.compute._validate_instance_group_policy( + self.context, instance, hints + ) + # and this just assert that we did in fact invoke the method + # that raises to ensure that if we refactor in the future this + # this test will fail if the function we mock is no longer called. + mock_get.assert_called_once_with(self.context, uuids.group_hint) + @mock.patch('nova.objects.InstanceGroup.get_by_uuid') @mock.patch('nova.objects.InstanceList.get_uuids_by_host') @mock.patch('nova.objects.InstanceGroup.get_by_hint') @@ -7610,7 +7800,7 @@ def test_validate_instance_group_policy_with_rules( nodes.return_value = ['nodename'] migration_list.return_value = [objects.Migration( uuid=uuids.migration, instance_uuid=uuids.instance)] - self.assertRaises(exception.RescheduledException, + self.assertRaises(exception.GroupAffinityViolation, self.compute._validate_instance_group_policy, self.context, instance, hints) @@ -7667,6 +7857,42 @@ def test_failed_bdm_prep_from_delete_raises_unexpected(self, mock_clean, mock_prepspawn.assert_called_once_with(self.instance) mock_failedspawn.assert_called_once_with(self.instance) + @mock.patch.object(virt_driver.ComputeDriver, 'failed_spawn_cleanup') + @mock.patch.object(virt_driver.ComputeDriver, 'prepare_for_spawn') + @mock.patch.object(virt_driver.ComputeDriver, + 'prepare_networks_before_block_device_mapping') + @mock.patch.object(virt_driver.ComputeDriver, + 'clean_networks_preparation') + def test_failed_prepare_for_spawn(self, mock_clean, mock_prepnet, + mock_prepspawn, mock_failedspawn): + mock_prepspawn.side_effect = exception.ComputeResourcesUnavailable( + reason="asdf") + with mock.patch.object(self.compute, + '_build_networks_for_instance', + return_value=self.network_info + ) as _build_networks_for_instance: + + try: + with self.compute._build_resources(self.context, self.instance, + self.requested_networks, self.security_groups, + self.image, self.block_device_mapping, + self.resource_provider_mapping, self.accel_uuids): + pass + except Exception as e: + self.assertIsInstance(e, + exception.ComputeResourcesUnavailable) + + _build_networks_for_instance.assert_has_calls( + [mock.call(self.context, self.instance, + self.requested_networks, self.security_groups, + self.resource_provider_mapping, + self.network_arqs)]) + + mock_prepnet.assert_not_called() + mock_clean.assert_called_once_with(self.instance, self.network_info) + mock_prepspawn.assert_called_once_with(self.instance) + mock_failedspawn.assert_called_once_with(self.instance) + @mock.patch.object(virt_driver.ComputeDriver, 'failed_spawn_cleanup') @mock.patch.object(virt_driver.ComputeDriver, 'prepare_for_spawn') @mock.patch.object(manager.ComputeManager, '_build_networks_for_instance') @@ -8563,11 +8789,9 @@ def _test_revert_resize_instance_destroy_disks(self, is_shared=False): @mock.patch.object(self.compute.network_api, 'setup_networks_on_host') @mock.patch.object(self.compute.network_api, 'migrate_instance_start') @mock.patch.object(compute_utils, 'notify_usage_exists') - @mock.patch.object(self.migration, 'save') @mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid') def do_test(get_by_instance_uuid, - migration_save, notify_usage_exists, migrate_instance_start, setup_networks_on_host, @@ -8639,7 +8863,6 @@ def _get_instance_nw_info(context, instance): @mock.patch.object(self.compute.network_api, 'migrate_instance_finish', side_effect=_migrate_instance_finish) @mock.patch.object(self.compute.network_api, 'setup_networks_on_host') - @mock.patch.object(self.migration, 'save') @mock.patch.object(self.instance, 'save') @mock.patch.object(self.compute, '_set_instance_info') @mock.patch.object(db, 'instance_fault_create') @@ -8653,7 +8876,6 @@ def do_test(notify_about_instance_usage, fault_create, set_instance_info, instance_save, - migration_save, setup_networks_on_host, migrate_instance_finish, get_instance_nw_info, @@ -8697,11 +8919,9 @@ def test_finish_revert_resize_migration_context(self): @mock.patch.object(self.compute.network_api, 'migrate_instance_start') @mock.patch.object(compute_utils, 'notify_usage_exists') @mock.patch.object(db, 'instance_extra_update_by_uuid') - @mock.patch.object(self.migration, 'save') @mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid') def do_revert_resize(mock_get_by_instance_uuid, - mock_migration_save, mock_extra_update, mock_notify_usage_exists, mock_migrate_instance_start, @@ -8748,7 +8968,6 @@ def do_revert_resize(mock_get_by_instance_uuid, @mock.patch.object(compute_utils, 'notify_about_instance_action') @mock.patch.object(self.compute, "_set_instance_info") @mock.patch.object(self.instance, 'save') - @mock.patch.object(self.migration, 'save') @mock.patch.object(compute_utils, 'add_instance_fault_from_exc') @mock.patch.object(db, 'instance_fault_create') @mock.patch.object(db, 'instance_extra_update_by_uuid') @@ -8772,7 +8991,6 @@ def do_finish_revert_resize(mock_attachment_complete, mock_extra_update, mock_fault_create, mock_fault_from_exc, - mock_mig_save, mock_inst_save, mock_set, mock_notify_about_instance_action, @@ -8866,7 +9084,6 @@ def test_confirm_resize_deletes_allocations_and_update_scheduler(self): @mock.patch.object(self.compute, '_delete_scheduler_instance_info') @mock.patch('nova.objects.Instance.get_by_uuid') @mock.patch('nova.objects.Migration.get_by_id') - @mock.patch.object(self.migration, 'save') @mock.patch.object(self.compute, '_notify_about_instance_usage') @mock.patch.object(self.compute, 'network_api') @mock.patch.object(self.compute.driver, 'confirm_migration') @@ -8875,7 +9092,7 @@ def test_confirm_resize_deletes_allocations_and_update_scheduler(self): @mock.patch.object(self.instance, 'save') def do_confirm_resize(mock_save, mock_drop, mock_delete, mock_confirm, mock_nwapi, mock_notify, - mock_mig_save, mock_mig_get, mock_inst_get, + mock_mig_get, mock_inst_get, mock_delete_scheduler_info): self._mock_rt() @@ -8958,16 +9175,16 @@ def test_confirm_resize_driver_confirm_migration_fails( instance_get_by_uuid.assert_called_once() def test_confirm_resize_calls_virt_driver_with_old_pci(self): - @mock.patch.object(self.migration, 'save') @mock.patch.object(self.compute, '_notify_about_instance_usage') @mock.patch.object(self.compute, 'network_api') @mock.patch.object(self.compute.driver, 'confirm_migration') @mock.patch.object(self.compute, '_delete_allocation_after_move') @mock.patch.object(self.instance, 'drop_migration_context') @mock.patch.object(self.instance, 'save') - def do_confirm_resize(mock_save, mock_drop, mock_delete, - mock_confirm, mock_nwapi, mock_notify, - mock_mig_save): + def do_confirm_resize( + mock_save, mock_drop, mock_delete, mock_confirm, mock_nwapi, + mock_notify + ): # Mock virt driver confirm_resize() to save the provided # network_info, we will check it later. updated_nw_info = [] @@ -8983,10 +9200,12 @@ def driver_confirm_resize(*args, **kwargs): self._mock_rt() old_devs = objects.PciDeviceList( objects=[objects.PciDevice( + compute_node_id=1, address='0000:04:00.2', request_id=uuids.pcidev1)]) new_devs = objects.PciDeviceList( objects=[objects.PciDevice( + compute_node_id=2, address='0000:05:00.3', request_id=uuids.pcidev1)]) self.instance.migration_context = objects.MigrationContext( @@ -9539,7 +9758,8 @@ def test_live_migration_wait_vif_plugged_vif_plug_error( self.assertEqual('error', self.migration.status) mock_rollback_live_mig.assert_called_once_with( self.context, self.instance, 'dest-host', - migrate_data=migrate_data, source_bdms=source_bdms) + migrate_data=migrate_data, source_bdms=source_bdms, + pre_live_migration=True) @mock.patch('nova.compute.rpcapi.ComputeAPI.pre_live_migration') @mock.patch('nova.compute.manager.ComputeManager._rollback_live_migration') @@ -9574,7 +9794,8 @@ def test_live_migration_wait_vif_plugged_timeout_error( self.assertEqual('error', self.migration.status) mock_rollback_live_mig.assert_called_once_with( self.context, self.instance, 'dest-host', - migrate_data=migrate_data, source_bdms=source_bdms) + migrate_data=migrate_data, source_bdms=source_bdms, + pre_live_migration=True) @mock.patch('nova.compute.rpcapi.ComputeAPI.pre_live_migration') @mock.patch('nova.compute.manager.ComputeManager._rollback_live_migration') @@ -9956,6 +10177,27 @@ def test_post_live_migration_new_allocations(self): self.instance, migration) + def test_post_live_migration_update_host(self): + @mock.patch.object(self.compute, '_get_compute_info') + def _test_post_live_migration(_get_compute_info): + dest_host = 'dest' + cn = objects.ComputeNode(hypervisor_hostname=dest_host) + _get_compute_info.return_value = cn + instance = fake_instance.fake_instance_obj(self.context, + node='src', + uuid=uuids.instance) + with mock.patch.object(self.compute, "_post_live_migration" + ) as plm, mock.patch.object(instance, "save") as save: + error = ValueError("some failure") + plm.side_effect = error + self.assertRaises( + ValueError, self.compute._post_live_migration_update_host, + self.context, instance, dest_host) + save.assert_called_once() + self.assertEqual(instance.host, dest_host) + + _test_post_live_migration() + def test_post_live_migration_cinder_pre_344_api(self): # Because live migration has # succeeded,_post_live_migration_remove_source_vol_connections() @@ -10955,40 +11197,94 @@ def get_pci_req_side_effect(context, instance, vif): _test() def test__update_migrate_vifs_profile_with_pci(self): - # Define two migrate vifs with only one pci that is required - # to be updated. Make sure method under test updated the correct one + # Define three migrate vifs with two pci devs that are required + # to be updated, one VF and on PF. + # Make sure method under test updated the correct devs with the correct + # values. nw_vifs = network_model.NetworkInfo( - [network_model.VIF( - id=uuids.port0, - vnic_type='direct', - type=network_model.VIF_TYPE_HW_VEB, - profile={'pci_slot': '0000:04:00.3', - 'pci_vendor_info': '15b3:1018', - 'physical_network': 'default'}), - network_model.VIF( - id=uuids.port1, - vnic_type='normal', - type=network_model.VIF_TYPE_OVS, - profile={'some': 'attribute'})]) - pci_dev = objects.PciDevice(request_id=uuids.pci_req, - address='0000:05:00.4', - vendor_id='15b3', - product_id='1018') - port_id_to_pci_dev = {uuids.port0: pci_dev} - mig_vifs = migrate_data_obj.VIFMigrateData.\ - create_skeleton_migrate_vifs(nw_vifs) - self.compute._update_migrate_vifs_profile_with_pci(mig_vifs, - port_id_to_pci_dev) + [ + network_model.VIF( + id=uuids.port0, + vnic_type='direct', + type=network_model.VIF_TYPE_HW_VEB, + profile={ + 'pci_slot': '0000:04:00.3', + 'pci_vendor_info': '15b3:1018', + 'physical_network': 'default', + }, + ), + network_model.VIF( + id=uuids.port1, + vnic_type='normal', + type=network_model.VIF_TYPE_OVS, + profile={'some': 'attribute'}, + ), + network_model.VIF( + id=uuids.port2, + vnic_type='direct-physical', + type=network_model.VIF_TYPE_HOSTDEV, + profile={ + 'pci_slot': '0000:01:00', + 'pci_vendor_info': '8086:154d', + 'physical_network': 'physnet2', + }, + ), + ] + ) + + pci_vf_dev = objects.PciDevice( + request_id=uuids.pci_req, + address='0000:05:00.4', + parent_addr='0000:05:00', + vendor_id='15b3', + product_id='1018', + compute_node_id=13, + dev_type=fields.PciDeviceType.SRIOV_VF, + ) + pci_pf_dev = objects.PciDevice( + request_id=uuids.pci_req2, + address='0000:01:00', + parent_addr='0000:02:00', + vendor_id='8086', + product_id='154d', + compute_node_id=13, + dev_type=fields.PciDeviceType.SRIOV_PF, + extra_info={'mac_address': 'b4:96:91:34:f4:36'}, + ) + port_id_to_pci_dev = { + uuids.port0: pci_vf_dev, + uuids.port2: pci_pf_dev, + } + mig_vifs = ( + migrate_data_obj.VIFMigrateData.create_skeleton_migrate_vifs( + nw_vifs) + ) + + self.compute._update_migrate_vifs_profile_with_pci( + mig_vifs, port_id_to_pci_dev) + # Make sure method under test updated the correct one. - changed_mig_vif = mig_vifs[0] + changed_vf_mig_vif = mig_vifs[0] unchanged_mig_vif = mig_vifs[1] + changed_pf_mig_vif = mig_vifs[2] # Migrate vifs profile was updated with pci_dev.address # for port ID uuids.port0. - self.assertEqual(changed_mig_vif.profile['pci_slot'], - pci_dev.address) + self.assertEqual(changed_vf_mig_vif.profile['pci_slot'], + pci_vf_dev.address) + # MAC is not added as this is a VF + self.assertNotIn('device_mac_address', changed_vf_mig_vif.profile) # Migrate vifs profile was unchanged for port ID uuids.port1. # i.e 'profile' attribute does not exist. self.assertNotIn('profile', unchanged_mig_vif) + # Migrate vifs profile was updated with pci_dev.address + # for port ID uuids.port2. + self.assertEqual(changed_pf_mig_vif.profile['pci_slot'], + pci_pf_dev.address) + # MAC is updated as this is a PF + self.assertEqual( + 'b4:96:91:34:f4:36', + changed_pf_mig_vif.profile['device_mac_address'] + ) def test_get_updated_nw_info_with_pci_mapping(self): old_dev = objects.PciDevice(address='0000:04:00.2') diff --git a/nova/tests/unit/compute/test_resource_tracker.py b/nova/tests/unit/compute/test_resource_tracker.py index 36236d58ded..5aab64e72c4 100644 --- a/nova/tests/unit/compute/test_resource_tracker.py +++ b/nova/tests/unit/compute/test_resource_tracker.py @@ -4069,7 +4069,7 @@ def test_merge_provider_configs_additional_traits_exception(self): expected = ("Provider config 'test_provider_config.yaml' attempts to " "define a trait that is owned by the virt driver or " - "specified via the placment api. Invalid traits '" + + "specified via the placement api. Invalid traits '" + ex_trait + "' must be removed from " "'test_provider_config.yaml'.") @@ -4205,9 +4205,9 @@ def test_clean_compute_node_cache(self, mock_remove): invalid_nodename = "invalid-node" self.rt.compute_nodes[_NODENAME] = self.compute self.rt.compute_nodes[invalid_nodename] = mock.sentinel.compute - with mock.patch.object( - self.rt.reportclient, "invalidate_resource_provider", - ) as mock_invalidate: - self.rt.clean_compute_node_cache([self.compute]) - mock_remove.assert_called_once_with(invalid_nodename) - mock_invalidate.assert_called_once_with(invalid_nodename) + mock_invalidate = self.rt.reportclient.invalidate_resource_provider + + self.rt.clean_compute_node_cache([self.compute]) + + mock_remove.assert_called_once_with(invalid_nodename) + mock_invalidate.assert_called_once_with(invalid_nodename) diff --git a/nova/tests/unit/compute/test_stats.py b/nova/tests/unit/compute/test_stats.py index e713794a19a..b95475f09db 100644 --- a/nova/tests/unit/compute/test_stats.py +++ b/nova/tests/unit/compute/test_stats.py @@ -208,6 +208,22 @@ def test_update_stats_for_instance_offloaded(self): self.assertEqual(0, self.stats.num_os_type("Linux")) self.assertEqual(0, self.stats["num_vm_" + vm_states.BUILDING]) + def test_update_stats_for_instance_being_unshelved(self): + instance = self._create_instance() + self.stats.update_stats_for_instance(instance) + self.assertEqual(1, self.stats.num_instances_for_project("1234")) + + instance["vm_state"] = vm_states.SHELVED_OFFLOADED + instance["task_state"] = task_states.SPAWNING + self.stats.update_stats_for_instance(instance) + + self.assertEqual(1, self.stats.num_instances) + self.assertEqual(1, self.stats.num_instances_for_project(1234)) + self.assertEqual(1, self.stats["num_os_type_Linux"]) + self.assertEqual(1, self.stats["num_vm_%s" % + vm_states.SHELVED_OFFLOADED]) + self.assertEqual(1, self.stats["num_task_%s" % task_states.SPAWNING]) + def test_io_workload(self): vms = [vm_states.ACTIVE, vm_states.BUILDING, vm_states.PAUSED] tasks = [task_states.RESIZE_MIGRATING, task_states.REBUILDING, diff --git a/nova/tests/unit/conductor/tasks/test_live_migrate.py b/nova/tests/unit/conductor/tasks/test_live_migrate.py index cb40c076c82..dd4ee7c3fec 100644 --- a/nova/tests/unit/conductor/tasks/test_live_migrate.py +++ b/nova/tests/unit/conductor/tasks/test_live_migrate.py @@ -345,6 +345,36 @@ def test_check_compatible_fails_with_hypervisor_too_old( mock.call(self.destination)], mock_get_info.call_args_list) + @mock.patch.object(live_migrate.LiveMigrationTask, '_get_compute_info') + def test_skip_hypervisor_version_check_on_lm_raise_ex(self, mock_get_info): + host1 = {'hypervisor_type': 'a', 'hypervisor_version': 7} + host2 = {'hypervisor_type': 'a', 'hypervisor_version': 6} + self.flags(group='workarounds', + skip_hypervisor_version_check_on_lm=False) + mock_get_info.side_effect = [objects.ComputeNode(**host1), + objects.ComputeNode(**host2)] + self.assertRaises(exception.DestinationHypervisorTooOld, + self.task._check_compatible_with_source_hypervisor, + self.destination) + self.assertEqual([mock.call(self.instance_host), + mock.call(self.destination)], + mock_get_info.call_args_list) + + @mock.patch.object(live_migrate.LiveMigrationTask, '_get_compute_info') + def test_skip_hypervisor_version_check_on_lm_do_not_raise_ex( + self, mock_get_info + ): + host1 = {'hypervisor_type': 'a', 'hypervisor_version': 7} + host2 = {'hypervisor_type': 'a', 'hypervisor_version': 6} + self.flags(group='workarounds', + skip_hypervisor_version_check_on_lm=True) + mock_get_info.side_effect = [objects.ComputeNode(**host1), + objects.ComputeNode(**host2)] + self.task._check_compatible_with_source_hypervisor(self.destination) + self.assertEqual([mock.call(self.instance_host), + mock.call(self.destination)], + mock_get_info.call_args_list) + @mock.patch.object(compute_rpcapi.ComputeAPI, 'check_can_live_migrate_destination') def test_check_requested_destination(self, mock_check): diff --git a/nova/tests/unit/conductor/test_conductor.py b/nova/tests/unit/conductor/test_conductor.py index 15aa960aad9..8c954db9a7c 100644 --- a/nova/tests/unit/conductor/test_conductor.py +++ b/nova/tests/unit/conductor/test_conductor.py @@ -17,6 +17,8 @@ import copy +import ddt +from keystoneauth1 import exceptions as ks_exc import mock from oslo_db import exception as db_exc from oslo_limit import exception as limit_exceptions @@ -52,6 +54,7 @@ from nova.objects import fields from nova.objects import request_spec from nova.scheduler.client import query +from nova.scheduler.client import report from nova.scheduler import utils as scheduler_utils from nova import test from nova.tests import fixtures @@ -4869,3 +4872,35 @@ def _test(mock_cache): logtext) self.assertIn('host3\' because it is not up', logtext) self.assertIn('image1 failed 1 times', logtext) + + +@ddt.ddt +class TestConductorTaskManager(test.NoDBTestCase): + def test_placement_client_startup(self): + self.assertIsNone(report.PLACEMENTCLIENT) + conductor_manager.ComputeTaskManager() + self.assertIsNotNone(report.PLACEMENTCLIENT) + + @ddt.data(ks_exc.MissingAuthPlugin, + ks_exc.Unauthorized, + test.TestingException) + def test_placement_client_startup_fatals(self, exc): + self.assertRaises(exc, + self._test_placement_client_startup_exception, exc) + + @ddt.data(ks_exc.EndpointNotFound, + ks_exc.DiscoveryFailure, + ks_exc.RequestTimeout, + ks_exc.GatewayTimeout, + ks_exc.ConnectFailure) + def test_placement_client_startup_non_fatal(self, exc): + self._test_placement_client_startup_exception(exc) + + @mock.patch.object(report, 'LOG') + def _test_placement_client_startup_exception(self, exc, mock_log): + with mock.patch.object(report.SchedulerReportClient, '_create_client', + side_effect=exc): + try: + conductor_manager.ComputeTaskManager() + finally: + mock_log.error.assert_called_once() diff --git a/nova/tests/unit/console/test_websocketproxy.py b/nova/tests/unit/console/test_websocketproxy.py index e05ae520d9a..cd99ad53f0a 100644 --- a/nova/tests/unit/console/test_websocketproxy.py +++ b/nova/tests/unit/console/test_websocketproxy.py @@ -589,12 +589,12 @@ def test_malformed_cookie(self, validate, check_port): self.wh.socket.assert_called_with('node1', 10000, connect=True) self.wh.do_proxy.assert_called_with('') - def test_reject_open_redirect(self): + def test_reject_open_redirect(self, url='//example.com/%2F..'): # This will test the behavior when an attempt is made to cause an open # redirect. It should be rejected. mock_req = mock.MagicMock() mock_req.makefile().readline.side_effect = [ - b'GET //example.com/%2F.. HTTP/1.1\r\n', + f'GET {url} HTTP/1.1\r\n'.encode('utf-8'), b'' ] @@ -619,41 +619,34 @@ def test_reject_open_redirect(self): result = output.readlines() # Verify no redirect happens and instead a 400 Bad Request is returned. - self.assertIn('400 URI must not start with //', result[0].decode()) + # NOTE: As of python 3.10.6 there is a fix for this vulnerability, + # which will cause a 301 Moved Permanently error to be returned + # instead that redirects to a sanitized version of the URL with extra + # leading '/' characters removed. + # See https://github.com/python/cpython/issues/87389 for details. + # We will consider either response to be valid for this test. This will + # also help if and when the above fix gets backported to older versions + # of python. + errmsg = result[0].decode() + expected_nova = '400 URI must not start with //' + expected_cpython = '301 Moved Permanently' + + self.assertTrue(expected_nova in errmsg or expected_cpython in errmsg) + + # If we detect the cpython fix, verify that the redirect location is + # now the same url but with extra leading '/' characters removed. + if expected_cpython in errmsg: + location = result[3].decode() + if location.startswith('Location: '): + location = location[len('Location: '):] + location = location.rstrip('\r\n') + self.assertTrue( + location.startswith('/example.com/%2F..'), + msg='Redirect location is not the expected sanitized URL', + ) def test_reject_open_redirect_3_slashes(self): - # This will test the behavior when an attempt is made to cause an open - # redirect. It should be rejected. - mock_req = mock.MagicMock() - mock_req.makefile().readline.side_effect = [ - b'GET ///example.com/%2F.. HTTP/1.1\r\n', - b'' - ] - - # Collect the response data to verify at the end. The - # SimpleHTTPRequestHandler writes the response data by calling the - # request socket sendall() method. - self.data = b'' - - def fake_sendall(data): - self.data += data - - mock_req.sendall.side_effect = fake_sendall - - client_addr = ('8.8.8.8', 54321) - mock_server = mock.MagicMock() - # This specifies that the server will be able to handle requests other - # than only websockets. - mock_server.only_upgrade = False - - # Constructing a handler will process the mock_req request passed in. - websocketproxy.NovaProxyRequestHandler( - mock_req, client_addr, mock_server) - - # Verify no redirect happens and instead a 400 Bad Request is returned. - self.data = self.data.decode() - self.assertIn('Error code: 400', self.data) - self.assertIn('Message: URI must not start with //', self.data) + self.test_reject_open_redirect(url='///example.com/%2F..') @mock.patch('nova.objects.ConsoleAuthToken.validate') def test_no_compute_rpcapi_with_invalid_token(self, mock_validate): diff --git a/nova/tests/unit/db/main/test_api.py b/nova/tests/unit/db/main/test_api.py index c9a9e83154a..e869d0403c3 100644 --- a/nova/tests/unit/db/main/test_api.py +++ b/nova/tests/unit/db/main/test_api.py @@ -279,33 +279,21 @@ def _test_pick_context_manager_disable_db_access( 'No DB access allowed in ', mock_log.error.call_args[0][0]) - @mock.patch.object(db, 'LOG') - @mock.patch.object(db, 'DISABLE_DB_ACCESS', return_value=True) - def test_pick_context_manager_writer_disable_db_access( - self, mock_DISABLE_DB_ACCESS, mock_log, - ): + def test_pick_context_manager_writer_disable_db_access(self): @db.pick_context_manager_writer def func(context, value): pass self._test_pick_context_manager_disable_db_access(func) - @mock.patch.object(db, 'LOG') - @mock.patch.object(db, 'DISABLE_DB_ACCESS', return_value=True) - def test_pick_context_manager_reader_disable_db_access( - self, mock_DISABLE_DB_ACCESS, mock_log, - ): + def test_pick_context_manager_reader_disable_db_access(self): @db.pick_context_manager_reader def func(context, value): pass self._test_pick_context_manager_disable_db_access(func) - @mock.patch.object(db, 'LOG') - @mock.patch.object(db, 'DISABLE_DB_ACCESS', return_value=True) - def test_pick_context_manager_reader_allow_async_disable_db_access( - self, mock_DISABLE_DB_ACCESS, mock_log, - ): + def test_pick_context_manager_reader_allow_async_disable_db_access(self): @db.pick_context_manager_reader_allow_async def func(context, value): pass diff --git a/nova/tests/unit/image/test_format_inspector.py b/nova/tests/unit/image/test_format_inspector.py new file mode 100644 index 00000000000..f74731f22df --- /dev/null +++ b/nova/tests/unit/image/test_format_inspector.py @@ -0,0 +1,662 @@ +# Copyright 2020 Red Hat, Inc +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import io +import os +import re +import struct +import subprocess +import tempfile +from unittest import mock + +from oslo_utils import units + +from nova.image import format_inspector +from nova import test + + +TEST_IMAGE_PREFIX = 'nova-unittest-formatinspector-' + + +def get_size_from_qemu_img(filename): + output = subprocess.check_output('qemu-img info "%s"' % filename, + shell=True) + for line in output.split(b'\n'): + m = re.search(b'^virtual size: .* .([0-9]+) bytes', line.strip()) + if m: + return int(m.group(1)) + + raise Exception('Could not find virtual size with qemu-img') + + +class TestFormatInspectors(test.NoDBTestCase): + def setUp(self): + super(TestFormatInspectors, self).setUp() + self._created_files = [] + + def tearDown(self): + super(TestFormatInspectors, self).tearDown() + for fn in self._created_files: + try: + os.remove(fn) + except Exception: + pass + + def _create_iso(self, image_size, subformat='9660'): + """Create an ISO file of the given size. + + :param image_size: The size of the image to create in bytes + :param subformat: The subformat to use, if any + """ + + # these tests depend on mkisofs + # being installed and in the path, + # if it is not installed, skip + try: + subprocess.check_output('mkisofs --version', shell=True) + except Exception: + self.skipTest('mkisofs not installed') + + size = image_size // units.Mi + base_cmd = "mkisofs" + if subformat == 'udf': + # depending on the distribution mkisofs may not support udf + # and may be provided by genisoimage instead. As a result we + # need to check if the command supports udf via help + # instead of checking the installed version. + # mkisofs --help outputs to stderr so we need to + # redirect it to stdout to use grep. + try: + subprocess.check_output( + 'mkisofs --help 2>&1 | grep udf', shell=True) + except Exception: + self.skipTest('mkisofs does not support udf format') + base_cmd += " -udf" + prefix = TEST_IMAGE_PREFIX + prefix += '-%s-' % subformat + fn = tempfile.mktemp(prefix=prefix, suffix='.iso') + self._created_files.append(fn) + subprocess.check_output( + 'dd if=/dev/zero of=%s bs=1M count=%i' % (fn, size), + shell=True) + # We need to use different file as input and output as the behavior + # of mkisofs is version dependent if both the input and the output + # are the same and can cause test failures + out_fn = "%s.iso" % fn + subprocess.check_output( + '%s -V "TEST" -o %s %s' % (base_cmd, out_fn, fn), + shell=True) + self._created_files.append(out_fn) + return out_fn + + def _create_img( + self, fmt, size, subformat=None, options=None, + backing_file=None): + """Create an image file of the given format and size. + + :param fmt: The format to create + :param size: The size of the image to create in bytes + :param subformat: The subformat to use, if any + :param options: A dictionary of options to pass to the format + :param backing_file: The backing file to use, if any + """ + + if fmt == 'iso': + return self._create_iso(size, subformat) + + if fmt == 'vhd': + # QEMU calls the vhd format vpc + fmt = 'vpc' + + # these tests depend on qemu-img being installed and in the path, + # if it is not installed, skip. we also need to ensure that the + # format is supported by qemu-img, this can vary depending on the + # distribution so we need to check if the format is supported via + # the help output. + try: + subprocess.check_output( + 'qemu-img --help | grep %s' % fmt, shell=True) + except Exception: + self.skipTest( + 'qemu-img not installed or does not support %s format' % fmt) + + if options is None: + options = {} + opt = '' + prefix = TEST_IMAGE_PREFIX + + if subformat: + options['subformat'] = subformat + prefix += subformat + '-' + + if options: + opt += '-o ' + ','.join('%s=%s' % (k, v) + for k, v in options.items()) + + if backing_file is not None: + opt += ' -b %s -F raw' % backing_file + + fn = tempfile.mktemp(prefix=prefix, + suffix='.%s' % fmt) + self._created_files.append(fn) + subprocess.check_output( + 'qemu-img create -f %s %s %s %i' % (fmt, opt, fn, size), + shell=True) + return fn + + def _create_allocated_vmdk(self, size_mb, subformat=None): + # We need a "big" VMDK file to exercise some parts of the code of the + # format_inspector. A way to create one is to first create an empty + # file, and then to convert it with the -S 0 option. + + if subformat is None: + # Matches qemu-img default, see `qemu-img convert -O vmdk -o help` + subformat = 'monolithicSparse' + + prefix = TEST_IMAGE_PREFIX + prefix += '-%s-' % subformat + fn = tempfile.mktemp(prefix=prefix, suffix='.vmdk') + self._created_files.append(fn) + raw = tempfile.mktemp(prefix=prefix, suffix='.raw') + self._created_files.append(raw) + + # Create a file with pseudo-random data, otherwise it will get + # compressed in the streamOptimized format + subprocess.check_output( + 'dd if=/dev/urandom of=%s bs=1M count=%i' % (raw, size_mb), + shell=True) + + # Convert it to VMDK + subprocess.check_output( + 'qemu-img convert -f raw -O vmdk -o subformat=%s -S 0 %s %s' % ( + subformat, raw, fn), + shell=True) + return fn + + def _test_format_at_block_size(self, format_name, img, block_size): + fmt = format_inspector.get_inspector(format_name)() + self.assertIsNotNone(fmt, + 'Did not get format inspector for %s' % ( + format_name)) + wrapper = format_inspector.InfoWrapper(open(img, 'rb'), fmt) + + while True: + chunk = wrapper.read(block_size) + if not chunk: + break + + wrapper.close() + return fmt + + def _test_format_at_image_size(self, format_name, image_size, + subformat=None): + """Test the format inspector for the given format at the + given image size. + + :param format_name: The format to test + :param image_size: The size of the image to create in bytes + :param subformat: The subformat to use, if any + """ + img = self._create_img(format_name, image_size, subformat=subformat) + + # Some formats have internal alignment restrictions making this not + # always exactly like image_size, so get the real value for comparison + virtual_size = get_size_from_qemu_img(img) + + # Read the format in various sizes, some of which will read whole + # sections in a single read, others will be completely unaligned, etc. + block_sizes = [64 * units.Ki, 1 * units.Mi] + # ISO images have a 32KB system area at the beginning of the image + # as a result reading that in 17 or 512 byte blocks takes too long, + # causing the test to fail. The 64KiB block size is enough to read + # the system area and header in a single read. the 1MiB block size + # adds very little time to the test so we include it. + if format_name != 'iso': + block_sizes.extend([17, 512]) + for block_size in block_sizes: + fmt = self._test_format_at_block_size(format_name, img, block_size) + self.assertTrue(fmt.format_match, + 'Failed to match %s at size %i block %i' % ( + format_name, image_size, block_size)) + self.assertEqual(virtual_size, fmt.virtual_size, + ('Failed to calculate size for %s at size %i ' + 'block %i') % (format_name, image_size, + block_size)) + memory = sum(fmt.context_info.values()) + self.assertLess(memory, 512 * units.Ki, + 'Format used more than 512KiB of memory: %s' % ( + fmt.context_info)) + + def _test_format(self, format_name, subformat=None): + # Try a few different image sizes, including some odd and very small + # sizes + for image_size in (512, 513, 2057, 7): + self._test_format_at_image_size(format_name, image_size * units.Mi, + subformat=subformat) + + def test_qcow2(self): + self._test_format('qcow2') + + def test_iso_9660(self): + self._test_format('iso', subformat='9660') + + def test_iso_udf(self): + self._test_format('iso', subformat='udf') + + def _generate_bad_iso(self): + # we want to emulate a malicious user who uploads a an + # ISO file has a qcow2 header in the system area + # of the ISO file + # we will create a qcow2 image and an ISO file + # and then copy the qcow2 header to the ISO file + # e.g. + # mkisofs -o orig.iso /etc/resolv.conf + # qemu-img create orig.qcow2 -f qcow2 64M + # dd if=orig.qcow2 of=outcome bs=32K count=1 + # dd if=orig.iso of=outcome bs=32K skip=1 seek=1 + + qcow = self._create_img('qcow2', 10 * units.Mi) + iso = self._create_iso(64 * units.Mi, subformat='9660') + # first ensure the files are valid + iso_fmt = self._test_format_at_block_size('iso', iso, 4 * units.Ki) + self.assertTrue(iso_fmt.format_match) + qcow_fmt = self._test_format_at_block_size('qcow2', qcow, 4 * units.Ki) + self.assertTrue(qcow_fmt.format_match) + # now copy the qcow2 header to an ISO file + prefix = TEST_IMAGE_PREFIX + prefix += '-bad-' + fn = tempfile.mktemp(prefix=prefix, suffix='.iso') + self._created_files.append(fn) + subprocess.check_output( + 'dd if=%s of=%s bs=32K count=1' % (qcow, fn), + shell=True) + subprocess.check_output( + 'dd if=%s of=%s bs=32K skip=1 seek=1' % (iso, fn), + shell=True) + return qcow, iso, fn + + def test_bad_iso_qcow2(self): + + _, _, fn = self._generate_bad_iso() + + iso_check = self._test_format_at_block_size('iso', fn, 4 * units.Ki) + qcow_check = self._test_format_at_block_size('qcow2', fn, 4 * units.Ki) + # this system area of the ISO file is not considered part of the format + # the qcow2 header is in the system area of the ISO file + # so the ISO file is still valid + self.assertTrue(iso_check.format_match) + # the qcow2 header is in the system area of the ISO file + # but that will be parsed by the qcow2 format inspector + # and it will match + self.assertTrue(qcow_check.format_match) + # if we call format_inspector.detect_file_format it should detect + # and raise an exception because both match internally. + e = self.assertRaises( + format_inspector.ImageFormatError, + format_inspector.detect_file_format, fn) + self.assertIn('Multiple formats detected', str(e)) + + def test_vhd(self): + self._test_format('vhd') + + def test_vhdx(self): + self._test_format('vhdx') + + def test_vmdk(self): + self._test_format('vmdk') + + def test_vmdk_stream_optimized(self): + self._test_format('vmdk', 'streamOptimized') + + def test_from_file_reads_minimum(self): + img = self._create_img('qcow2', 10 * units.Mi) + file_size = os.stat(img).st_size + fmt = format_inspector.QcowInspector.from_file(img) + # We know everything we need from the first 512 bytes of a QCOW image, + # so make sure that we did not read the whole thing when we inspect + # a local file. + self.assertLess(fmt.actual_size, file_size) + + def test_qed_always_unsafe(self): + img = self._create_img('qed', 10 * units.Mi) + fmt = format_inspector.get_inspector('qed').from_file(img) + self.assertTrue(fmt.format_match) + self.assertFalse(fmt.safety_check()) + + def _test_vmdk_bad_descriptor_offset(self, subformat=None): + format_name = 'vmdk' + image_size = 10 * units.Mi + descriptorOffsetAddr = 0x1c + BAD_ADDRESS = 0x400 + img = self._create_img(format_name, image_size, subformat=subformat) + + # Corrupt the header + fd = open(img, 'r+b') + fd.seek(descriptorOffsetAddr) + fd.write(struct.pack(' + mdev_b2107403_110c_45b0_af87_32cc91597b8a_0000_41_00_0 + /sys/devices/pci0000:40/0000:40:03.1/0000:41:00.0/b2107403-110c-45b0-af87-32cc91597b8a + pci_0000_41_00_0 + + vfio_mdev + + + + b2107403-110c-45b0-af87-32cc91597b8a + + + """ + + obj = config.LibvirtConfigNodeDevice() + obj.parse_str(xmlin) + self.assertIsInstance(obj.mdev_information, + config.LibvirtConfigNodeDeviceMdevInformation) + self.assertEqual("nvidia-442", obj.mdev_information.type) + self.assertEqual(57, obj.mdev_information.iommu_group) + self.assertEqual("b2107403-110c-45b0-af87-32cc91597b8a", + obj.mdev_information.uuid) def test_config_vdpa_device(self): xmlin = """ diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index fd3d322b198..85ea8f0283f 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -740,16 +740,15 @@ def setUp(self): 'resolve_driver_format', imagebackend.Image._get_driver_format) - self.useFixture(nova_fixtures.LibvirtFixture()) + self.libvirt = self.useFixture(nova_fixtures.LibvirtFixture()) + self.cgroups = self.useFixture(nova_fixtures.CGroupsFixture()) # ensure tests perform the same on all host architectures; this is # already done by the fakelibvirt fixture but we want to change the # architecture in some tests - _p = mock.patch('os.uname') - self.mock_uname = _p.start() + self.mock_uname = self.libvirt.mock_uname self.mock_uname.return_value = fakelibvirt.os_uname( 'Linux', '', '5.4.0-0-generic', '', fields.Architecture.X86_64) - self.addCleanup(_p.stop) self.test_instance = _create_test_instance() network_info = objects.InstanceInfoCache( @@ -2260,6 +2259,8 @@ def test_device_metadata(self, mock_version): instance_ref.info_cache = objects.InstanceInfoCache( network_info=network_info) + pci_utils.get_mac_by_pci_address.side_effect = None + pci_utils.get_mac_by_pci_address.return_value = 'da:d1:f2:91:95:c1' with test.nested( mock.patch('nova.objects.VirtualInterfaceList' '.get_by_instance_uuid', return_value=vifs), @@ -2269,8 +2270,7 @@ def test_device_metadata(self, mock_version): return_value=guest), mock.patch.object(nova.virt.libvirt.guest.Guest, 'get_xml_desc', return_value=xml), - mock.patch.object(pci_utils, 'get_mac_by_pci_address', - return_value='da:d1:f2:91:95:c1')): + ): metadata_obj = drvr._build_device_metadata(self.context, instance_ref) metadata = metadata_obj.devices @@ -2957,9 +2957,7 @@ def test_get_live_migrate_numa_info_empty(self, _): 'fake-instance-numa-topology', 'fake-flavor', 'fake-image-meta').obj_to_primitive()) - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=True) - def test_get_guest_config_numa_host_instance_fits(self, is_able): + def test_get_guest_config_numa_host_instance_fits(self): self.flags(cpu_shared_set=None, cpu_dedicated_set=None, group='compute') instance_ref = objects.Instance(**self.test_instance) @@ -2991,14 +2989,12 @@ def test_get_guest_config_numa_host_instance_fits(self, is_able): cfg = drvr._get_guest_config(instance_ref, [], image_meta, disk_info) self.assertIsNone(cfg.cpuset) - self.assertEqual(0, len(cfg.cputune.vcpupin)) + self.assertIsNone(cfg.cputune) self.assertIsNone(cfg.cpu.numa) @mock.patch('nova.privsep.utils.supports_direct_io', new=mock.Mock(return_value=True)) - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=True) - def test_get_guest_config_numa_host_instance_no_fit(self, is_able): + def test_get_guest_config_numa_host_instance_no_fit(self): instance_ref = objects.Instance(**self.test_instance) image_meta = objects.ImageMeta.from_dict(self.test_image_meta) flavor = objects.Flavor(memory_mb=4096, vcpus=4, root_gb=496, @@ -3028,7 +3024,7 @@ def test_get_guest_config_numa_host_instance_no_fit(self, is_able): image_meta, disk_info) self.assertFalse(choice_mock.called) self.assertIsNone(cfg.cpuset) - self.assertEqual(0, len(cfg.cputune.vcpupin)) + self.assertIsNone(cfg.cputune) self.assertIsNone(cfg.cpu.numa) def _test_get_guest_memory_backing_config( @@ -3389,10 +3385,7 @@ def test_get_guest_memory_backing_config_file_backed_hugepages(self): self._test_get_guest_memory_backing_config, host_topology, inst_topology, numa_tune) - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=True) - def test_get_guest_config_numa_host_instance_pci_no_numa_info( - self, is_able): + def test_get_guest_config_numa_host_instance_pci_no_numa_info(self): self.flags(cpu_shared_set='3', cpu_dedicated_set=None, group='compute') @@ -3436,14 +3429,12 @@ def test_get_guest_config_numa_host_instance_pci_no_numa_info( cfg = conn._get_guest_config(instance_ref, [], image_meta, disk_info) self.assertEqual(set([3]), cfg.cpuset) - self.assertEqual(0, len(cfg.cputune.vcpupin)) + self.assertIsNone(cfg.cputune) self.assertIsNone(cfg.cpu.numa) @mock.patch('nova.privsep.utils.supports_direct_io', new=mock.Mock(return_value=True)) - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=True) - def test_get_guest_config_numa_host_instance_2pci_no_fit(self, is_able): + def test_get_guest_config_numa_host_instance_2pci_no_fit(self): self.flags(cpu_shared_set='3', cpu_dedicated_set=None, group='compute') instance_ref = objects.Instance(**self.test_instance) @@ -3490,7 +3481,7 @@ def test_get_guest_config_numa_host_instance_2pci_no_fit(self, is_able): image_meta, disk_info) self.assertFalse(choice_mock.called) self.assertEqual(set([3]), cfg.cpuset) - self.assertEqual(0, len(cfg.cputune.vcpupin)) + self.assertIsNone(cfg.cputune) self.assertIsNone(cfg.cpu.numa) @mock.patch.object(fakelibvirt.Connection, 'getType') @@ -3551,10 +3542,7 @@ def test_get_guest_config_numa_other_arch_qemu(self): exception.NUMATopologyUnsupported, None) - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=True) - def test_get_guest_config_numa_host_instance_fit_w_cpu_pinset( - self, is_able): + def test_get_guest_config_numa_host_instance_fit_w_cpu_pinset(self): self.flags(cpu_shared_set='2-3', cpu_dedicated_set=None, group='compute') @@ -3589,12 +3577,10 @@ def test_get_guest_config_numa_host_instance_fit_w_cpu_pinset( # NOTE(ndipanov): we make sure that pin_set was taken into account # when choosing viable cells self.assertEqual(set([2, 3]), cfg.cpuset) - self.assertEqual(0, len(cfg.cputune.vcpupin)) + self.assertIsNone(cfg.cputune) self.assertIsNone(cfg.cpu.numa) - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=True) - def test_get_guest_config_non_numa_host_instance_topo(self, is_able): + def test_get_guest_config_non_numa_host_instance_topo(self): instance_topology = objects.InstanceNUMATopology(cells=[ objects.InstanceNUMACell( id=0, cpuset=set([0]), pcpuset=set(), memory=1024), @@ -3631,7 +3617,7 @@ def test_get_guest_config_non_numa_host_instance_topo(self, is_able): cfg = drvr._get_guest_config(instance_ref, [], image_meta, disk_info) self.assertIsNone(cfg.cpuset) - self.assertEqual(0, len(cfg.cputune.vcpupin)) + self.assertIsNone(cfg.cputune) self.assertIsNone(cfg.numatune) self.assertIsNotNone(cfg.cpu.numa) for instance_cell, numa_cfg_cell in zip( @@ -3641,9 +3627,7 @@ def test_get_guest_config_non_numa_host_instance_topo(self, is_able): self.assertEqual(instance_cell.memory * units.Ki, numa_cfg_cell.memory) - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=True) - def test_get_guest_config_numa_host_instance_topo(self, is_able): + def test_get_guest_config_numa_host_instance_topo(self): self.flags(cpu_shared_set='0-5', cpu_dedicated_set=None, group='compute') @@ -6974,14 +6958,12 @@ def test_get_guest_config_with_rng_limits(self): self.assertEqual(cfg.devices[5].rate_bytes, 1024) self.assertEqual(cfg.devices[5].rate_period, 2) - @mock.patch('nova.virt.libvirt.driver.os.path.exists') - @test.patch_exists(SEV_KERNEL_PARAM_FILE, False) - def test_get_guest_config_with_rng_backend(self, mock_path): + @test.patch_exists(SEV_KERNEL_PARAM_FILE, result=False, other=True) + def test_get_guest_config_with_rng_backend(self): self.flags(virt_type='kvm', rng_dev_path='/dev/hw_rng', group='libvirt') self.flags(pointer_model='ps2mouse') - mock_path.return_value = True drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) instance_ref = objects.Instance(**self.test_instance) @@ -7038,26 +7020,6 @@ def test_get_guest_config_with_rng_dev_not_present(self, mock_path): [], image_meta, disk_info) - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=True) - def test_guest_cpu_shares_with_multi_vcpu(self, is_able): - self.flags(virt_type='kvm', group='libvirt') - - drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) - - instance_ref = objects.Instance(**self.test_instance) - instance_ref.flavor.vcpus = 4 - image_meta = objects.ImageMeta.from_dict(self.test_image_meta) - - disk_info = blockinfo.get_disk_info(CONF.libvirt.virt_type, - instance_ref, - image_meta) - - cfg = drvr._get_guest_config(instance_ref, [], - image_meta, disk_info) - - self.assertEqual(4096, cfg.cputune.shares) - @mock.patch.object( host.Host, "is_cpu_control_policy_capable", return_value=True) def test_get_guest_config_with_cpu_quota(self, is_able): @@ -7396,9 +7358,7 @@ def test_get_guest_config_disk_cachemodes_network( self.flags(images_type='rbd', group='libvirt') self._test_get_guest_config_disk_cachemodes('rbd') - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=True) - def test_get_guest_config_with_bogus_cpu_quota(self, is_able): + def test_get_guest_config_with_bogus_cpu_quota(self): self.flags(virt_type='kvm', group='libvirt') drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) @@ -7416,9 +7376,10 @@ def test_get_guest_config_with_bogus_cpu_quota(self, is_able): drvr._get_guest_config, instance_ref, [], image_meta, disk_info) - @mock.patch.object( - host.Host, "is_cpu_control_policy_capable", return_value=False) - def test_get_update_guest_cputune(self, is_able): + def test_get_update_guest_cputune(self): + # No CPU controller on the host + self.cgroups.version = 0 + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) instance_ref = objects.Instance(**self.test_instance) instance_ref.flavor.extra_specs = {'quota:cpu_shares': '10000', @@ -7591,11 +7552,8 @@ def test_get_guest_config_armv7(self, mock_numa, mock_storage): @mock.patch.object(libvirt_driver.LibvirtDriver, "_get_guest_storage_config") @mock.patch.object(libvirt_driver.LibvirtDriver, "_has_numa_support") - @mock.patch('os.path.exists', return_value=True) - @test.patch_exists(SEV_KERNEL_PARAM_FILE, False) - def test_get_guest_config_aarch64( - self, mock_path_exists, mock_numa, mock_storage, - ): + @test.patch_exists(SEV_KERNEL_PARAM_FILE, result=False, other=True) + def test_get_guest_config_aarch64(self, mock_numa, mock_storage): TEST_AMOUNT_OF_PCIE_SLOTS = 8 CONF.set_override("num_pcie_ports", TEST_AMOUNT_OF_PCIE_SLOTS, group='libvirt') @@ -7615,7 +7573,6 @@ def test_get_guest_config_aarch64( cfg = drvr._get_guest_config(instance_ref, _fake_network_info(self), image_meta, disk_info) - self.assertTrue(mock_path_exists.called) self.assertEqual(cfg.os_mach_type, "virt") num_ports = 0 @@ -7632,10 +7589,9 @@ def test_get_guest_config_aarch64( @mock.patch.object(libvirt_driver.LibvirtDriver, "_get_guest_storage_config") @mock.patch.object(libvirt_driver.LibvirtDriver, "_has_numa_support") - @mock.patch('os.path.exists', return_value=True) - @test.patch_exists(SEV_KERNEL_PARAM_FILE, False) + @test.patch_exists(SEV_KERNEL_PARAM_FILE, result=False, other=True) def test_get_guest_config_aarch64_with_graphics( - self, mock_path_exists, mock_numa, mock_storage, + self, mock_numa, mock_storage, ): self.mock_uname.return_value = fakelibvirt.os_uname( 'Linux', '', '5.4.0-0-generic', '', fields.Architecture.AARCH64) @@ -7645,7 +7601,6 @@ def test_get_guest_config_aarch64_with_graphics( cfg = self._get_guest_config_with_graphics() - self.assertTrue(mock_path_exists.called) self.assertEqual(cfg.os_mach_type, "virt") usbhost_exists = False @@ -8893,7 +8848,7 @@ def test_unquiesce(self, mock_has_min_version): def test_create_snapshot_metadata(self): base = objects.ImageMeta.from_dict( - {'disk_format': 'raw'}) + {'disk_format': 'qcow2'}) instance_data = {'kernel_id': 'kernel', 'project_id': 'prj_id', 'ramdisk_id': 'ram_id', @@ -8925,10 +8880,12 @@ def test_create_snapshot_metadata(self): {'disk_format': 'ami', 'container_format': 'test_container'}) expected['properties']['os_type'] = instance['os_type'] - expected['disk_format'] = base.disk_format + # The disk_format of the snapshot should be the *actual* format of the + # thing we upload, regardless of what type of image we booted from. + expected['disk_format'] = img_fmt expected['container_format'] = base.container_format ret = drvr._create_snapshot_metadata(base, instance, img_fmt, snp_name) - self.assertEqual(ret, expected) + self.assertEqual(expected, ret) def test_get_volume_driver(self): conn = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) @@ -9231,7 +9188,7 @@ def test_disconnect_multiattach_single_connection( drvr._disconnect_volume( self.context, fake_connection_info, fake_instance_1) mock_volume_driver.disconnect_volume.assert_called_once_with( - fake_connection_info, fake_instance_1) + fake_connection_info, fake_instance_1, force=False) @mock.patch.object(libvirt_driver.LibvirtDriver, '_detach_encryptor') @mock.patch('nova.objects.InstanceList.get_uuids_by_host') @@ -9605,7 +9562,12 @@ def test_detach_volume_order_with_encryptors(self, mock_get_guest, device_name='vdc', ), mock.call.detach_encryptor(**encryption), - mock.call.disconnect_volume(connection_info, instance)]) + mock.call.disconnect_volume( + connection_info, + instance, + force=False, + ) + ]) get_device_conf_func = mock_detach_with_retry.mock_calls[0][1][2] self.assertEqual(mock_guest.get_disk, get_device_conf_func.func) self.assertEqual(('vdc',), get_device_conf_func.args) @@ -10915,6 +10877,25 @@ def test_check_can_live_migrate_guest_cpu_none_model( 'serial_listen_addr': None}, result.obj_to_primitive()['nova_object.data']) + @mock.patch( + 'nova.network.neutron.API.has_port_binding_extension', + new=mock.Mock(return_value=False)) + @mock.patch.object(libvirt_driver.LibvirtDriver, + '_create_shared_storage_test_file', + return_value='fake') + @mock.patch.object(libvirt_driver.LibvirtDriver, '_compare_cpu') + def test_check_can_live_migrate_guest_cpu_none_model_skip_compare( + self, mock_cpu, mock_test_file): + self.flags(group='workarounds', skip_cpu_compare_on_dest=True) + instance_ref = objects.Instance(**self.test_instance) + instance_ref.vcpu_model = test_vcpu_model.fake_vcpumodel + instance_ref.vcpu_model.model = None + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) + compute_info = {'cpu_info': 'asdf', 'disk_available_least': 1} + drvr.check_can_live_migrate_destination( + self.context, instance_ref, compute_info, compute_info) + mock_cpu.assert_not_called() + @mock.patch( 'nova.network.neutron.API.has_port_binding_extension', new=mock.Mock(return_value=False)) @@ -11388,8 +11369,6 @@ def test_check_can_live_migrate_source_block_migration_none_no_share(self): False, False) - @mock.patch('nova.virt.libvirt.driver.LibvirtDriver.' - '_assert_dest_node_has_enough_disk') @mock.patch('nova.virt.libvirt.driver.LibvirtDriver.' '_assert_dest_node_has_enough_disk') @mock.patch('nova.virt.libvirt.driver.LibvirtDriver.' @@ -11397,7 +11376,7 @@ def test_check_can_live_migrate_source_block_migration_none_no_share(self): @mock.patch('nova.virt.libvirt.driver.LibvirtDriver.' '_check_shared_storage_test_file') def test_check_can_live_migration_source_disk_over_commit_none(self, - mock_check, mock_shared_block, mock_enough, mock_disk_check): + mock_check, mock_shared_block, mock_disk_check): mock_check.return_value = False mock_shared_block.return_value = False @@ -11615,7 +11594,7 @@ def test_live_migration_update_graphics_xml(self, mock_xml, mock_migrateToURI3, mock_min_version): self.compute = manager.ComputeManager() - instance_ref = self.test_instance + instance_ref = objects.Instance(**self.test_instance) target_connection = '127.0.0.2' xml_tmpl = ("" @@ -12295,7 +12274,7 @@ def test_live_migration_update_serial_console_xml(self, mock_xml, mock_get, mock_min_version): self.compute = manager.ComputeManager() - instance_ref = self.test_instance + instance_ref = objects.Instance(**self.test_instance) target_connection = '127.0.0.2' xml_tmpl = ("" @@ -12585,7 +12564,7 @@ def test_live_migration_raises_exception(self, mock_xml, mock_min_version): # Prepare data self.compute = manager.ComputeManager() - instance_ref = self.test_instance + instance_ref = objects.Instance(**self.test_instance) target_connection = '127.0.0.2' disk_paths = ['vda', 'vdb'] @@ -13816,10 +13795,11 @@ def test_create_images_and_backing_images_exist( '/fake/instance/dir', disk_info) self.assertFalse(mock_fetch_image.called) + @mock.patch('nova.image.format_inspector.detect_file_format') @mock.patch('nova.privsep.path.utime') @mock.patch('nova.virt.libvirt.utils.create_cow_image') def test_create_images_and_backing_ephemeral_gets_created( - self, mock_create_cow_image, mock_utime): + self, mock_create_cow_image, mock_utime, mock_detect): drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) base_dir = os.path.join(CONF.instances_path, @@ -13850,8 +13830,11 @@ def test_create_images_and_backing_ephemeral_gets_created( 'ephemeral_foo') ] + # This also asserts that the filesystem label name is generated + # correctly as 'ephemeral0' to help prevent regression of the + # related bug fix from https://launchpad.net/bugs/2061701 create_ephemeral_mock.assert_called_once_with( - ephemeral_size=1, fs_label='ephemeral_foo', + ephemeral_size=1, fs_label='ephemeral0', os_type='linux', target=ephemeral_backing) fetch_image_mock.assert_called_once_with( @@ -15529,8 +15512,7 @@ def test_create_image_with_ephemerals(self, mock_get_ext): filename=filename, size=100 * units.Gi, ephemeral_size=mock.ANY, specified_fs=None) - @mock.patch.object(nova.virt.libvirt.imagebackend.Image, 'cache') - def test_create_image_resize_snap_backend(self, mock_cache): + def test_create_image_resize_snap_backend(self): drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) instance = objects.Instance(**self.test_instance) instance.task_state = task_states.RESIZE_FINISH @@ -15556,11 +15538,13 @@ def test_create_ephemeral_specified_fs(self, fake_mkfs): fake_mkfs.assert_has_calls([mock.call('ext4', '/dev/something', 'myVol')]) + @mock.patch('nova.image.format_inspector.detect_file_format') @mock.patch('nova.privsep.path.utime') @mock.patch('nova.virt.libvirt.utils.fetch_image') @mock.patch('nova.virt.libvirt.utils.create_cow_image') def test_create_ephemeral_specified_fs_not_valid( - self, mock_create_cow_image, mock_fetch_image, mock_utime): + self, mock_create_cow_image, mock_fetch_image, mock_utime, + mock_detect): CONF.set_override('default_ephemeral_format', 'ext4') ephemerals = [{'device_type': 'disk', 'disk_bus': 'virtio', @@ -15989,9 +15973,10 @@ def test_get_host_ip_addr(self): self.assertEqual(ip, CONF.my_ip) @mock.patch.object(libvirt_driver.LOG, 'warning') - @mock.patch('nova.compute.utils.get_machine_ips') - def test_check_my_ip(self, mock_ips, mock_log): - mock_ips.return_value = ['8.8.8.8', '75.75.75.75'] + def test_check_my_ip(self, mock_log): + + self.libvirt.mock_get_machine_ips.return_value = [ + '8.8.8.8', '75.75.75.75'] drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) drvr._check_my_ip() mock_log.assert_called_once_with(u'my_ip address (%(my_ip)s) was ' @@ -16013,6 +15998,7 @@ def test_conn_event_handler(self): drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) service_mock = mock.MagicMock() service_mock.disabled.return_value = False + drvr._host._init_events.return_value = None with test.nested( mock.patch.object(drvr._host, "_connect", side_effect=fakelibvirt.make_libvirtError( @@ -16020,8 +16006,6 @@ def test_conn_event_handler(self): "Failed to connect to host", error_code= fakelibvirt.VIR_ERR_INTERNAL_ERROR)), - mock.patch.object(drvr._host, "_init_events", - return_value=None), mock.patch.object(objects.Service, "get_by_compute_host", return_value=service_mock)): @@ -16036,6 +16020,7 @@ def test_command_with_broken_connection(self): drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) service_mock = mock.MagicMock() service_mock.disabled.return_value = False + drvr._host._init_events.return_value = None with test.nested( mock.patch.object(drvr._host, "_connect", side_effect=fakelibvirt.make_libvirtError( @@ -16043,8 +16028,6 @@ def test_command_with_broken_connection(self): "Failed to connect to host", error_code= fakelibvirt.VIR_ERR_INTERNAL_ERROR)), - mock.patch.object(drvr._host, "_init_events", - return_value=None), mock.patch.object(host.Host, "has_min_version", return_value=True), mock.patch.object(drvr, "_do_quality_warnings", @@ -16064,11 +16047,10 @@ def test_service_resume_after_broken_connection(self): drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) service_mock = mock.MagicMock() service_mock.disabled.return_value = True + drvr._host._init_events.return_value = None with test.nested( mock.patch.object(drvr._host, "_connect", return_value=mock.MagicMock()), - mock.patch.object(drvr._host, "_init_events", - return_value=None), mock.patch.object(host.Host, "has_min_version", return_value=True), mock.patch.object(drvr, "_do_quality_warnings", @@ -17556,12 +17538,11 @@ def get_host_capabilities_stub(self): got = drvr._get_cpu_info() self.assertEqual(want, got) - @mock.patch.object(pci_utils, 'get_ifname_by_pci_address', - return_value='ens1') @mock.patch.object(host.Host, 'list_pci_devices', return_value=['pci_0000_04_00_3', 'pci_0000_04_10_7', 'pci_0000_04_11_7']) - def test_get_pci_passthrough_devices(self, mock_list, mock_get_ifname): + def test_get_pci_passthrough_devices(self, mock_list): + pci_utils.get_ifname_by_pci_address.return_value = 'ens1' drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) @@ -17595,7 +17576,10 @@ def test_get_pci_passthrough_devices(self, mock_list, mock_get_ifname): "vendor_id": '8086', "dev_type": fields.PciDeviceType.SRIOV_PF, "phys_function": None, - "numa_node": None}, + "numa_node": None, + # value defined in the LibvirtFixture + "mac_address": "52:54:00:1e:59:c6", + }, { "dev_id": "pci_0000_04_10_7", "domain": 0, @@ -17631,7 +17615,7 @@ def test_get_pci_passthrough_devices(self, mock_list, mock_get_ifname): # The first call for every VF is to determine parent_ifname and # the second call to determine the MAC address. - mock_get_ifname.assert_has_calls([ + pci_utils.get_ifname_by_pci_address.assert_has_calls([ mock.call('0000:04:10.7', pf_interface=True), mock.call('0000:04:11.7', pf_interface=True), ]) @@ -19804,16 +19788,64 @@ def test_cleanup_destroy_secrets(self, mock_disconnect_volume): self.context, mock.sentinel.connection_info, instance, - destroy_secrets=False + destroy_secrets=False, + force=True ), mock.call( self.context, mock.sentinel.connection_info, instance, - destroy_secrets=True + destroy_secrets=True, + force=True ) ]) + @mock.patch('nova.virt.libvirt.driver.LibvirtDriver._get_volume_driver') + @mock.patch( + 'nova.virt.libvirt.driver.LibvirtDriver._should_disconnect_target', + new=mock.Mock(return_value=True)) + @mock.patch('nova.virt.libvirt.driver.LibvirtDriver._detach_encryptor', + new=mock.Mock()) + @mock.patch('nova.virt.libvirt.driver.LibvirtDriver._undefine_domain', + new=mock.Mock()) + @mock.patch('nova.virt.libvirt.driver.LibvirtDriver._get_vpmems', + new=mock.Mock(return_value=None)) + def test_cleanup_disconnect_volume(self, mock_vol_driver): + """Verify that we call disconnect_volume() with force=True + + cleanup() is called by destroy() when an instance is being deleted and + force=True should be passed down to os-brick's disconnect_volume() + call, which will ensure removal of devices regardless of errors. + + We need to ensure that devices are removed when an instance is being + deleted to avoid leaving leftover devices that could later be + erroneously connected by external entities (example: multipathd) to + instances that should not have access to the volumes. + + See https://bugs.launchpad.net/nova/+bug/2004555 for details. + """ + connection_info = mock.MagicMock() + block_device_info = { + 'block_device_mapping': [ + { + 'connection_info': connection_info + } + ] + } + instance = objects.Instance(self.context, **self.test_instance) + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI()) + + drvr.cleanup( + self.context, + instance, + network_info={}, + block_device_info=block_device_info, + destroy_vifs=False, + destroy_disks=False, + ) + mock_vol_driver.return_value.disconnect_volume.assert_called_once_with( + connection_info, instance, force=True) + @mock.patch.object(libvirt_driver.LibvirtDriver, '_get_volume_encryption') @mock.patch.object(libvirt_driver.LibvirtDriver, '_allow_native_luksv1') def test_swap_volume_native_luks_blocked(self, mock_allow_native_luksv1, @@ -21655,6 +21687,7 @@ def setUp(self): self.flags(sysinfo_serial="none", group="libvirt") self.flags(instances_path=self.useFixture(fixtures.TempDir()).path) self.useFixture(nova_fixtures.LibvirtFixture()) + self.useFixture(nova_fixtures.CGroupsFixture()) os_vif.initialize() self.drvr = libvirt_driver.LibvirtDriver( @@ -22041,11 +22074,8 @@ def test_migrate_disk_and_power_off_resize_error_default_ephemeral( self.drvr.migrate_disk_and_power_off, 'ctx', instance, '10.0.0.1', flavor_obj, None) - @mock.patch('nova.virt.libvirt.driver.LibvirtDriver' - '._get_instance_disk_info') @mock.patch('nova.virt.driver.block_device_info_get_ephemerals') - def test_migrate_disk_and_power_off_resize_error_eph(self, mock_get, - mock_get_disk_info): + def test_migrate_disk_and_power_off_resize_error_eph(self, mock_get): mappings = [ { 'device_name': '/dev/sdb4', @@ -22092,7 +22122,6 @@ def test_migrate_disk_and_power_off_resize_error_eph(self, mock_get, # Old flavor, eph is 20, real disk is 3, target is 2, fail flavor = {'root_gb': 10, 'ephemeral_gb': 2} flavor_obj = objects.Flavor(**flavor) - mock_get_disk_info.return_value = fake_disk_info_json(instance) self.assertRaises( exception.InstanceFaultRollback, @@ -25542,9 +25571,7 @@ def test_get_gpu_inventories_with_a_single_type(self): } self._test_get_gpu_inventories(drvr, expected, ['nvidia-11']) - @mock.patch('nova.virt.libvirt.driver.LibvirtDriver' - '._get_mdev_capable_devices') - def test_get_gpu_inventories_with_two_types(self, get_mdev_capable_devs): + def test_get_gpu_inventories_with_two_types(self): self.flags(enabled_mdev_types=['nvidia-11', 'nvidia-12'], group='devices') # we need to call the below again to ensure the updated @@ -28203,7 +28230,8 @@ def test_ami(self): utils.get_system_metadata_from_image( {'disk_format': 'ami'}) - self._test_snapshot(disk_format='ami') + # If we're uploading a qcow2, we must set the disk_format as such + self._test_snapshot(disk_format='qcow2') @mock.patch('nova.virt.libvirt.utils.get_disk_type_from_path', new=mock.Mock(return_value=None)) @@ -28491,13 +28519,11 @@ class LVMSnapshotTests(_BaseSnapshotTests): new=mock.Mock(return_value=None)) @mock.patch('nova.virt.libvirt.utils.get_disk_type_from_path', new=mock.Mock(return_value='lvm')) - @mock.patch('nova.virt.libvirt.utils.file_open', - side_effect=[io.BytesIO(b''), io.BytesIO(b'')]) @mock.patch.object(libvirt_driver.imagebackend.images, 'convert_image') @mock.patch.object(libvirt_driver.imagebackend.lvm, 'volume_info') def _test_lvm_snapshot(self, disk_format, mock_volume_info, - mock_convert_image, mock_file_open): + mock_convert_image): self.flags(images_type='lvm', images_volume_group='nova-vg', group='libvirt') diff --git a/nova/tests/unit/virt/libvirt/test_guest.py b/nova/tests/unit/virt/libvirt/test_guest.py index 70d438d816a..47e9ba4b623 100644 --- a/nova/tests/unit/virt/libvirt/test_guest.py +++ b/nova/tests/unit/virt/libvirt/test_guest.py @@ -1040,3 +1040,25 @@ def test_job_info_operation_invalid(self, mock_stats, mock_info): mock_stats.assert_called_once_with() mock_info.assert_called_once_with() + + @mock.patch.object(fakelibvirt.virDomain, "jobInfo") + @mock.patch.object(fakelibvirt.virDomain, "jobStats") + def test_job_stats_no_ram(self, mock_stats, mock_info): + mock_stats.side_effect = fakelibvirt.make_libvirtError( + fakelibvirt.libvirtError, + "internal error: migration was active, but no RAM info was set", + error_code=fakelibvirt.VIR_ERR_INTERNAL_ERROR, + error_message="migration was active, but no RAM info was set") + + info = self.guest.get_job_info() + + self.assertIsInstance(info, libvirt_guest.JobInfo) + self.assertEqual(fakelibvirt.VIR_DOMAIN_JOB_NONE, info.type) + self.assertEqual(0, info.time_elapsed) + self.assertEqual(0, info.time_remaining) + self.assertEqual(0, info.memory_total) + self.assertEqual(0, info.memory_processed) + self.assertEqual(0, info.memory_remaining) + + mock_stats.assert_called_once_with() + self.assertFalse(mock_info.called) diff --git a/nova/tests/unit/virt/libvirt/test_host.py b/nova/tests/unit/virt/libvirt/test_host.py index d71d13ab372..4d48762c910 100644 --- a/nova/tests/unit/virt/libvirt/test_host.py +++ b/nova/tests/unit/virt/libvirt/test_host.py @@ -16,6 +16,7 @@ import os +import ddt import eventlet from eventlet import greenthread from eventlet import tpool @@ -71,11 +72,10 @@ def setUp(self): self.useFixture(nova_fixtures.LibvirtFixture()) self.host = host.Host("qemu:///system") - @mock.patch("nova.virt.libvirt.host.Host._init_events") - def test_repeat_initialization(self, mock_init_events): + def test_repeat_initialization(self): for i in range(3): self.host.initialize() - mock_init_events.assert_called_once_with() + self.host._init_events.assert_called_once_with() @mock.patch.object(fakelibvirt.virConnect, "registerCloseCallback") def test_close_callback(self, mock_close): @@ -1113,8 +1113,9 @@ def test_get_pcinet_info(self): expect_vf = ["rx", "tx", "sg", "tso", "gso", "gro", "rxvlan", "txvlan"] self.assertEqual(expect_vf, actualvf) - @mock.patch.object(pci_utils, 'get_ifname_by_pci_address') - def test_get_pcidev_info_non_nic(self, mock_get_ifname): + def test_get_pcidev_info_non_nic(self): + pci_utils.get_mac_by_pci_address.side_effect = ( + exception.PciDeviceNotFoundById('0000:04:00.3')) dev_name = "pci_0000_04_11_7" pci_dev = fakelibvirt.NodeDevice( self.host._get_connection(), @@ -1128,11 +1129,10 @@ def test_get_pcidev_info_non_nic(self, mock_get_ifname): 'parent_addr': '0000:04:00.3', } self.assertEqual(expect_vf, actual_vf) - mock_get_ifname.assert_not_called() + pci_utils.get_ifname_by_pci_address.assert_not_called() - @mock.patch.object(pci_utils, 'get_ifname_by_pci_address', - return_value='ens1') - def test_get_pcidev_info(self, mock_get_ifname): + def test_get_pcidev_info(self): + pci_utils.get_ifname_by_pci_address.return_value = 'ens1' devs = { "pci_0000_04_00_3", "pci_0000_04_10_7", "pci_0000_04_11_7", "pci_0000_04_00_1", "pci_0000_03_00_0", "pci_0000_03_00_1", @@ -1156,9 +1156,9 @@ def test_get_pcidev_info(self, mock_get_ifname): dev for dev in node_devs.values() if dev.name() in devs] name = "pci_0000_04_00_3" - actual_vf = self.host._get_pcidev_info( + actual_pf = self.host._get_pcidev_info( name, node_devs[name], net_devs, [], []) - expect_vf = { + expect_pf = { "dev_id": "pci_0000_04_00_3", "address": "0000:04:00.3", "product_id": '1521', @@ -1166,8 +1166,10 @@ def test_get_pcidev_info(self, mock_get_ifname): "vendor_id": '8086', "label": 'label_8086_1521', "dev_type": obj_fields.PciDeviceType.SRIOV_PF, + # value defined in the LibvirtFixture + "mac_address": "52:54:00:1e:59:c6", } - self.assertEqual(expect_vf, actual_vf) + self.assertEqual(expect_pf, actual_pf) name = "pci_0000_04_10_7" actual_vf = self.host._get_pcidev_info( @@ -1222,9 +1224,9 @@ def test_get_pcidev_info(self, mock_get_ifname): self.assertEqual(expect_vf, actual_vf) name = "pci_0000_03_00_0" - actual_vf = self.host._get_pcidev_info( + actual_pf = self.host._get_pcidev_info( name, node_devs[name], net_devs, [], []) - expect_vf = { + expect_pf = { "dev_id": "pci_0000_03_00_0", "address": "0000:03:00.0", "product_id": '1013', @@ -1232,13 +1234,15 @@ def test_get_pcidev_info(self, mock_get_ifname): "vendor_id": '15b3', "label": 'label_15b3_1013', "dev_type": obj_fields.PciDeviceType.SRIOV_PF, + # value defined in the LibvirtFixture + "mac_address": "52:54:00:1e:59:c6", } - self.assertEqual(expect_vf, actual_vf) + self.assertEqual(expect_pf, actual_pf) name = "pci_0000_03_00_1" - actual_vf = self.host._get_pcidev_info( + actual_pf = self.host._get_pcidev_info( name, node_devs[name], net_devs, [], []) - expect_vf = { + expect_pf = { "dev_id": "pci_0000_03_00_1", "address": "0000:03:00.1", "product_id": '1013', @@ -1246,8 +1250,10 @@ def test_get_pcidev_info(self, mock_get_ifname): "vendor_id": '15b3', "label": 'label_15b3_1013', "dev_type": obj_fields.PciDeviceType.SRIOV_PF, + # value defined in the LibvirtFixture + "mac_address": "52:54:00:1e:59:c6", } - self.assertEqual(expect_vf, actual_vf) + self.assertEqual(expect_pf, actual_pf) # Parent PF with a VPD cap. name = "pci_0000_82_00_0" @@ -1264,6 +1270,8 @@ def test_get_pcidev_info(self, mock_get_ifname): "capabilities": { # Should be obtained from the parent PF in this case. "vpd": {"card_serial_number": "MT2113X00000"}}, + # value defined in the LibvirtFixture + "mac_address": "52:54:00:1e:59:c6", } self.assertEqual(expect_pf, actual_pf) @@ -1548,25 +1556,59 @@ def test_compare_cpu(self, mock_compareCPU): self.host.compare_cpu("cpuxml") mock_compareCPU.assert_called_once_with("cpuxml", 0) - def test_is_cpu_control_policy_capable_ok(self): + def test_is_cpu_control_policy_capable_via_neither(self): + self.useFixture(nova_fixtures.CGroupsFixture(version=0)) + self.assertFalse(self.host.is_cpu_control_policy_capable()) + + def test_is_cpu_control_policy_capable_via_cgroupsv1(self): + self.useFixture(nova_fixtures.CGroupsFixture(version=1)) + self.assertTrue(self.host.is_cpu_control_policy_capable()) + + def test_is_cpu_control_policy_capable_via_cgroupsv2(self): + self.useFixture(nova_fixtures.CGroupsFixture(version=2)) + self.assertTrue(self.host.is_cpu_control_policy_capable()) + + def test_has_cgroupsv1_cpu_controller_ok(self): m = mock.mock_open( - read_data="""cg /cgroup/cpu,cpuacct cg opt1,cpu,opt3 0 0 -cg /cgroup/memory cg opt1,opt2 0 0 -""") - with mock.patch('builtins.open', m, create=True): - self.assertTrue(self.host.is_cpu_control_policy_capable()) + read_data=( + "cg /cgroup/cpu,cpuacct cg opt1,cpu,opt3 0 0" + "cg /cgroup/memory cg opt1,opt2 0 0" + ) + ) + with mock.patch("builtins.open", m, create=True): + self.assertTrue(self.host._has_cgroupsv1_cpu_controller()) + + def test_has_cgroupsv1_cpu_controller_ko(self): + m = mock.mock_open( + read_data=( + "cg /cgroup/cpu,cpuacct cg opt1,opt2,opt3 0 0" + "cg /cgroup/memory cg opt1,opt2 0 0" + ) + ) + with mock.patch("builtins.open", m, create=True): + self.assertFalse(self.host._has_cgroupsv1_cpu_controller()) + + @mock.patch("builtins.open", side_effect=IOError) + def test_has_cgroupsv1_cpu_controller_ioerror(self, _): + self.assertFalse(self.host._has_cgroupsv1_cpu_controller()) + + def test_has_cgroupsv2_cpu_controller_ok(self): + m = mock.mock_open( + read_data="cpuset cpu io memory hugetlb pids rdma misc" + ) + with mock.patch("builtins.open", m, create=True): + self.assertTrue(self.host._has_cgroupsv2_cpu_controller()) - def test_is_cpu_control_policy_capable_ko(self): + def test_has_cgroupsv2_cpu_controller_ko(self): m = mock.mock_open( - read_data="""cg /cgroup/cpu,cpuacct cg opt1,opt2,opt3 0 0 -cg /cgroup/memory cg opt1,opt2 0 0 -""") - with mock.patch('builtins.open', m, create=True): - self.assertFalse(self.host.is_cpu_control_policy_capable()) + read_data="memory pids" + ) + with mock.patch("builtins.open", m, create=True): + self.assertFalse(self.host._has_cgroupsv2_cpu_controller()) - @mock.patch('builtins.open', side_effect=IOError) - def test_is_cpu_control_policy_capable_ioerror(self, mock_open): - self.assertFalse(self.host.is_cpu_control_policy_capable()) + @mock.patch("builtins.open", side_effect=IOError) + def test_has_cgroupsv2_cpu_controller_ioerror(self, _): + self.assertFalse(self.host._has_cgroupsv2_cpu_controller()) def test_get_canonical_machine_type(self): # this test relies on configuration from the FakeLibvirtFixture @@ -1928,6 +1970,7 @@ def setUp(self): self.host = host.Host("qemu:///system") +@ddt.ddt class TestLibvirtSEVUnsupported(TestLibvirtSEV): @mock.patch.object(os.path, 'exists', return_value=False) def test_kernel_parameter_missing(self, fake_exists): @@ -1935,19 +1978,26 @@ def test_kernel_parameter_missing(self, fake_exists): fake_exists.assert_called_once_with( '/sys/module/kvm_amd/parameters/sev') + @ddt.data( + ('0\n', False), + ('N\n', False), + ('1\n', True), + ('Y\n', True), + ) + @ddt.unpack @mock.patch.object(os.path, 'exists', return_value=True) - @mock.patch('builtins.open', mock.mock_open(read_data="0\n")) - def test_kernel_parameter_zero(self, fake_exists): - self.assertFalse(self.host._kernel_supports_amd_sev()) - fake_exists.assert_called_once_with( - '/sys/module/kvm_amd/parameters/sev') - - @mock.patch.object(os.path, 'exists', return_value=True) - @mock.patch('builtins.open', mock.mock_open(read_data="1\n")) - def test_kernel_parameter_one(self, fake_exists): - self.assertTrue(self.host._kernel_supports_amd_sev()) - fake_exists.assert_called_once_with( - '/sys/module/kvm_amd/parameters/sev') + def test_kernel_parameter( + self, sev_param_value, expected_support, mock_exists + ): + with mock.patch( + 'builtins.open', mock.mock_open(read_data=sev_param_value) + ): + self.assertIs( + expected_support, + self.host._kernel_supports_amd_sev() + ) + mock_exists.assert_called_once_with( + '/sys/module/kvm_amd/parameters/sev') @mock.patch.object(os.path, 'exists', return_value=True) @mock.patch('builtins.open', mock.mock_open(read_data="1\n")) diff --git a/nova/tests/unit/virt/libvirt/test_imagebackend.py b/nova/tests/unit/virt/libvirt/test_imagebackend.py index decb27f9824..ce6cf3909a6 100644 --- a/nova/tests/unit/virt/libvirt/test_imagebackend.py +++ b/nova/tests/unit/virt/libvirt/test_imagebackend.py @@ -522,13 +522,15 @@ def test_cache_template_exists(self, mock_exists): mock_exists.assert_has_calls(exist_calls) + @mock.patch('nova.image.format_inspector.detect_file_format') @mock.patch.object(imagebackend.utils, 'synchronized') @mock.patch('nova.virt.libvirt.utils.create_cow_image') @mock.patch.object(os.path, 'exists', side_effect=[]) @mock.patch.object(imagebackend.Image, 'verify_base_size') @mock.patch('nova.privsep.path.utime') def test_create_image( - self, mock_utime, mock_verify, mock_exist, mock_create, mock_sync + self, mock_utime, mock_verify, mock_exist, mock_create, mock_sync, + mock_detect_format ): mock_sync.side_effect = lambda *a, **kw: self._fake_deco fn = mock.MagicMock() @@ -549,7 +551,10 @@ def test_create_image( mock_exist.assert_has_calls(exist_calls) self.assertTrue(mock_sync.called) mock_utime.assert_called() + mock_detect_format.assert_called_once() + mock_detect_format.return_value.safety_check.assert_called_once_with() + @mock.patch('nova.image.format_inspector.detect_file_format') @mock.patch.object(imagebackend.utils, 'synchronized') @mock.patch('nova.virt.libvirt.utils.create_cow_image') @mock.patch.object(imagebackend.disk, 'extend') @@ -557,7 +562,8 @@ def test_create_image( @mock.patch.object(imagebackend.Qcow2, 'get_disk_size') @mock.patch('nova.privsep.path.utime') def test_create_image_too_small(self, mock_utime, mock_get, mock_exist, - mock_extend, mock_create, mock_sync): + mock_extend, mock_create, mock_sync, + mock_detect_format): mock_sync.side_effect = lambda *a, **kw: self._fake_deco mock_get.return_value = self.SIZE fn = mock.MagicMock() @@ -574,7 +580,9 @@ def test_create_image_too_small(self, mock_utime, mock_get, mock_exist, self.assertTrue(mock_sync.called) self.assertFalse(mock_create.called) self.assertFalse(mock_extend.called) + mock_detect_format.assert_called_once() + @mock.patch('nova.image.format_inspector.detect_file_format') @mock.patch.object(imagebackend.utils, 'synchronized') @mock.patch('nova.virt.libvirt.utils.create_cow_image') @mock.patch('nova.virt.libvirt.utils.get_disk_backing_file') @@ -586,7 +594,8 @@ def test_create_image_too_small(self, mock_utime, mock_get, mock_exist, def test_generate_resized_backing_files(self, mock_utime, mock_copy, mock_verify, mock_exist, mock_extend, mock_get, - mock_create, mock_sync): + mock_create, mock_sync, + mock_detect_format): mock_sync.side_effect = lambda *a, **kw: self._fake_deco mock_get.return_value = self.QCOW2_BASE fn = mock.MagicMock() @@ -613,7 +622,9 @@ def test_generate_resized_backing_files(self, mock_utime, mock_copy, self.assertTrue(mock_sync.called) self.assertFalse(mock_create.called) mock_utime.assert_called() + mock_detect_format.assert_called_once() + @mock.patch('nova.image.format_inspector.detect_file_format') @mock.patch.object(imagebackend.utils, 'synchronized') @mock.patch('nova.virt.libvirt.utils.create_cow_image') @mock.patch('nova.virt.libvirt.utils.get_disk_backing_file') @@ -624,7 +635,8 @@ def test_generate_resized_backing_files(self, mock_utime, mock_copy, def test_qcow2_exists_and_has_no_backing_file(self, mock_utime, mock_verify, mock_exist, mock_extend, mock_get, - mock_create, mock_sync): + mock_create, mock_sync, + mock_detect_format): mock_sync.side_effect = lambda *a, **kw: self._fake_deco mock_get.return_value = None fn = mock.MagicMock() @@ -645,6 +657,31 @@ def test_qcow2_exists_and_has_no_backing_file(self, mock_utime, self.assertTrue(mock_sync.called) self.assertFalse(mock_create.called) self.assertFalse(mock_extend.called) + mock_detect_format.assert_called_once() + + @mock.patch('nova.image.format_inspector.detect_file_format') + @mock.patch.object(imagebackend.utils, 'synchronized') + @mock.patch('nova.virt.libvirt.utils.create_image') + @mock.patch('nova.virt.libvirt.utils.get_disk_backing_file') + @mock.patch.object(imagebackend.disk, 'extend') + @mock.patch.object(os.path, 'exists', side_effect=[]) + @mock.patch.object(imagebackend.Image, 'verify_base_size') + def test_qcow2_exists_and_fails_safety_check(self, + mock_verify, mock_exist, + mock_extend, mock_get, + mock_create, mock_sync, + mock_detect_format): + mock_detect_format.return_value.safety_check.return_value = False + mock_sync.side_effect = lambda *a, **kw: self._fake_deco + mock_get.return_value = None + fn = mock.MagicMock() + mock_exist.side_effect = [False, True, False, True, True] + image = self.image_class(self.INSTANCE, self.NAME) + + self.assertRaises(exception.InvalidDiskInfo, + image.create_image, fn, self.TEMPLATE_PATH, + self.SIZE) + mock_verify.assert_not_called() def test_resolve_driver_format(self): image = self.image_class(self.INSTANCE, self.NAME) diff --git a/nova/tests/unit/virt/libvirt/test_migration.py b/nova/tests/unit/virt/libvirt/test_migration.py index f4e64fbe53e..70488f88cf4 100644 --- a/nova/tests/unit/virt/libvirt/test_migration.py +++ b/nova/tests/unit/virt/libvirt/test_migration.py @@ -28,6 +28,7 @@ from nova import test from nova.tests import fixtures as nova_fixtures from nova.tests.fixtures import libvirt as fakelibvirt +from nova.tests.unit.virt.libvirt import test_driver from nova.virt.libvirt import config as vconfig from nova.virt.libvirt import guest as libvirt_guest from nova.virt.libvirt import host @@ -80,16 +81,51 @@ def test_get_updated_guest_xml( get_volume_config = mock.MagicMock() mock_guest.get_xml_desc.return_value = '' - migration.get_updated_guest_xml( - mock.sentinel.instance, mock_guest, data, get_volume_config) + instance = objects.Instance(**test_driver._create_test_instance()) + migration.get_updated_guest_xml(instance, mock_guest, data, + get_volume_config) mock_graphics.assert_called_once_with(mock.ANY, data) mock_serial.assert_called_once_with(mock.ANY, data) mock_volume.assert_called_once_with( - mock.ANY, data, mock.sentinel.instance, get_volume_config) + mock.ANY, data, instance, get_volume_config) mock_perf_events_xml.assert_called_once_with(mock.ANY, data) mock_memory_backing.assert_called_once_with(mock.ANY, data) self.assertEqual(1, mock_tostring.called) + def test_update_quota_xml(self): + old_xml = """ + fake-instance + + 42 + 1337 + + """ + instance = objects.Instance(**test_driver._create_test_instance()) + new_xml = migration._update_quota_xml(instance, + etree.fromstring(old_xml)) + new_xml = etree.tostring(new_xml, encoding='unicode') + self.assertXmlEqual( + """ + fake-instance + + 1337 + + """, new_xml) + + def test_update_quota_xml_empty_cputune(self): + old_xml = """ + fake-instance + + 42 + + """ + instance = objects.Instance(**test_driver._create_test_instance()) + new_xml = migration._update_quota_xml(instance, + etree.fromstring(old_xml)) + new_xml = etree.tostring(new_xml, encoding='unicode') + self.assertXmlEqual('fake-instance', + new_xml) + def test_update_device_resources_xml_vpmem(self): # original xml for vpmems, /dev/dax0.1 and /dev/dax0.2 here # are vpmem device path on source host diff --git a/nova/tests/unit/virt/libvirt/test_utils.py b/nova/tests/unit/virt/libvirt/test_utils.py index 4e73c662c57..0d0fad30b33 100644 --- a/nova/tests/unit/virt/libvirt/test_utils.py +++ b/nova/tests/unit/virt/libvirt/test_utils.py @@ -29,6 +29,7 @@ from nova.compute import utils as compute_utils from nova import context from nova import exception +from nova.image import format_inspector from nova import objects from nova.objects import fields as obj_fields import nova.privsep.fs @@ -116,20 +117,87 @@ def test_create_image(self, mock_execute): @mock.patch('os.path.exists', return_value=True) @mock.patch('oslo_concurrency.processutils.execute') @mock.patch('nova.virt.images.qemu_img_info') - def test_create_cow_image(self, mock_info, mock_execute, mock_exists): + @mock.patch('nova.image.format_inspector.detect_file_format') + def _test_create_cow_image( + self, mock_detect, mock_info, mock_execute, + mock_exists, backing_file=None, safety_check=True + ): + if isinstance(backing_file, dict): + backing_info = backing_file + backing_file = backing_info.pop('file', None) + else: + backing_info = {} + backing_backing_file = backing_info.pop('backing_file', None) + backing_fmt = backing_info.pop('backing_fmt', + mock.sentinel.backing_fmt) + mock_execute.return_value = ('stdout', None) mock_info.return_value = mock.Mock( - file_format=mock.sentinel.backing_fmt, - cluster_size=mock.sentinel.cluster_size) + file_format=backing_fmt, + cluster_size=mock.sentinel.cluster_size, + backing_file=backing_backing_file, + format_specific=backing_info) + + mock_detect.return_value.safety_check.return_value = safety_check + libvirt_utils.create_cow_image(mock.sentinel.backing_path, mock.sentinel.new_path) mock_info.assert_called_once_with(mock.sentinel.backing_path) mock_execute.assert_has_calls([mock.call( 'qemu-img', 'create', '-f', 'qcow2', '-o', 'backing_file=%s,backing_fmt=%s,cluster_size=%s' % ( - mock.sentinel.backing_path, mock.sentinel.backing_fmt, + mock.sentinel.backing_path, backing_fmt, mock.sentinel.cluster_size), mock.sentinel.new_path)]) + if backing_file: + mock_detect.return_value.safety_check.assert_called_once_with() + + def test_create_image_qcow2(self): + self._test_create_cow_image() + + def test_create_image_backing_file(self): + self._test_create_cow_image( + backing_file=mock.sentinel.backing_file + ) + + def test_create_image_base_has_backing_file(self): + self.assertRaises( + exception.InvalidDiskInfo, + self._test_create_cow_image, + backing_file={'file': mock.sentinel.backing_file, + 'backing_file': mock.sentinel.backing_backing_file}, + ) + + def test_create_image_base_has_data_file(self): + self.assertRaises( + exception.InvalidDiskInfo, + self._test_create_cow_image, + backing_file={'file': mock.sentinel.backing_file, + 'backing_file': mock.sentinel.backing_backing_file, + 'data': {'data-file': mock.sentinel.data_file}}, + ) + + def test_create_image_size_none(self): + self._test_create_cow_image( + backing_file=mock.sentinel.backing_file, + ) + + def test_create_image_vmdk(self): + self._test_create_cow_image( + backing_file={'file': mock.sentinel.backing_file, + 'backing_fmt': 'vmdk', + 'backing_file': None, + 'data': {'create-type': 'monolithicSparse'}} + ) + + def test_create_image_vmdk_invalid_type(self): + self.assertRaises(exception.ImageUnacceptable, + self._test_create_cow_image, + backing_file={'file': mock.sentinel.backing_file, + 'backing_fmt': 'vmdk', + 'backing_file': None, + 'data': {'create-type': 'monolithicFlat'}} + ) @ddt.unpack @ddt.data({'fs_type': 'some_fs_type', @@ -321,11 +389,13 @@ def test_fetch_initrd_image(self, mock_images): mock_images.assert_called_once_with( _context, image_id, target, trusted_certs) + @mock.patch.object(images, 'IMAGE_API') + @mock.patch.object(format_inspector, 'detect_file_format') @mock.patch.object(compute_utils, 'disk_ops_semaphore') @mock.patch('nova.privsep.utils.supports_direct_io', return_value=True) @mock.patch('nova.privsep.qemu.unprivileged_convert_image') def test_fetch_raw_image(self, mock_convert_image, mock_direct_io, - mock_disk_op_sema): + mock_disk_op_sema, mock_detect, mock_glance): def fake_rename(old, new): self.executes.append(('mv', old, new)) @@ -336,7 +406,7 @@ def fake_unlink(path): def fake_rm_on_error(path, remove=None): self.executes.append(('rm', '-f', path)) - def fake_qemu_img_info(path): + def fake_qemu_img_info(path, format=None): class FakeImgInfo(object): pass @@ -354,6 +424,7 @@ class FakeImgInfo(object): FakeImgInfo.file_format = file_format FakeImgInfo.backing_file = backing_file FakeImgInfo.virtual_size = 1 + FakeImgInfo.format_specific = None if file_format == 'raw' else {} return FakeImgInfo() @@ -364,6 +435,8 @@ class FakeImgInfo(object): self.stub_out('oslo_utils.fileutils.delete_if_exists', fake_rm_on_error) + mock_inspector = mock_detect.return_value + # Since the remove param of fileutils.remove_path_on_error() # is initialized at load time, we must provide a wrapper # that explicitly resets it to our fake delete_if_exists() @@ -374,6 +447,10 @@ class FakeImgInfo(object): context = 'opaque context' image_id = '4' + # Make sure qcow2 gets converted to raw + mock_inspector.safety_check.return_value = True + mock_inspector.__str__.return_value = 'qcow2' + mock_glance.get.return_value = {'disk_format': 'qcow2'} target = 't.qcow2' self.executes = [] expected_commands = [('rm', 't.qcow2.part'), @@ -385,14 +462,46 @@ class FakeImgInfo(object): 't.qcow2.part', 't.qcow2.converted', 'qcow2', 'raw', CONF.instances_path, False) mock_convert_image.reset_mock() - + mock_inspector.safety_check.assert_called_once_with() + mock_detect.assert_called_once_with('t.qcow2.part') + + # Make sure raw does not get converted + mock_detect.reset_mock() + mock_inspector.safety_check.reset_mock() + mock_inspector.safety_check.return_value = True + mock_inspector.__str__.return_value = 'raw' + mock_glance.get.return_value = {'disk_format': 'raw'} target = 't.raw' self.executes = [] expected_commands = [('mv', 't.raw.part', 't.raw')] images.fetch_to_raw(context, image_id, target) self.assertEqual(self.executes, expected_commands) mock_convert_image.assert_not_called() - + mock_inspector.safety_check.assert_called_once_with() + mock_detect.assert_called_once_with('t.raw.part') + + # Make sure safety check failure prevents us from proceeding + mock_detect.reset_mock() + mock_inspector.safety_check.reset_mock() + mock_inspector.safety_check.return_value = False + mock_inspector.__str__.return_value = 'qcow2' + mock_glance.get.return_value = {'disk_format': 'qcow2'} + target = 'backing.qcow2' + self.executes = [] + expected_commands = [('rm', '-f', 'backing.qcow2.part')] + self.assertRaises(exception.ImageUnacceptable, + images.fetch_to_raw, context, image_id, target) + self.assertEqual(self.executes, expected_commands) + mock_convert_image.assert_not_called() + mock_inspector.safety_check.assert_called_once_with() + mock_detect.assert_called_once_with('backing.qcow2.part') + + # Make sure a format mismatch prevents us from proceeding + mock_detect.reset_mock() + mock_inspector.safety_check.reset_mock() + mock_inspector.safety_check.side_effect = ( + format_inspector.ImageFormatError) + mock_glance.get.return_value = {'disk_format': 'qcow2'} target = 'backing.qcow2' self.executes = [] expected_commands = [('rm', '-f', 'backing.qcow2.part')] @@ -400,6 +509,8 @@ class FakeImgInfo(object): images.fetch_to_raw, context, image_id, target) self.assertEqual(self.executes, expected_commands) mock_convert_image.assert_not_called() + mock_inspector.safety_check.assert_called_once_with() + mock_detect.assert_called_once_with('backing.qcow2.part') del self.executes diff --git a/nova/tests/unit/virt/libvirt/test_vif.py b/nova/tests/unit/virt/libvirt/test_vif.py index 43504efeb53..697300b9cfc 100644 --- a/nova/tests/unit/virt/libvirt/test_vif.py +++ b/nova/tests/unit/virt/libvirt/test_vif.py @@ -517,18 +517,17 @@ def setup_os_vif_objects(self): def setUp(self): super(LibvirtVifTestCase, self).setUp() - self.useFixture(nova_fixtures.LibvirtFixture(stub_os_vif=False)) + self.libvirt = self.useFixture( + nova_fixtures.LibvirtFixture(stub_os_vif=False)) # os_vif.initialize is typically done in nova-compute startup os_vif.initialize() self.setup_os_vif_objects() # multiqueue configuration is host OS specific - _a = mock.patch('os.uname') - self.mock_uname = _a.start() + self.mock_uname = self.libvirt.mock_uname self.mock_uname.return_value = fakelibvirt.os_uname( 'Linux', '', '5.10.13-200-generic', '', 'x86_64') - self.addCleanup(_a.stop) def _get_node(self, xml): doc = etree.fromstring(xml) @@ -983,14 +982,9 @@ def test_generic_driver_bridge(self): self.vif_bridge, self.vif_bridge['network']['bridge']) - @mock.patch.object(pci_utils, 'get_ifname_by_pci_address') - @mock.patch.object(pci_utils, 'get_vf_num_by_pci_address', return_value=1) - @mock.patch('nova.privsep.linux_net.set_device_macaddr') - @mock.patch('nova.privsep.linux_net.set_device_macaddr_and_vlan') - def _test_hw_veb_op(self, op, vlan, mock_set_macaddr_and_vlan, - mock_set_macaddr, mock_get_vf_num, - mock_get_ifname): - mock_get_ifname.side_effect = ['eth1', 'eth13'] + def _test_hw_veb_op(self, op, vlan): + self.libvirt.mock_get_vf_num_by_pci_address.return_value = 1 + pci_utils.get_ifname_by_pci_address.side_effect = ['eth1', 'eth13'] vlan_id = int(vlan) port_state = 'up' if vlan_id > 0 else 'down' mac = ('00:00:00:00:00:00' if op.__name__ == 'unplug' @@ -1005,10 +999,13 @@ def _test_hw_veb_op(self, op, vlan, mock_set_macaddr_and_vlan, 'set_macaddr': [mock.call('eth13', mac, port_state=port_state)] } op(self.instance, self.vif_hw_veb_macvtap) - mock_get_ifname.assert_has_calls(calls['get_ifname']) - mock_get_vf_num.assert_has_calls(calls['get_vf_num']) - mock_set_macaddr.assert_has_calls(calls['set_macaddr']) - mock_set_macaddr_and_vlan.assert_called_once_with( + pci_utils.get_ifname_by_pci_address.assert_has_calls( + calls['get_ifname']) + self.libvirt.mock_get_vf_num_by_pci_address.assert_has_calls( + calls['get_vf_num']) + self.libvirt.mock_set_device_macaddr.assert_has_calls( + calls['set_macaddr']) + self.libvirt.mock_set_device_macaddr_and_vlan.assert_called_once_with( 'eth1', 1, mock.ANY, vlan_id) def test_plug_hw_veb(self): @@ -1218,9 +1215,8 @@ def test_hostdev_physical_driver(self): self.assertEqual(1, len(node)) self._assertPciEqual(node, self.vif_hostdev_physical) - @mock.patch.object(pci_utils, 'get_ifname_by_pci_address', - return_value='eth1') - def test_hw_veb_driver_macvtap(self, mock_get_ifname): + def test_hw_veb_driver_macvtap(self): + pci_utils.get_ifname_by_pci_address.return_value = 'eth1' d = vif.LibvirtGenericVIFDriver() xml = self._get_instance_xml(d, self.vif_hw_veb_macvtap) node = self._get_node(xml) diff --git a/nova/tests/unit/virt/libvirt/volume/test_fibrechannel.py b/nova/tests/unit/virt/libvirt/volume/test_fibrechannel.py index 89a59f2f1ab..f0d403e3004 100644 --- a/nova/tests/unit/virt/libvirt/volume/test_fibrechannel.py +++ b/nova/tests/unit/virt/libvirt/volume/test_fibrechannel.py @@ -81,3 +81,23 @@ def test_extend_volume(self): self.assertEqual(requested_size, new_size) libvirt_driver.connector.extend_volume.assert_called_once_with( connection_info['data']) + + def test_disconnect_volume(self): + device_path = '/dev/fake-dev' + connection_info = {'data': {'device_path': device_path}} + + libvirt_driver = fibrechannel.LibvirtFibreChannelVolumeDriver( + self.fake_host) + libvirt_driver.connector.disconnect_volume = mock.MagicMock() + libvirt_driver.disconnect_volume( + connection_info, mock.sentinel.instance) + + libvirt_driver.connector.disconnect_volume.assert_called_once_with( + connection_info['data'], connection_info['data'], force=False) + + # Verify force=True + libvirt_driver.connector.disconnect_volume.reset_mock() + libvirt_driver.disconnect_volume( + connection_info, mock.sentinel.instance, force=True) + libvirt_driver.connector.disconnect_volume.assert_called_once_with( + connection_info['data'], connection_info['data'], force=True) diff --git a/nova/tests/unit/virt/libvirt/volume/test_iscsi.py b/nova/tests/unit/virt/libvirt/volume/test_iscsi.py index f8a64abea5f..540c9c822d0 100644 --- a/nova/tests/unit/virt/libvirt/volume/test_iscsi.py +++ b/nova/tests/unit/virt/libvirt/volume/test_iscsi.py @@ -57,10 +57,19 @@ def test_libvirt_iscsi_driver_disconnect_volume_with_devicenotfound(self, device=device_path)) libvirt_driver.disconnect_volume(connection_info, mock.sentinel.instance) + libvirt_driver.connector.disconnect_volume.assert_called_once_with( + connection_info['data'], None, force=False) msg = mock_LOG_warning.call_args_list[0] self.assertIn('Ignoring VolumeDeviceNotFound', msg[0][0]) + # Verify force=True + libvirt_driver.connector.disconnect_volume.reset_mock() + libvirt_driver.disconnect_volume( + connection_info, mock.sentinel.instance, force=True) + libvirt_driver.connector.disconnect_volume.assert_called_once_with( + connection_info['data'], None, force=True) + def test_extend_volume(self): device_path = '/dev/fake-dev' connection_info = {'data': {'device_path': device_path}} diff --git a/nova/tests/unit/virt/libvirt/volume/test_lightos.py b/nova/tests/unit/virt/libvirt/volume/test_lightos.py index 554647acf40..1eb9583d4cf 100644 --- a/nova/tests/unit/virt/libvirt/volume/test_lightos.py +++ b/nova/tests/unit/virt/libvirt/volume/test_lightos.py @@ -30,7 +30,7 @@ def test_libvirt_lightos_driver(self, mock_factory, mock_helper): device_scan_attempts=5) @mock.patch('os_brick.initiator.connector.InitiatorConnector.factory', - new=mock.Mock(return_value=mock.Mock())) + new=mock.Mock()) def test_libvirt_lightos_driver_connect(self): lightos_driver = lightos.LibvirtLightOSVolumeDriver( self.fake_host) @@ -40,15 +40,16 @@ def test_libvirt_lightos_driver_connect(self): 'name': 'aLightVolume', 'conf': config} connection_info = {'data': disk_info} - with mock.patch.object(lightos_driver.connector, - 'connect_volume', - return_value={'path': '/dev/dms1234567'}): - lightos_driver.connect_volume(connection_info, None) - (lightos_driver.connector.connect_volume. - assert_called_once_with( - connection_info['data'])) - self.assertEqual('/dev/dms1234567', - connection_info['data']['device_path']) + lightos_driver.connector.connect_volume.return_value = ( + {'path': '/dev/dms1234567'}) + + lightos_driver.connect_volume(connection_info, None) + + lightos_driver.connector.connect_volume.assert_called_once_with( + connection_info['data']) + self.assertEqual( + '/dev/dms1234567', + connection_info['data']['device_path']) @mock.patch('os_brick.initiator.connector.InitiatorConnector.factory', new=mock.Mock(return_value=mock.Mock())) @@ -61,7 +62,13 @@ def test_libvirt_lightos_driver_disconnect(self): connection_info = {'data': disk_info} lightos_driver.disconnect_volume(connection_info, None) lightos_driver.connector.disconnect_volume.assert_called_once_with( - disk_info, None) + disk_info, None, force=False) + + # Verify force=True + lightos_driver.connector.disconnect_volume.reset_mock() + lightos_driver.disconnect_volume(connection_info, None, force=True) + lightos_driver.connector.disconnect_volume.assert_called_once_with( + disk_info, None, force=True) @mock.patch('os_brick.initiator.connector.InitiatorConnector.factory', new=mock.Mock(return_value=mock.Mock())) diff --git a/nova/tests/unit/virt/libvirt/volume/test_nvme.py b/nova/tests/unit/virt/libvirt/volume/test_nvme.py index fcb303b4c3e..2803903e9fc 100644 --- a/nova/tests/unit/virt/libvirt/volume/test_nvme.py +++ b/nova/tests/unit/virt/libvirt/volume/test_nvme.py @@ -56,14 +56,15 @@ def test_libvirt_nvme_driver_connect(self): 'name': 'aNVMEVolume', 'conf': config} connection_info = {'data': disk_info} - with mock.patch.object(nvme_driver.connector, - 'connect_volume', - return_value={'path': '/dev/dms1234567'}): - nvme_driver.connect_volume(connection_info, None) - nvme_driver.connector.connect_volume.assert_called_once_with( - connection_info['data']) - self.assertEqual('/dev/dms1234567', - connection_info['data']['device_path']) + nvme_driver.connector.connect_volume.return_value = ( + {'path': '/dev/dms1234567'}) + + nvme_driver.connect_volume(connection_info, None) + + nvme_driver.connector.connect_volume.assert_called_once_with( + connection_info['data']) + self.assertEqual( + '/dev/dms1234567', connection_info['data']['device_path']) @mock.patch('os_brick.initiator.connector.InitiatorConnector.factory', new=mock.Mock(return_value=mock.Mock())) @@ -76,7 +77,13 @@ def test_libvirt_nvme_driver_disconnect(self): connection_info = {'data': disk_info} nvme_driver.disconnect_volume(connection_info, None) nvme_driver.connector.disconnect_volume.assert_called_once_with( - disk_info, None) + disk_info, None, force=False) + + # Verify force=True + nvme_driver.connector.disconnect_volume.reset_mock() + nvme_driver.disconnect_volume(connection_info, None, force=True) + nvme_driver.connector.disconnect_volume.assert_called_once_with( + disk_info, None, force=True) @mock.patch('os_brick.initiator.connector.InitiatorConnector.factory', new=mock.Mock(return_value=mock.Mock())) diff --git a/nova/tests/unit/virt/libvirt/volume/test_scaleio.py b/nova/tests/unit/virt/libvirt/volume/test_scaleio.py index 6d9247cd2d9..ed5ab08a6e6 100644 --- a/nova/tests/unit/virt/libvirt/volume/test_scaleio.py +++ b/nova/tests/unit/virt/libvirt/volume/test_scaleio.py @@ -49,7 +49,13 @@ def test_libvirt_scaleio_driver_disconnect(self): conn = {'data': mock.sentinel.conn_data} sio.disconnect_volume(conn, mock.sentinel.instance) sio.connector.disconnect_volume.assert_called_once_with( - mock.sentinel.conn_data, None) + mock.sentinel.conn_data, None, force=False) + + # Verify force=True + sio.connector.disconnect_volume.reset_mock() + sio.disconnect_volume(conn, mock.sentinel.instance, force=True) + sio.connector.disconnect_volume.assert_called_once_with( + mock.sentinel.conn_data, None, force=True) @mock.patch('os_brick.initiator.connector.InitiatorConnector.factory', new=mock.Mock(return_value=mock.Mock())) diff --git a/nova/tests/unit/virt/libvirt/volume/test_storpool.py b/nova/tests/unit/virt/libvirt/volume/test_storpool.py index e14954f1487..9ceac072602 100644 --- a/nova/tests/unit/virt/libvirt/volume/test_storpool.py +++ b/nova/tests/unit/virt/libvirt/volume/test_storpool.py @@ -53,9 +53,11 @@ def connect_volume(self, connection_info): } return {'type': 'block', 'path': test_attached[v]['path']} - def disconnect_volume(self, connection_info, device_info): + def disconnect_volume(self, connection_info, device_info, **kwargs): self.inst.assertIn('client_id', connection_info) self.inst.assertIn('volume', connection_info) + self.inst.assertIn('force', kwargs) + self.inst.assertEqual(self.inst.force, kwargs.get('force')) v = connection_info['volume'] if v not in test_attached: @@ -86,6 +88,11 @@ def factory(self, proto, helper): class LibvirtStorPoolVolumeDriverTestCase( test_volume.LibvirtVolumeBaseTestCase): + def setUp(self): + super().setUp() + # This is for testing the force flag of disconnect_volume() + self.force = False + def mock_storpool(f): def _config_inner_inner1(inst, *args, **kwargs): @mock.patch( @@ -175,3 +182,10 @@ def test_storpool_attach_detach_extend(self): libvirt_driver.disconnect_volume(ci_2, mock.sentinel.instance) self.assertDictEqual({}, test_attached) + + # Connect the volume again so we can detach it again + libvirt_driver.connect_volume(ci_2, mock.sentinel.instance) + # Verify force=True + self.force = True + libvirt_driver.disconnect_volume( + ci_2, mock.sentinel.instance, force=True) diff --git a/nova/tests/unit/virt/libvirt/volume/test_vzstorage.py b/nova/tests/unit/virt/libvirt/volume/test_vzstorage.py index 883cebb55a1..032ceb4fe59 100644 --- a/nova/tests/unit/virt/libvirt/volume/test_vzstorage.py +++ b/nova/tests/unit/virt/libvirt/volume/test_vzstorage.py @@ -95,7 +95,13 @@ def test_libvirt_vzstorage_driver_disconnect(self): conn = {'data': mock.sentinel.conn_data} drv.disconnect_volume(conn, mock.sentinel.instance) drv.connector.disconnect_volume.assert_called_once_with( - mock.sentinel.conn_data, None) + mock.sentinel.conn_data, None, force=False) + + # Verify force=True + drv.connector.disconnect_volume.reset_mock() + drv.disconnect_volume(conn, mock.sentinel.instance, force=True) + drv.connector.disconnect_volume.assert_called_once_with( + mock.sentinel.conn_data, None, force=True) def test_libvirt_vzstorage_driver_get_config(self): libvirt_driver = vzstorage.LibvirtVZStorageVolumeDriver(self.fake_host) diff --git a/nova/tests/unit/virt/test_block_device.py b/nova/tests/unit/virt/test_block_device.py index aff6c5ef199..94d9297ca35 100644 --- a/nova/tests/unit/virt/test_block_device.py +++ b/nova/tests/unit/virt/test_block_device.py @@ -433,24 +433,23 @@ def test_driver_blank_block_device(self): def _test_call_wait_func(self, delete_on_termination, delete_fail=False): test_bdm = self.driver_classes['volume'](self.volume_bdm) test_bdm['delete_on_termination'] = delete_on_termination - with mock.patch.object(self.volume_api, 'delete') as vol_delete: - wait_func = mock.MagicMock() - mock_exception = exception.VolumeNotCreated(volume_id='fake-id', - seconds=1, - attempts=1, - volume_status='error') - wait_func.side_effect = mock_exception - - if delete_on_termination and delete_fail: - vol_delete.side_effect = Exception() - - self.assertRaises(exception.VolumeNotCreated, - test_bdm._call_wait_func, - context=self.context, - wait_func=wait_func, - volume_api=self.volume_api, - volume_id='fake-id') - self.assertEqual(delete_on_termination, vol_delete.called) + if delete_on_termination and delete_fail: + self.volume_api.delete.side_effect = Exception() + + wait_func = mock.MagicMock() + mock_exception = exception.VolumeNotCreated(volume_id='fake-id', + seconds=1, + attempts=1, + volume_status='error') + wait_func.side_effect = mock_exception + + self.assertRaises(exception.VolumeNotCreated, + test_bdm._call_wait_func, + context=self.context, + wait_func=wait_func, + volume_api=self.volume_api, + volume_id='fake-id') + self.assertEqual(delete_on_termination, self.volume_api.delete.called) def test_call_wait_delete_volume(self): self._test_call_wait_func(True) @@ -483,25 +482,24 @@ def test_volume_delete_attachment( volume['shared_targets'] = True volume['service_uuid'] = uuids.service_uuid + if delete_attachment_raises: + self.volume_api.attachment_delete.side_effect = ( + delete_attachment_raises) + + self.virt_driver.get_volume_connector.return_value = connector + with test.nested( mock.patch.object(driver_bdm, '_get_volume', return_value=volume), - mock.patch.object(self.virt_driver, 'get_volume_connector', - return_value=connector), mock.patch('os_brick.initiator.utils.guard_connection'), - mock.patch.object(self.volume_api, 'attachment_delete'), - ) as (mock_get_volume, mock_get_connector, mock_guard, - vapi_attach_del): - - if delete_attachment_raises: - vapi_attach_del.side_effect = delete_attachment_raises + ) as (mock_get_volume, mock_guard): driver_bdm.detach(elevated_context, instance, self.volume_api, self.virt_driver, attachment_id=attachment_id) mock_guard.assert_called_once_with(volume) - vapi_attach_del.assert_called_once_with(elevated_context, - attachment_id) + self.volume_api.attachment_delete.assert_called_once_with( + elevated_context, attachment_id) def test_volume_delete_attachment_with_shared_targets(self): self.test_volume_delete_attachment(include_shared_targets=True) @@ -952,31 +950,28 @@ def test_snapshot_attach_fail_volume(self): instance = fake_instance.fake_instance_obj(mock.sentinel.ctx, **{'uuid': uuids.uuid}) - with test.nested( - mock.patch.object(self.volume_api, 'get_snapshot', - return_value=snapshot), - mock.patch.object(self.volume_api, 'create', return_value=volume), - mock.patch.object(self.volume_api, 'delete'), - ) as (vol_get_snap, vol_create, vol_delete): - wait_func = mock.MagicMock() - mock_exception = exception.VolumeNotCreated(volume_id=volume['id'], - seconds=1, - attempts=1, - volume_status='error') - wait_func.side_effect = mock_exception - self.assertRaises(exception.VolumeNotCreated, - test_bdm.attach, context=self.context, - instance=instance, - volume_api=self.volume_api, - virt_driver=self.virt_driver, - wait_func=wait_func) - - vol_get_snap.assert_called_once_with( - self.context, 'fake-snapshot-id-1') - vol_create.assert_called_once_with( - self.context, 3, '', '', availability_zone=None, - snapshot=snapshot, volume_type=None) - vol_delete.assert_called_once_with(self.context, volume['id']) + self.volume_api.get_snapshot.return_value = snapshot + self.volume_api.create.return_value = volume + wait_func = mock.MagicMock() + mock_exception = exception.VolumeNotCreated(volume_id=volume['id'], + seconds=1, + attempts=1, + volume_status='error') + wait_func.side_effect = mock_exception + self.assertRaises(exception.VolumeNotCreated, + test_bdm.attach, context=self.context, + instance=instance, + volume_api=self.volume_api, + virt_driver=self.virt_driver, + wait_func=wait_func) + + self.volume_api.get_snapshot.assert_called_once_with( + self.context, 'fake-snapshot-id-1') + self.volume_api.create.assert_called_once_with( + self.context, 3, '', '', availability_zone=None, + snapshot=snapshot, volume_type=None) + self.volume_api.delete.assert_called_once_with( + self.context, volume['id']) def test_snapshot_attach_volume(self): test_bdm = self.driver_classes['volsnapshot']( @@ -984,19 +979,17 @@ def test_snapshot_attach_volume(self): instance = {'id': 'fake_id', 'uuid': uuids.uuid} - with test.nested( - mock.patch.object(self.driver_classes['volume'], 'attach'), - mock.patch.object(self.volume_api, 'get_snapshot'), - mock.patch.object(self.volume_api, 'create'), - ) as (mock_attach, mock_get_snapshot, mock_create): + with mock.patch.object( + self.driver_classes['volume'], 'attach' + ) as mock_attach: test_bdm.attach(self.context, instance, self.volume_api, self.virt_driver) self.assertEqual('fake-volume-id-2', test_bdm.volume_id) mock_attach.assert_called_once_with( self.context, instance, self.volume_api, self.virt_driver) # Make sure theses are not called - mock_get_snapshot.assert_not_called() - mock_create.assert_not_called() + self.volume_api.get_snapshot.assert_not_called() + self.volume_api.create.assert_not_called() def test_snapshot_attach_no_volume_and_no_volume_type(self): bdm = self.driver_classes['volsnapshot'](self.volsnapshot_bdm) @@ -1006,15 +999,10 @@ def test_snapshot_attach_no_volume_and_no_volume_type(self): original_volume = {'id': uuids.original_volume_id, 'volume_type_id': 'original_volume_type'} new_volume = {'id': uuids.new_volume_id} - with test.nested( - mock.patch.object(self.driver_classes['volume'], 'attach'), - mock.patch.object(self.volume_api, 'get_snapshot', - return_value=snapshot), - mock.patch.object(self.volume_api, 'get', - return_value=original_volume), - mock.patch.object(self.volume_api, 'create', - return_value=new_volume), - ) as (mock_attach, mock_get_snapshot, mock_get, mock_create): + self.volume_api.get_snapshot.return_value = snapshot + self.volume_api.get.return_value = original_volume + self.volume_api.create.return_value = new_volume + with mock.patch.object(self.driver_classes["volume"], "attach"): bdm.volume_id = None bdm.volume_type = None bdm.attach(self.context, instance, self.volume_api, @@ -1022,10 +1010,11 @@ def test_snapshot_attach_no_volume_and_no_volume_type(self): # Assert that the original volume type is fetched, stored within # the bdm and then used to create the new snapshot based volume. - mock_get.assert_called_once_with(self.context, - uuids.original_volume_id) + self.volume_api.get.assert_called_once_with( + self.context, uuids.original_volume_id) self.assertEqual('original_volume_type', bdm.volume_type) - mock_create.assert_called_once_with(self.context, bdm.volume_size, + self.volume_api.create.assert_called_once_with( + self.context, bdm.volume_size, '', '', volume_type='original_volume_type', snapshot=snapshot, availability_zone=None) @@ -1097,27 +1086,25 @@ def test_image_attach_fail_volume(self): instance = fake_instance.fake_instance_obj(mock.sentinel.ctx, **{'uuid': uuids.uuid}) - with test.nested( - mock.patch.object(self.volume_api, 'create', return_value=volume), - mock.patch.object(self.volume_api, 'delete'), - ) as (vol_create, vol_delete): - wait_func = mock.MagicMock() - mock_exception = exception.VolumeNotCreated(volume_id=volume['id'], - seconds=1, - attempts=1, - volume_status='error') - wait_func.side_effect = mock_exception - self.assertRaises(exception.VolumeNotCreated, - test_bdm.attach, context=self.context, - instance=instance, - volume_api=self.volume_api, - virt_driver=self.virt_driver, - wait_func=wait_func) - - vol_create.assert_called_once_with( - self.context, 1, '', '', image_id=image['id'], - availability_zone=None, volume_type=None) - vol_delete.assert_called_once_with(self.context, volume['id']) + self.volume_api.create.return_value = volume + wait_func = mock.MagicMock() + mock_exception = exception.VolumeNotCreated(volume_id=volume['id'], + seconds=1, + attempts=1, + volume_status='error') + wait_func.side_effect = mock_exception + self.assertRaises(exception.VolumeNotCreated, + test_bdm.attach, context=self.context, + instance=instance, + volume_api=self.volume_api, + virt_driver=self.virt_driver, + wait_func=wait_func) + + self.volume_api.create.assert_called_once_with( + self.context, 1, '', '', image_id=image['id'], + availability_zone=None, volume_type=None) + self.volume_api.delete.assert_called_once_with( + self.context, volume['id']) def test_image_attach_volume(self): test_bdm = self.driver_classes['volimage']( @@ -1125,19 +1112,17 @@ def test_image_attach_volume(self): instance = {'id': 'fake_id', 'uuid': uuids.uuid} - with test.nested( - mock.patch.object(self.driver_classes['volume'], 'attach'), - mock.patch.object(self.volume_api, 'get_snapshot'), - mock.patch.object(self.volume_api, 'create'), - ) as (mock_attch, mock_get_snapshot, mock_create): + with mock.patch.object( + self.driver_classes['volume'], 'attach' + ) as mock_attach: test_bdm.attach(self.context, instance, self.volume_api, self.virt_driver) self.assertEqual('fake-volume-id-2', test_bdm.volume_id) - mock_attch.assert_called_once_with( + mock_attach.assert_called_once_with( self.context, instance, self.volume_api, self.virt_driver) # Make sure theses are not called - mock_get_snapshot.assert_not_called() - mock_create.assert_not_called() + self.volume_api.get_snapshot.assert_not_called() + self.volume_api.create.assert_not_called() def test_blank_attach_fail_volume(self): no_blank_volume = self.volblank_bdm_dict.copy() @@ -1149,30 +1134,26 @@ def test_blank_attach_fail_volume(self): **{'uuid': uuids.uuid}) volume = {'id': 'fake-volume-id-2', 'display_name': '%s-blank-vol' % uuids.uuid} + self.volume_api.create.return_value = volume + wait_func = mock.MagicMock() + mock_exception = exception.VolumeNotCreated(volume_id=volume['id'], + seconds=1, + attempts=1, + volume_status='error') + wait_func.side_effect = mock_exception + self.assertRaises(exception.VolumeNotCreated, + test_bdm.attach, context=self.context, + instance=instance, + volume_api=self.volume_api, + virt_driver=self.virt_driver, + wait_func=wait_func) - with test.nested( - mock.patch.object(self.volume_api, 'create', return_value=volume), - mock.patch.object(self.volume_api, 'delete'), - ) as (vol_create, vol_delete): - wait_func = mock.MagicMock() - mock_exception = exception.VolumeNotCreated(volume_id=volume['id'], - seconds=1, - attempts=1, - volume_status='error') - wait_func.side_effect = mock_exception - self.assertRaises(exception.VolumeNotCreated, - test_bdm.attach, context=self.context, - instance=instance, - volume_api=self.volume_api, - virt_driver=self.virt_driver, - wait_func=wait_func) - - vol_create.assert_called_once_with( - self.context, test_bdm.volume_size, - '%s-blank-vol' % uuids.uuid, - '', volume_type=None, availability_zone=None) - vol_delete.assert_called_once_with( - self.context, volume['id']) + self.volume_api.create.assert_called_once_with( + self.context, test_bdm.volume_size, + '%s-blank-vol' % uuids.uuid, + '', volume_type=None, availability_zone=None) + self.volume_api.delete.assert_called_once_with( + self.context, volume['id']) def test_blank_attach_volume(self): no_blank_volume = self.volblank_bdm_dict.copy() @@ -1481,13 +1462,9 @@ def _test_boot_from_volume_source_snapshot_volume_type( 'display_name': 'fake-snapshot-vol'} self.stub_volume_create(volume) - with test.nested( - mock.patch.object(self.volume_api, 'get_snapshot', - return_value=snapshot), - mock.patch.object(volume_class, 'attach') - ) as ( - vol_get_snap, vol_attach - ): + self.volume_api.get_snapshot.return_value = snapshot + + with mock.patch.object(volume_class, 'attach') as vol_attach: test_bdm.attach(self.context, instance, self.volume_api, self.virt_driver) diff --git a/nova/tests/unit/virt/test_images.py b/nova/tests/unit/virt/test_images.py index 085b169db3c..0f5cb7c1928 100644 --- a/nova/tests/unit/virt/test_images.py +++ b/nova/tests/unit/virt/test_images.py @@ -16,6 +16,8 @@ import mock from oslo_concurrency import processutils +from oslo_serialization import jsonutils +from oslo_utils import imageutils from nova.compute import utils as compute_utils from nova import exception @@ -97,11 +99,18 @@ def test_qemu_img_info_with_disk_not_found(self, exists, mocked_execute): exists.assert_called_once_with(path) mocked_execute.assert_called_once() + @mock.patch.object(images, 'IMAGE_API') + @mock.patch('nova.image.format_inspector.detect_file_format') @mock.patch.object(images, 'convert_image', side_effect=exception.ImageUnacceptable) @mock.patch.object(images, 'qemu_img_info') @mock.patch.object(images, 'fetch') - def test_fetch_to_raw_errors(self, convert_image, qemu_img_info, fetch): + def test_fetch_to_raw_errors(self, convert_image, qemu_img_info, fetch, + mock_detect, glance): + inspector = mock_detect.return_value + inspector.safety_check.return_value = True + inspector.__str__.return_value = 'qcow2' + glance.get.return_value = {'disk_format': 'qcow2'} qemu_img_info.backing_file = None qemu_img_info.file_format = 'qcow2' qemu_img_info.virtual_size = 20 @@ -110,6 +119,48 @@ def test_fetch_to_raw_errors(self, convert_image, qemu_img_info, fetch): images.fetch_to_raw, None, 'href123', '/no/path') + @mock.patch.object(images, 'IMAGE_API') + @mock.patch('nova.image.format_inspector.detect_file_format') + @mock.patch.object(images, 'convert_image', + side_effect=exception.ImageUnacceptable) + @mock.patch.object(images, 'qemu_img_info') + @mock.patch.object(images, 'fetch') + def test_fetch_to_raw_data_file(self, convert_image, qemu_img_info_fn, + fetch, mock_detect, mock_glance): + mock_glance.get.return_value = {'disk_format': 'qcow2'} + inspector = mock_detect.return_value + inspector.safety_check.return_value = True + inspector.__str__.return_value = 'qcow2' + # NOTE(danms): the above test needs the following line as well, as it + # is broken without it. + qemu_img_info = qemu_img_info_fn.return_value + qemu_img_info.backing_file = None + qemu_img_info.file_format = 'qcow2' + qemu_img_info.virtual_size = 20 + qemu_img_info.format_specific = {'data': {'data-file': 'somefile'}} + self.assertRaisesRegex(exception.ImageUnacceptable, + 'Image href123 is unacceptable.*somefile', + images.fetch_to_raw, + None, 'href123', '/no/path') + + @mock.patch('nova.image.format_inspector.detect_file_format') + @mock.patch.object(images, 'IMAGE_API') + @mock.patch('os.rename') + @mock.patch.object(images, 'qemu_img_info') + @mock.patch.object(images, 'fetch') + def test_fetch_to_raw_from_raw(self, fetch, qemu_img_info_fn, mock_rename, + mock_glance, mock_detect): + # Make sure we support a case where we fetch an already-raw image and + # qemu-img returns None for "format_specific". + mock_glance.get.return_value = {'disk_format': 'raw'} + mock_detect.return_value.__str__.return_value = 'raw' + qemu_img_info = qemu_img_info_fn.return_value + qemu_img_info.file_format = 'raw' + qemu_img_info.backing_file = None + qemu_img_info.format_specific = None + images.fetch_to_raw(None, 'href123', '/no/path') + mock_rename.assert_called_once_with('/no/path.part', '/no/path') + @mock.patch.object(compute_utils, 'disk_ops_semaphore') @mock.patch('nova.privsep.utils.supports_direct_io', return_value=True) @mock.patch('oslo_concurrency.processutils.execute') @@ -135,3 +186,199 @@ def test_convert_image_without_direct_io_support(self, mock_execute, '-O', 'out_format', '-f', 'in_format', 'source', 'dest') mock_disk_op_sema.__enter__.assert_called_once() self.assertTupleEqual(expected, mock_execute.call_args[0]) + + def test_convert_image_vmdk_allowed_list_checking(self): + info = {'format': 'vmdk', + 'format-specific': { + 'type': 'vmdk', + 'data': { + 'create-type': 'monolithicFlat', + }}} + + # If the format is not in the allowed list, we should get an error + self.assertRaises(exception.ImageUnacceptable, + images.check_vmdk_image, 'foo', + imageutils.QemuImgInfo(jsonutils.dumps(info), + format='json')) + + # With the format in the allowed list, no error + self.flags(vmdk_allowed_types=['streamOptimized', 'monolithicFlat', + 'monolithicSparse'], + group='compute') + images.check_vmdk_image('foo', + imageutils.QemuImgInfo(jsonutils.dumps(info), + format='json')) + + # With an empty list, allow nothing + self.flags(vmdk_allowed_types=[], group='compute') + self.assertRaises(exception.ImageUnacceptable, + images.check_vmdk_image, 'foo', + imageutils.QemuImgInfo(jsonutils.dumps(info), + format='json')) + + @mock.patch.object(images, 'IMAGE_API') + @mock.patch('nova.image.format_inspector.detect_file_format') + @mock.patch.object(images, 'fetch') + @mock.patch('nova.privsep.qemu.unprivileged_qemu_img_info') + def test_fetch_checks_vmdk_rules(self, mock_info, mock_fetch, mock_detect, + mock_glance): + mock_glance.get.return_value = {'disk_format': 'vmdk'} + inspector = mock_detect.return_value + inspector.safety_check.return_value = True + inspector.__str__.return_value = 'vmdk' + info = {'format': 'vmdk', + 'format-specific': { + 'type': 'vmdk', + 'data': { + 'create-type': 'monolithicFlat', + }}} + mock_info.return_value = jsonutils.dumps(info) + with mock.patch('os.path.exists', return_value=True): + e = self.assertRaises(exception.ImageUnacceptable, + images.fetch_to_raw, None, 'foo', 'anypath') + self.assertIn('Invalid VMDK create-type specified', str(e)) + + @mock.patch('os.rename') + @mock.patch.object(images, 'IMAGE_API') + @mock.patch('nova.image.format_inspector.get_inspector') + @mock.patch('nova.image.format_inspector.detect_file_format') + @mock.patch.object(images, 'fetch') + @mock.patch('nova.privsep.qemu.unprivileged_qemu_img_info') + def test_fetch_iso_is_raw( + self, mock_info, mock_fetch, mock_detect_file_format, mock_gi, + mock_glance, mock_rename): + mock_glance.get.return_value = {'disk_format': 'iso'} + inspector = mock_gi.return_value.from_file.return_value + inspector.safety_check.return_value = True + inspector.__str__.return_value = 'iso' + mock_detect_file_format.return_value = inspector + # qemu-img does not have a parser for iso so it is treated as raw + info = { + "virtual-size": 356352, + "filename": "foo.iso", + "format": "raw", + "actual-size": 356352, + "dirty-flag": False + } + mock_info.return_value = jsonutils.dumps(info) + with mock.patch('os.path.exists', return_value=True): + images.fetch_to_raw(None, 'foo', 'anypath') + # Make sure we called info with -f raw for an iso, since qemu-img does + # not support iso + mock_info.assert_called_once_with('anypath.part', format=None) + # Make sure that since we considered this to be a raw file, we did the + # just-rename-don't-convert path + mock_rename.assert_called_once_with('anypath.part', 'anypath') + + @mock.patch.object(images, 'IMAGE_API') + @mock.patch('nova.image.format_inspector.detect_file_format') + @mock.patch.object(images, 'qemu_img_info') + @mock.patch.object(images, 'fetch') + def test_fetch_to_raw_inspector(self, fetch, qemu_img_info, mock_detect, + mock_glance): + # Image claims to be qcow2, is qcow2, but fails safety check, so we + # abort before qemu-img-info + mock_glance.get.return_value = {'disk_format': 'qcow2'} + inspector = mock_detect.return_value + inspector.safety_check.return_value = False + inspector.__str__.return_value = 'qcow2' + self.assertRaises(exception.ImageUnacceptable, + images.fetch_to_raw, None, 'href123', '/no.path') + qemu_img_info.assert_not_called() + mock_detect.assert_called_once_with('/no.path.part') + inspector.safety_check.assert_called_once_with() + mock_glance.get.assert_called_once_with(None, 'href123') + + # Image claims to be qcow2, is qcow2, passes safety check, so we make + # it all the way to qemu-img-info + inspector.safety_check.return_value = True + qemu_img_info.side_effect = test.TestingException + self.assertRaises(test.TestingException, + images.fetch_to_raw, None, 'href123', '/no.path') + + # Image claims to be qcow2 in glance, but the image is something else, + # so we abort before qemu-img-info + qemu_img_info.reset_mock() + mock_detect.reset_mock() + inspector.safety_check.reset_mock() + mock_detect.return_value.__str__.return_value = 'vmdk' + self.assertRaises(exception.ImageUnacceptable, + images.fetch_to_raw, None, 'href123', '/no.path') + mock_detect.assert_called_once_with('/no.path.part') + inspector.safety_check.assert_called_once_with() + qemu_img_info.assert_not_called() + + @mock.patch.object(images, 'IMAGE_API') + @mock.patch('nova.image.format_inspector.detect_file_format') + @mock.patch.object(images, 'qemu_img_info') + @mock.patch.object(images, 'fetch') + def test_fetch_to_raw_inspector_disabled(self, fetch, qemu_img_info, + mock_gi, mock_glance): + self.flags(disable_deep_image_inspection=True, + group='workarounds') + qemu_img_info.side_effect = test.TestingException + self.assertRaises(test.TestingException, + images.fetch_to_raw, None, 'href123', '/no.path') + # If deep inspection is disabled, we should never call the inspector + mock_gi.assert_not_called() + # ... and we let qemu-img detect the format itself. + qemu_img_info.assert_called_once_with('/no.path.part') + mock_glance.get.assert_not_called() + + @mock.patch.object(images, 'IMAGE_API') + @mock.patch.object(images, 'qemu_img_info') + @mock.patch('nova.image.format_inspector.detect_file_format') + def test_fetch_inspect_ami(self, detect, imginfo, glance): + glance.get.return_value = {'disk_format': 'ami'} + detect.return_value.__str__.return_value = 'raw' + self.assertRaises(exception.ImageUnacceptable, + images.fetch_to_raw, None, 'href123', '/no.path') + # Make sure 'ami was translated into 'raw' before we call qemu-img + imginfo.assert_called_once_with('/no.path.part') + + @mock.patch.object(images, 'IMAGE_API') + @mock.patch.object(images, 'qemu_img_info') + @mock.patch('nova.image.format_inspector.detect_file_format') + def test_fetch_inspect_aki(self, detect, imginfo, glance): + glance.get.return_value = {'disk_format': 'aki'} + detect.return_value.__str__.return_value = 'raw' + self.assertRaises(exception.ImageUnacceptable, + images.fetch_to_raw, None, 'href123', '/no.path') + # Make sure 'aki was translated into 'raw' before we call qemu-img + imginfo.assert_called_once_with('/no.path.part') + + @mock.patch.object(images, 'IMAGE_API') + @mock.patch.object(images, 'qemu_img_info') + @mock.patch('nova.image.format_inspector.detect_file_format') + def test_fetch_inspect_ari(self, detect, imginfo, glance): + glance.get.return_value = {'disk_format': 'ari'} + detect.return_value.__str__.return_value = 'raw' + self.assertRaises(exception.ImageUnacceptable, + images.fetch_to_raw, None, 'href123', '/no.path') + # Make sure 'aki was translated into 'raw' before we call qemu-img + imginfo.assert_called_once_with('/no.path.part') + + @mock.patch.object(images, 'IMAGE_API') + @mock.patch.object(images, 'qemu_img_info') + def test_fetch_inspect_unknown_format(self, imginfo, glance): + glance.get.return_value = {'disk_format': 'commodore-64-disk'} + self.assertRaises(exception.ImageUnacceptable, + images.fetch_to_raw, None, 'href123', '/no.path') + # Unsupported formats do not make it past deep inspection + imginfo.assert_not_called() + + @mock.patch.object(images, 'IMAGE_API') + @mock.patch.object(images, 'qemu_img_info') + @mock.patch('nova.image.format_inspector.detect_file_format') + def test_fetch_inspect_disagrees_qemu(self, mock_detect, imginfo, glance): + glance.get.return_value = {'disk_format': 'qcow2'} + mock_detect.return_value.__str__.return_value = 'qcow2' + # Glance and inspector think it is a qcow2 file, but qemu-img does not + # agree. + imginfo.return_value.data_file = None + imginfo.return_value.file_format = 'vmdk' + ex = self.assertRaises(exception.ImageUnacceptable, + images.fetch_to_raw, + None, 'href123', '/no.path') + self.assertIn('content does not match disk_format', str(ex)) + imginfo.assert_called_once_with('/no.path.part') diff --git a/nova/tests/unit/virt/test_virt_drivers.py b/nova/tests/unit/virt/test_virt_drivers.py index 8dcad485bca..3ca1f5dcb5e 100644 --- a/nova/tests/unit/virt/test_virt_drivers.py +++ b/nova/tests/unit/virt/test_virt_drivers.py @@ -832,11 +832,17 @@ def setUp(self): # This is needed for the live migration tests which spawn off the # operation for monitoring. self.useFixture(nova_fixtures.SpawnIsSynchronousFixture()) + self.useFixture(nova_fixtures.CGroupsFixture()) # When destroying an instance, os-vif will try to execute some commands # which hang tests so let's just stub out the unplug call to os-vif # since we don't care about it. self.stub_out('os_vif.unplug', lambda a, kw: None) self.stub_out('nova.compute.utils.get_machine_ips', lambda: []) + self.stub_out('nova.virt.libvirt.utils.get_disk_size', + lambda *a, **k: 123456) + self.stub_out('nova.virt.libvirt.utils.get_disk_backing_file', + lambda *a, **k: None) + self.stub_out('nova.privsep.path.chown', lambda *a, **k: None) def test_init_host_image_type_rbd_force_raw_images_true(self): CONF.set_override('images_type', 'rbd', group='libvirt') diff --git a/nova/tests/unit/virt/vmwareapi/test_images.py b/nova/tests/unit/virt/vmwareapi/test_images.py index 7cfec00c97f..b3a3cfd941e 100644 --- a/nova/tests/unit/virt/vmwareapi/test_images.py +++ b/nova/tests/unit/virt/vmwareapi/test_images.py @@ -117,13 +117,11 @@ def test_fetch_image_ova(self, mock_tar_open, mock_write_class, mock.patch.object(images.IMAGE_API, 'download'), mock.patch.object(images, 'image_transfer'), mock.patch.object(images, '_build_shadow_vm_config_spec'), - mock.patch.object(session, '_call_method'), mock.patch.object(vm_util, 'get_vmdk_info') ) as (mock_image_api_get, mock_image_api_download, mock_image_transfer, mock_build_shadow_vm_config_spec, - mock_call_method, mock_get_vmdk_info): image_data = {'id': 'fake-id', 'disk_format': 'vmdk', @@ -172,7 +170,7 @@ def fake_extract(name): mock_write_handle) mock_get_vmdk_info.assert_called_once_with( session, mock.sentinel.vm_ref, 'fake-vm') - mock_call_method.assert_called_once_with( + session._call_method.assert_called_once_with( session.vim, "UnregisterVM", mock.sentinel.vm_ref) @mock.patch('oslo_vmware.rw_handles.ImageReadHandle') @@ -188,13 +186,11 @@ def test_fetch_image_stream_optimized(self, mock.patch.object(images.IMAGE_API, 'download'), mock.patch.object(images, 'image_transfer'), mock.patch.object(images, '_build_shadow_vm_config_spec'), - mock.patch.object(session, '_call_method'), mock.patch.object(vm_util, 'get_vmdk_info') ) as (mock_image_api_get, mock_image_api_download, mock_image_transfer, mock_build_shadow_vm_config_spec, - mock_call_method, mock_get_vmdk_info): image_data = {'id': 'fake-id', 'disk_format': 'vmdk', @@ -220,7 +216,7 @@ def test_fetch_image_stream_optimized(self, mock_image_transfer.assert_called_once_with(mock_read_handle, mock_write_handle) - mock_call_method.assert_called_once_with( + session._call_method.assert_called_once_with( session.vim, "UnregisterVM", mock.sentinel.vm_ref) mock_get_vmdk_info.assert_called_once_with( session, mock.sentinel.vm_ref, 'fake-vm') diff --git a/nova/tests/unit/volume/test_cinder.py b/nova/tests/unit/volume/test_cinder.py index 0c170c05e49..ffa46ce2aa1 100644 --- a/nova/tests/unit/volume/test_cinder.py +++ b/nova/tests/unit/volume/test_cinder.py @@ -520,16 +520,15 @@ def test_attachment_delete(self, mock_cinderclient): @mock.patch('nova.volume.cinder.cinderclient') def test_attachment_delete_failed(self, mock_cinderclient, mock_log): mock_cinderclient.return_value.attachments.delete.side_effect = ( - cinder_exception.NotFound(404, '404')) + cinder_exception.BadRequest(400, '400')) attachment_id = uuids.attachment - ex = self.assertRaises(exception.VolumeAttachmentNotFound, + ex = self.assertRaises(exception.InvalidInput, self.api.attachment_delete, self.ctx, attachment_id) - self.assertEqual(404, ex.code) - self.assertIn(attachment_id, str(ex)) + self.assertEqual(400, ex.code) @mock.patch('nova.volume.cinder.cinderclient', side_effect=exception.CinderAPIVersionNotAvailable( @@ -545,6 +544,16 @@ def test_attachment_delete_unsupported_api_version(self, mock_cinderclient.assert_called_once_with(self.ctx, '3.44', skip_version_check=True) + @mock.patch('nova.volume.cinder.cinderclient') + def test_attachment_delete_not_found(self, mock_cinderclient): + mock_cinderclient.return_value.attachments.delete.side_effect = ( + cinder_exception.ClientException(404)) + + attachment_id = uuids.attachment + self.api.attachment_delete(self.ctx, attachment_id) + + self.assertEqual(1, mock_cinderclient.call_count) + @mock.patch('nova.volume.cinder.cinderclient') def test_attachment_delete_internal_server_error(self, mock_cinderclient): mock_cinderclient.return_value.attachments.delete.side_effect = ( @@ -568,6 +577,29 @@ def test_attachment_delete_internal_server_error_do_not_raise( self.assertEqual(2, mock_cinderclient.call_count) + @mock.patch('nova.volume.cinder.cinderclient') + def test_attachment_delete_gateway_timeout(self, mock_cinderclient): + mock_cinderclient.return_value.attachments.delete.side_effect = ( + cinder_exception.ClientException(504)) + + self.assertRaises(cinder_exception.ClientException, + self.api.attachment_delete, + self.ctx, uuids.attachment_id) + + self.assertEqual(5, mock_cinderclient.call_count) + + @mock.patch('nova.volume.cinder.cinderclient') + def test_attachment_delete_gateway_timeout_do_not_raise( + self, mock_cinderclient): + # generate exception, and then have a normal return on the next retry + mock_cinderclient.return_value.attachments.delete.side_effect = [ + cinder_exception.ClientException(504), None] + + attachment_id = uuids.attachment + self.api.attachment_delete(self.ctx, attachment_id) + + self.assertEqual(2, mock_cinderclient.call_count) + @mock.patch('nova.volume.cinder.cinderclient') def test_attachment_delete_bad_request_exception(self, mock_cinderclient): mock_cinderclient.return_value.attachments.delete.side_effect = ( @@ -1243,3 +1275,14 @@ def test_admin_context_without_token(self, admin_ctx = context.get_admin_context() params = cinder._get_cinderclient_parameters(admin_ctx) self.assertEqual(params[0], mock_admin_auth) + + @mock.patch('nova.service_auth._SERVICE_AUTH') + @mock.patch('nova.volume.cinder._ADMIN_AUTH') + def test_admin_context_without_user_token_but_with_service_token( + self, mock_admin_auth, mock_service_auth + ): + self.flags(send_service_user_token=True, group='service_user') + admin_ctx = context.get_admin_context() + params = cinder._get_cinderclient_parameters(admin_ctx) + self.assertEqual(mock_admin_auth, params[0].user_auth) + self.assertEqual(mock_service_auth, params[0].service_auth) diff --git a/nova/utils.py b/nova/utils.py index ec5e6c92480..664056a09fd 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -29,6 +29,7 @@ import tempfile import eventlet +from eventlet import tpool from keystoneauth1 import loading as ks_loading import netaddr from openstack import connection @@ -685,7 +686,7 @@ def context_wrapper(*args, **kwargs): def tpool_execute(func, *args, **kwargs): """Run func in a native thread""" - eventlet.tpool.execute(func, *args, **kwargs) + tpool.execute(func, *args, **kwargs) def is_none_string(val): diff --git a/nova/virt/disk/api.py b/nova/virt/disk/api.py index 9902c0608ba..580e4daf1f1 100644 --- a/nova/virt/disk/api.py +++ b/nova/virt/disk/api.py @@ -125,7 +125,21 @@ def extend(image, size): nova.privsep.libvirt.ploop_resize(image.path, size) return - processutils.execute('qemu-img', 'resize', image.path, size) + # NOTE(danms): We should not call qemu-img without a format, and + # only qcow2 and raw are supported. So check which one we're being + # told this is supposed to be and pass that to qemu-img. Also note + # that we need to pass the qemu format string to this command, which + # may or may not be the same as the FORMAT_* constant, so be + # explicit here. + if image.format == imgmodel.FORMAT_RAW: + format = 'raw' + elif image.format == imgmodel.FORMAT_QCOW2: + format = 'qcow2' + else: + LOG.warning('Attempting to resize image %s with format %s, ' + 'which is not supported', image.path, image.format) + raise exception.InvalidDiskFormat(disk_format=image.format) + processutils.execute('qemu-img', 'resize', '-f', format, image.path, size) if (image.format != imgmodel.FORMAT_RAW and not CONF.resize_fs_using_block_device): diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 5aab8ce3007..02fc1f07bcb 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -891,6 +891,36 @@ class FakeLiveMigrateDriverWithNestedCustomResources( class FakeDriverWithPciResources(SmallFakeDriver): + """NOTE: this driver provides symmetric compute nodes. Each compute will + have the same resources with the same addresses. It is dangerous as using + this driver can hide issues when in an asymmetric environment nova fails to + update entities according to the host specific addresses (e.g. pci_slot of + the neutron port bindings). + + The current non virt driver specific functional test environment has many + shortcomings making it really hard to simulate host specific virt drivers. + + 1) The virt driver is instantiated by the service logic from the name of + the driver class. This makes passing input to the driver instance from the + test at init time pretty impossible. This could be solved with some + fixtures around nova.virt.driver.load_compute_driver() + + 2) The compute service access the hypervisor not only via the virt + interface but also reads the sysfs of the host. So simply providing a fake + virt driver instance is not enough to isolate simulated compute services + that are running on the same host. Also these low level sysfs reads are not + having host specific information in the call params. So simply mocking the + low level call does not give a way to provide host specific return values. + + 3) CONF is global, and it is read dynamically by the driver. So + providing host specific CONF to driver instances without race conditions + between the drivers are extremely hard especially if periodic tasks are + enabled. + + The libvirt based functional test env under nova.tests.functional.libvirt + has better support to create asymmetric environments. So please consider + using that if possible instead. + """ PCI_ADDR_PF1 = '0000:01:00.0' PCI_ADDR_PF1_VF1 = '0000:01:00.1' @@ -955,6 +985,11 @@ def setUp(self): ], group='pci') + # These mocks should be removed after bug + # https://bugs.launchpad.net/nova/+bug/1961587 has been fixed and + # every SRIOV device related information is transferred through the + # virt driver and the PciDevice object instead of queried with + # sysfs calls by the network.neutron.API code. self.useFixture(fixtures.MockPatch( 'nova.pci.utils.get_mac_by_pci_address', return_value='52:54:00:1e:59:c6')) diff --git a/nova/virt/hyperv/vmops.py b/nova/virt/hyperv/vmops.py index 3ec7e90c306..08adeada761 100644 --- a/nova/virt/hyperv/vmops.py +++ b/nova/virt/hyperv/vmops.py @@ -747,7 +747,7 @@ def destroy(self, instance, network_info, block_device_info, # should be disconnected even if the VM doesn't exist anymore, # so they are not leaked. self.unplug_vifs(instance, network_info) - self._volumeops.disconnect_volumes(block_device_info) + self._volumeops.disconnect_volumes(block_device_info, force=True) if destroy_disks: self._delete_disk_files(instance_name) diff --git a/nova/virt/hyperv/volumeops.py b/nova/virt/hyperv/volumeops.py index da5b40f3751..d2bfed2441e 100644 --- a/nova/virt/hyperv/volumeops.py +++ b/nova/virt/hyperv/volumeops.py @@ -59,10 +59,10 @@ def attach_volumes(self, volumes, instance_name): for vol in volumes: self.attach_volume(vol['connection_info'], instance_name) - def disconnect_volumes(self, block_device_info): + def disconnect_volumes(self, block_device_info, force=False): mapping = driver.block_device_info_get_mapping(block_device_info) for vol in mapping: - self.disconnect_volume(vol['connection_info']) + self.disconnect_volume(vol['connection_info'], force=force) def attach_volume(self, connection_info, instance_name, disk_bus=constants.CTRL_TYPE_SCSI): @@ -116,9 +116,9 @@ def _attach_volume(self, connection_info, instance_name, volume_driver.set_disk_qos_specs(connection_info, qos_specs) - def disconnect_volume(self, connection_info): + def disconnect_volume(self, connection_info, force=False): volume_driver = self._get_volume_driver(connection_info) - volume_driver.disconnect_volume(connection_info) + volume_driver.disconnect_volume(connection_info, force=force) def detach_volume(self, connection_info, instance_name): LOG.debug("Detaching volume: %(connection_info)s " @@ -231,8 +231,8 @@ def _connector(self): def connect_volume(self, connection_info): return self._connector.connect_volume(connection_info['data']) - def disconnect_volume(self, connection_info): - self._connector.disconnect_volume(connection_info['data']) + def disconnect_volume(self, connection_info, force=False): + self._connector.disconnect_volume(connection_info['data'], force=force) def get_disk_resource_path(self, connection_info): disk_paths = self._connector.get_volume_paths(connection_info['data']) diff --git a/nova/virt/images.py b/nova/virt/images.py index 5358f3766ac..193c80fb636 100644 --- a/nova/virt/images.py +++ b/nova/virt/images.py @@ -30,6 +30,7 @@ import nova.conf from nova import exception from nova.i18n import _ +from nova.image import format_inspector from nova.image import glance import nova.privsep.qemu @@ -110,18 +111,113 @@ def get_info(context, image_href): return IMAGE_API.get(context, image_href) +def check_vmdk_image(image_id, data): + # Check some rules about VMDK files. Specifically we want to make + # sure that the "create-type" of the image is one that we allow. + # Some types of VMDK files can reference files outside the disk + # image and we do not want to allow those for obvious reasons. + + types = CONF.compute.vmdk_allowed_types + + if not len(types): + LOG.warning('Refusing to allow VMDK image as vmdk_allowed_' + 'types is empty') + msg = _('Invalid VMDK create-type specified') + raise exception.ImageUnacceptable(image_id=image_id, reason=msg) + + try: + create_type = data.format_specific['data']['create-type'] + except KeyError: + msg = _('Unable to determine VMDK create-type') + raise exception.ImageUnacceptable(image_id=image_id, reason=msg) + + if create_type not in CONF.compute.vmdk_allowed_types: + LOG.warning('Refusing to process VMDK file with create-type of %r ' + 'which is not in allowed set of: %s', create_type, + ','.join(CONF.compute.vmdk_allowed_types)) + msg = _('Invalid VMDK create-type specified') + raise exception.ImageUnacceptable(image_id=image_id, reason=msg) + + +def do_image_deep_inspection(img, image_href, path): + ami_formats = ('ami', 'aki', 'ari') + disk_format = img['disk_format'] + try: + # NOTE(danms): Use our own cautious inspector module to make sure + # the image file passes safety checks. + # See https://bugs.launchpad.net/nova/+bug/2059809 for details. + + # Make sure we have a format inspector for the claimed format, else + # it is something we do not support and must reject. AMI is excluded. + if (disk_format not in ami_formats and + not format_inspector.get_inspector(disk_format)): + raise exception.ImageUnacceptable( + image_id=image_href, + reason=_('Image not in a supported format')) + + inspector = format_inspector.detect_file_format(path) + if not inspector.safety_check(): + raise exception.ImageUnacceptable( + image_id=image_href, + reason=(_('Image does not pass safety check'))) + + # AMI formats can be other things, so don't obsess over this + # requirement for them. Otherwise, make sure our detection agrees + # with glance. + if disk_format not in ami_formats and str(inspector) != disk_format: + # If we detected the image as something other than glance claimed, + # we abort. + raise exception.ImageUnacceptable( + image_id=image_href, + reason=_('Image content does not match disk_format')) + except format_inspector.ImageFormatError: + # If the inspector we chose based on the image's metadata does not + # think the image is the proper format, we refuse to use it. + raise exception.ImageUnacceptable( + image_id=image_href, + reason=_('Image content does not match disk_format')) + except Exception: + raise exception.ImageUnacceptable( + image_id=image_href, + reason=_('Image not in a supported format')) + if disk_format in ('iso',) + ami_formats: + # ISO or AMI image passed safety check; qemu will treat this as raw + # from here so return the expected formats it will find. + disk_format = 'raw' + return disk_format + + def fetch_to_raw(context, image_href, path, trusted_certs=None): path_tmp = "%s.part" % path fetch(context, image_href, path_tmp, trusted_certs) with fileutils.remove_path_on_error(path_tmp): - data = qemu_img_info(path_tmp) + if not CONF.workarounds.disable_deep_image_inspection: + # If we're doing deep inspection, we take the determined format + # from it. + img = IMAGE_API.get(context, image_href) + force_format = do_image_deep_inspection(img, image_href, path_tmp) + else: + force_format = None + # Only run qemu-img after we have done deep inspection (if enabled). + # If it was not enabled, we will let it detect the format. + data = qemu_img_info(path_tmp) fmt = data.file_format if fmt is None: raise exception.ImageUnacceptable( reason=_("'qemu-img info' parsing failed."), image_id=image_href) + elif force_format is not None and fmt != force_format: + # Format inspector and qemu-img must agree on the format, else + # we reject. This will catch VMDK some variants that we don't + # explicitly support because qemu will identify them as such + # and we will not. + LOG.warning('Image %s detected by qemu as %s but we expected %s', + image_href, fmt, force_format) + raise exception.ImageUnacceptable( + image_id=image_href, + reason=_('Image content does not match disk_format')) backing_file = data.backing_file if backing_file is not None: @@ -129,6 +225,18 @@ def fetch_to_raw(context, image_href, path, trusted_certs=None): reason=(_("fmt=%(fmt)s backed by: %(backing_file)s") % {'fmt': fmt, 'backing_file': backing_file})) + try: + data_file = data.format_specific['data']['data-file'] + except (KeyError, TypeError, AttributeError): + data_file = None + if data_file is not None: + raise exception.ImageUnacceptable(image_id=image_href, + reason=(_("fmt=%(fmt)s has data-file: %(data_file)s") % + {'fmt': fmt, 'data_file': data_file})) + + if fmt == 'vmdk': + check_vmdk_image(image_href, data) + if fmt != "raw" and CONF.force_raw_images: staged = "%s.converted" % path LOG.debug("%s was %s, converting to raw", image_href, fmt) diff --git a/nova/virt/ironic/driver.py b/nova/virt/ironic/driver.py index 7970f185412..f21694da478 100644 --- a/nova/virt/ironic/driver.py +++ b/nova/virt/ironic/driver.py @@ -397,6 +397,18 @@ def prepare_for_spawn(self, instance): _("Ironic node uuid not supplied to " "driver for instance %s.") % instance.uuid) node = self._get_node(node_uuid) + + # Its possible this node has just moved from deleting + # to cleaning. Placement will update the inventory + # as all reserved, but this instance might have got here + # before that happened, but after the previous allocation + # got deleted. We trigger a re-schedule to another node. + if (self._node_resources_used(node) or + self._node_resources_unavailable(node)): + msg = "Chosen ironic node %s is not available" % node_uuid + LOG.info(msg, instance=instance) + raise exception.ComputeResourcesUnavailable(reason=msg) + self._set_instance_id(node, instance) def failed_spawn_cleanup(self, instance): diff --git a/nova/virt/libvirt/config.py b/nova/virt/libvirt/config.py index 1a81be3ade5..47e92e3ca91 100644 --- a/nova/virt/libvirt/config.py +++ b/nova/virt/libvirt/config.py @@ -3299,6 +3299,7 @@ def __init__(self, **kwargs): root_name="capability", **kwargs) self.type = None self.iommu_group = None + self.uuid = None def parse_dom(self, xmldoc): super(LibvirtConfigNodeDeviceMdevInformation, @@ -3308,6 +3309,8 @@ def parse_dom(self, xmldoc): self.type = c.get('id') if c.tag == "iommuGroup": self.iommu_group = int(c.get('number')) + if c.tag == "uuid": + self.uuid = c.text class LibvirtConfigNodeDeviceVpdCap(LibvirtConfigObject): diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 94e7b1945aa..da2da25ea9f 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -1639,7 +1639,7 @@ def _cleanup(self, context, instance, network_info, block_device_info=None, try: self._disconnect_volume( context, connection_info, instance, - destroy_secrets=destroy_secrets) + destroy_secrets=destroy_secrets, force=True) except Exception as exc: with excutils.save_and_reraise_exception() as ctxt: if cleanup_instance_disks: @@ -1956,7 +1956,7 @@ def _should_disconnect_target(self, context, instance, multiattach, return (False if connection_count > 1 else True) def _disconnect_volume(self, context, connection_info, instance, - encryption=None, destroy_secrets=True): + encryption=None, destroy_secrets=True, force=False): self._detach_encryptor( context, connection_info, @@ -1968,7 +1968,8 @@ def _disconnect_volume(self, context, connection_info, instance, multiattach = connection_info.get('multiattach', False) if self._should_disconnect_target( context, instance, multiattach, vol_driver, volume_id): - vol_driver.disconnect_volume(connection_info, instance) + vol_driver.disconnect_volume( + connection_info, instance, force=force) else: LOG.info('Detected multiple connections on this host for ' 'volume: %(volume)s, skipping target disconnect.', @@ -2928,11 +2929,7 @@ def _create_snapshot_metadata(self, image_meta, instance, if instance.os_type: metadata['properties']['os_type'] = instance.os_type - # NOTE(vish): glance forces ami disk format to be ami - if image_meta.disk_format == 'ami': - metadata['disk_format'] = 'ami' - else: - metadata['disk_format'] = img_fmt + metadata['disk_format'] = img_fmt if image_meta.obj_attr_is_set("container_format"): metadata['container_format'] = image_meta.container_format @@ -4612,6 +4609,13 @@ def _inject_data(self, disk, instance, injection_info): {'img_id': img_id, 'e': e}, instance=instance) + @staticmethod + def _get_fs_label_ephemeral(index: int) -> str: + # Use a consistent naming convention for FS labels. We need to be + # mindful of various filesystems label name length limitations. + # See for example: https://bugs.launchpad.net/nova/+bug/2061701 + return f'ephemeral{index}' + # NOTE(sileht): many callers of this method assume that this # method doesn't fail if an image already exists but instead # think that it will be reused (ie: (live)-migration/resize) @@ -4718,7 +4722,7 @@ def raw(fname): created_disks = created_disks or not disk_image.exists() fn = functools.partial(self._create_ephemeral, - fs_label='ephemeral0', + fs_label=self._get_fs_label_ephemeral(0), os_type=instance.os_type, is_block_dev=disk_image.is_block_dev, vm_mode=vm_mode) @@ -4742,7 +4746,7 @@ def raw(fname): raise exception.InvalidBDMFormat(details=msg) fn = functools.partial(self._create_ephemeral, - fs_label='ephemeral%d' % idx, + fs_label=self._get_fs_label_ephemeral(idx), os_type=instance.os_type, is_block_dev=disk_image.is_block_dev, vm_mode=vm_mode) @@ -5681,15 +5685,11 @@ def _update_guest_cputune(self, guest, flavor): if not is_able or CONF.libvirt.virt_type not in ('lxc', 'kvm', 'qemu'): return - if guest.cputune is None: - guest.cputune = vconfig.LibvirtConfigGuestCPUTune() - # Setting the default cpu.shares value to be a value - # dependent on the number of vcpus - guest.cputune.shares = 1024 * guest.vcpus - for name in cputuning: key = "quota:cpu_" + name if key in flavor.extra_specs: + if guest.cputune is None: + guest.cputune = vconfig.LibvirtConfigGuestCPUTune() setattr(guest.cputune, name, int(flavor.extra_specs[key])) @@ -8019,15 +8019,52 @@ def _get_mdev_capable_devices(self, types=None): def _get_mediated_device_information(self, devname): """Returns a dict of a mediated device.""" - virtdev = self._host.device_lookup_by_name(devname) + # LP #1951656 - In Libvirt 7.7, the mdev name now includes the PCI + # address of the parent device (e.g. mdev__) due to + # the mdevctl allowing for multiple mediated devs having the same UUID + # defined (only one can be active at a time). Since the guest + # information doesn't have the parent ID, try to lookup which + # mediated device is available that matches the UUID. If multiple + # devices are found that match the UUID, then this is an error + # condition. + try: + virtdev = self._host.device_lookup_by_name(devname) + except libvirt.libvirtError as ex: + if ex.get_error_code() != libvirt.VIR_ERR_NO_NODE_DEVICE: + raise + mdevs = [dev for dev in self._host.list_mediated_devices() + if dev.startswith(devname)] + # If no matching devices are found, simply raise the original + # exception indicating that no devices are found. + if not mdevs: + raise + elif len(mdevs) > 1: + msg = ("The mediated device name %(devname)s refers to a UUID " + "that is present in multiple libvirt mediated devices. " + "Matching libvirt mediated devices are %(devices)s. " + "Mediated device UUIDs must be unique for Nova." % + {'devname': devname, + 'devices': ', '.join(mdevs)}) + raise exception.InvalidLibvirtMdevConfig(reason=msg) + + LOG.debug('Found requested device %s as %s. Using that.', + devname, mdevs[0]) + virtdev = self._host.device_lookup_by_name(mdevs[0]) xmlstr = virtdev.XMLDesc(0) cfgdev = vconfig.LibvirtConfigNodeDevice() cfgdev.parse_str(xmlstr) + # Starting with Libvirt 7.3, the uuid information is available in the + # node device information. If its there, use that. Otherwise, + # fall back to the previous behavior of parsing the uuid from the + # devname. + if cfgdev.mdev_information.uuid: + mdev_uuid = cfgdev.mdev_information.uuid + else: + mdev_uuid = libvirt_utils.mdev_name2uuid(cfgdev.name) device = { "dev_id": cfgdev.name, - # name is like mdev_00ead764_fdc0_46b6_8db9_2963f5c815b4 - "uuid": libvirt_utils.mdev_name2uuid(cfgdev.name), + "uuid": mdev_uuid, # the physical GPU PCI device "parent": cfgdev.parent, "type": cfgdev.mdev_information.type, @@ -8115,6 +8152,7 @@ def _get_existing_mdevs_not_assigned(self, parent, requested_types=None): :param requested_types: Filter out the result for only mediated devices having those types. """ + LOG.debug('Searching for available mdevs...') allocated_mdevs = self._get_all_assigned_mediated_devices() mdevs = self._get_mediated_devices(requested_types) available_mdevs = set() @@ -8130,6 +8168,7 @@ def _get_existing_mdevs_not_assigned(self, parent, requested_types=None): available_mdevs.add(mdev["uuid"]) available_mdevs -= set(allocated_mdevs) + LOG.info('Available mdevs at: %s.', available_mdevs) return available_mdevs def _create_new_mediated_device(self, parent, uuid=None): @@ -8141,6 +8180,7 @@ def _create_new_mediated_device(self, parent, uuid=None): :returns: the newly created mdev UUID or None if not possible """ + LOG.debug('Attempting to create new mdev...') supported_types = self.supported_vgpu_types # Try to see if we can still create a new mediated device devices = self._get_mdev_capable_devices(supported_types) @@ -8152,6 +8192,7 @@ def _create_new_mediated_device(self, parent, uuid=None): # The device is not the one that was called, not creating # the mdev continue + LOG.debug('Trying on: %s.', dev_name) dev_supported_type = self._get_vgpu_type_per_pgpu(dev_name) if dev_supported_type and device['types'][ dev_supported_type]['availableInstances'] > 0: @@ -8161,7 +8202,13 @@ def _create_new_mediated_device(self, parent, uuid=None): pci_addr = "{}:{}:{}.{}".format(*dev_name[4:].split('_')) chosen_mdev = nova.privsep.libvirt.create_mdev( pci_addr, dev_supported_type, uuid=uuid) + LOG.info('Created mdev: %s on pGPU: %s.', + chosen_mdev, pci_addr) return chosen_mdev + LOG.debug('Failed: No available instances on device.') + LOG.info('Failed to create mdev. ' + 'No free space found among the following devices: %s.', + [dev['dev_id'] for dev in devices]) @utils.synchronized(VGPU_RESOURCE_SEMAPHORE) def _allocate_mdevs(self, allocations): @@ -8244,6 +8291,8 @@ def _allocate_mdevs(self, allocations): # Take the first available mdev chosen_mdev = mdevs_available.pop() else: + LOG.debug('No available mdevs where found. ' + 'Creating an new one...') chosen_mdev = self._create_new_mediated_device(parent_device) if not chosen_mdev: # If we can't find devices having available VGPUs, just raise @@ -8251,6 +8300,7 @@ def _allocate_mdevs(self, allocations): reason='mdev-capable resource is not available') else: chosen_mdevs.append(chosen_mdev) + LOG.info('Allocated mdev: %s.', chosen_mdev) return chosen_mdevs def _detach_mediated_devices(self, guest): @@ -9338,15 +9388,16 @@ def check_can_live_migrate_destination(self, context, instance, disk_available_mb = ( (disk_available_gb * units.Ki) - CONF.reserved_host_disk_mb) - # Compare CPU - try: - if not instance.vcpu_model or not instance.vcpu_model.model: - source_cpu_info = src_compute_info['cpu_info'] - self._compare_cpu(None, source_cpu_info, instance) - else: - self._compare_cpu(instance.vcpu_model, None, instance) - except exception.InvalidCPUInfo as e: - raise exception.MigrationPreCheckError(reason=e) + if not CONF.workarounds.skip_cpu_compare_on_dest: + # Compare CPU + try: + if not instance.vcpu_model or not instance.vcpu_model.model: + source_cpu_info = src_compute_info['cpu_info'] + self._compare_cpu(None, source_cpu_info, instance) + else: + self._compare_cpu(instance.vcpu_model, None, instance) + except exception.InvalidCPUInfo as e: + raise exception.MigrationPreCheckError(reason=e) # Create file on storage, to be checked on source host filename = self._create_shared_storage_test_file(instance) @@ -10418,10 +10469,13 @@ def rollback_live_migration_at_source(self, context, instance, :param instance: the instance being migrated :param migrate_date: a LibvirtLiveMigrateData object """ - network_info = network_model.NetworkInfo( - [vif.source_vif for vif in migrate_data.vifs - if "source_vif" in vif and vif.source_vif]) - self._reattach_instance_vifs(context, instance, network_info) + # NOTE(artom) migrate_data.vifs might not be set if our Neutron doesn't + # have the multiple port bindings extension. + if 'vifs' in migrate_data and migrate_data.vifs: + network_info = network_model.NetworkInfo( + [vif.source_vif for vif in migrate_data.vifs + if "source_vif" in vif and vif.source_vif]) + self._reattach_instance_vifs(context, instance, network_info) def rollback_live_migration_at_destination(self, context, instance, network_info, @@ -10719,7 +10773,7 @@ def _create_images_and_backing(self, context, instance, instance_dir, # cached. disk.cache( fetch_func=self._create_ephemeral, - fs_label=cache_name, + fs_label=self._get_fs_label_ephemeral(0), os_type=instance.os_type, filename=cache_name, size=info['virt_disk_size'], diff --git a/nova/virt/libvirt/guest.py b/nova/virt/libvirt/guest.py index 53080e41f0b..68bd4ca5b07 100644 --- a/nova/virt/libvirt/guest.py +++ b/nova/virt/libvirt/guest.py @@ -655,6 +655,7 @@ def get_job_info(self): stats = self._domain.jobStats() return JobInfo(**stats) except libvirt.libvirtError as ex: + errmsg = ex.get_error_message() if ex.get_error_code() == libvirt.VIR_ERR_NO_SUPPORT: # Remote libvirt doesn't support new API LOG.debug("Missing remote virDomainGetJobStats: %s", ex) @@ -667,6 +668,12 @@ def get_job_info(self): # away completclsely LOG.debug("Domain has shutdown/gone away: %s", ex) return JobInfo(type=libvirt.VIR_DOMAIN_JOB_COMPLETED) + elif (ex.get_error_code() == libvirt.VIR_ERR_INTERNAL_ERROR and + errmsg and "migration was active, " + "but no RAM info was set" in errmsg): + LOG.debug("Migration is active or completed but " + "virDomainGetJobStats is missing ram: %s", ex) + return JobInfo(type=libvirt.VIR_DOMAIN_JOB_NONE) else: LOG.debug("Failed to get job stats: %s", ex) raise diff --git a/nova/virt/libvirt/host.py b/nova/virt/libvirt/host.py index cdf47008de4..b1a94e5f315 100644 --- a/nova/virt/libvirt/host.py +++ b/nova/virt/libvirt/host.py @@ -46,6 +46,7 @@ from oslo_serialization import jsonutils from oslo_utils import excutils from oslo_utils import importutils +from oslo_utils import strutils from oslo_utils import units from oslo_utils import versionutils @@ -1267,6 +1268,20 @@ def _get_vpd_card_serial_number( return None return vpd_cap.card_serial_number + def _get_pf_details(self, device: dict, pci_address: str) -> dict: + if device.get('dev_type') != fields.PciDeviceType.SRIOV_PF: + return {} + + try: + return { + 'mac_address': pci_utils.get_mac_by_pci_address(pci_address) + } + except exception.PciDeviceNotFoundById: + LOG.debug( + 'Cannot get MAC address of the PF %s. It is probably attached ' + 'to a guest already', pci_address) + return {} + def _get_pcidev_info( self, devname: str, @@ -1426,6 +1441,7 @@ def _get_vpd_details( _get_device_type(cfgdev, address, dev, net_devs, vdpa_devs)) device.update(_get_device_capabilities(device, dev, net_devs)) device.update(_get_vpd_details(device, dev, pci_devs)) + device.update(self._get_pf_details(device, address)) return device def get_vdpa_nodedev_by_address( @@ -1487,7 +1503,7 @@ def list_mdev_capable_devices(self, flags=0): def list_mediated_devices(self, flags=0): """Lookup mediated devices. - :returns: a list of virNodeDevice instance + :returns: a list of strings with the name of the instance """ return self._list_devices("mdev", flags=flags) @@ -1532,15 +1548,44 @@ def is_cpu_control_policy_capable(self): CONFIG_CGROUP_SCHED may be disabled in some kernel configs to improve scheduler latency. """ + return self._has_cgroupsv1_cpu_controller() or \ + self._has_cgroupsv2_cpu_controller() + + def _has_cgroupsv1_cpu_controller(self): + LOG.debug(f"Searching host: '{self.get_hostname()}' " + "for CPU controller through CGroups V1...") try: with open("/proc/self/mounts", "r") as fd: for line in fd.readlines(): # mount options and split options bits = line.split()[3].split(",") if "cpu" in bits: + LOG.debug("CPU controller found on host.") + return True + LOG.debug("CPU controller missing on host.") + return False + except IOError as ex: + LOG.debug(f"Search failed due to: '{ex}'. " + "Maybe the host is not running under CGroups V1. " + "Deemed host to be missing controller by this approach.") + return False + + def _has_cgroupsv2_cpu_controller(self): + LOG.debug(f"Searching host: '{self.get_hostname()}' " + "for CPU controller through CGroups V2...") + try: + with open("/sys/fs/cgroup/cgroup.controllers", "r") as fd: + for line in fd.readlines(): + bits = line.split() + if "cpu" in bits: + LOG.debug("CPU controller found on host.") return True + LOG.debug("CPU controller missing on host.") return False - except IOError: + except IOError as ex: + LOG.debug(f"Search failed due to: '{ex}'. " + "Maybe the host is not running under CGroups V2. " + "Deemed host to be missing controller by this approach.") return False def get_canonical_machine_type(self, arch, machine) -> str: @@ -1656,9 +1701,9 @@ def _kernel_supports_amd_sev(self) -> bool: return False with open(SEV_KERNEL_PARAM_FILE) as f: - contents = f.read() - LOG.debug("%s contains [%s]", SEV_KERNEL_PARAM_FILE, contents) - return contents == "1\n" + content = f.read() + LOG.debug("%s contains [%s]", SEV_KERNEL_PARAM_FILE, content) + return strutils.bool_from_string(content) @property def supports_amd_sev(self) -> bool: diff --git a/nova/virt/libvirt/imagebackend.py b/nova/virt/libvirt/imagebackend.py index 617adfe0303..6a5252b11b3 100644 --- a/nova/virt/libvirt/imagebackend.py +++ b/nova/virt/libvirt/imagebackend.py @@ -34,6 +34,7 @@ import nova.conf from nova import exception from nova.i18n import _ +from nova.image import format_inspector from nova.image import glance import nova.privsep.libvirt import nova.privsep.path @@ -637,6 +638,20 @@ def create_qcow2_image(base, target, size): if not os.path.exists(base): prepare_template(target=base, *args, **kwargs) + # NOTE(danms): We need to perform safety checks on the base image + # before we inspect it for other attributes. We do this each time + # because additional safety checks could have been added since we + # downloaded the image. + if not CONF.workarounds.disable_deep_image_inspection: + inspector = format_inspector.detect_file_format(base) + if not inspector.safety_check(): + LOG.warning('Base image %s failed safety check', base) + # NOTE(danms): This is the same exception as would be raised + # by qemu_img_info() if the disk format was unreadable or + # otherwise unsuitable. + raise exception.InvalidDiskInfo( + reason=_('Base image failed safety check')) + # NOTE(ankit): Update the mtime of the base file so the image # cache manager knows it is in use. _update_utime_ignore_eacces(base) diff --git a/nova/virt/libvirt/migration.py b/nova/virt/libvirt/migration.py index 8cea9f29831..4726111a765 100644 --- a/nova/virt/libvirt/migration.py +++ b/nova/virt/libvirt/migration.py @@ -62,6 +62,7 @@ def get_updated_guest_xml(instance, guest, migrate_data, get_volume_config, xml_doc, migrate_data, instance, get_volume_config) xml_doc = _update_perf_events_xml(xml_doc, migrate_data) xml_doc = _update_memory_backing_xml(xml_doc, migrate_data) + xml_doc = _update_quota_xml(instance, xml_doc) if get_vif_config is not None: xml_doc = _update_vif_xml(xml_doc, migrate_data, get_vif_config) if 'dst_numa_info' in migrate_data: @@ -71,6 +72,18 @@ def get_updated_guest_xml(instance, guest, migrate_data, get_volume_config, return etree.tostring(xml_doc, encoding='unicode') +def _update_quota_xml(instance, xml_doc): + flavor_shares = instance.flavor.extra_specs.get('quota:cpu_shares') + cputune = xml_doc.find('./cputune') + shares = xml_doc.find('./cputune/shares') + if shares is not None and not flavor_shares: + cputune.remove(shares) + # Remove the cputune element entirely if it has no children left. + if cputune is not None and not list(cputune): + xml_doc.remove(cputune) + return xml_doc + + def _update_device_resources_xml(xml_doc, new_resources): vpmems = [] for resource in new_resources: diff --git a/nova/virt/libvirt/utils.py b/nova/virt/libvirt/utils.py index 834f242c792..93c5b38cb77 100644 --- a/nova/virt/libvirt/utils.py +++ b/nova/virt/libvirt/utils.py @@ -34,6 +34,7 @@ from nova import context as nova_context from nova import exception from nova.i18n import _ +from nova.image import format_inspector from nova import objects from nova.objects import fields as obj_fields import nova.privsep.fs @@ -139,7 +140,36 @@ def create_cow_image( base_cmd = ['qemu-img', 'create', '-f', 'qcow2'] cow_opts = [] if backing_file: + # NOTE(danms): We need to perform safety checks on the base image + # before we inspect it for other attributes. We do this each time + # because additional safety checks could have been added since we + # downloaded the image. + if not CONF.workarounds.disable_deep_image_inspection: + inspector = format_inspector.detect_file_format(backing_file) + if not inspector.safety_check(): + LOG.warning('Base image %s failed safety check', backing_file) + # NOTE(danms): This is the same exception as would be raised + # by qemu_img_info() if the disk format was unreadable or + # otherwise unsuitable. + raise exception.InvalidDiskInfo( + reason=_('Base image failed safety check')) + base_details = images.qemu_img_info(backing_file) + if base_details.file_format == 'vmdk': + images.check_vmdk_image('base', base_details) + if base_details.backing_file is not None: + LOG.warning('Base image %s failed safety check', backing_file) + raise exception.InvalidDiskInfo( + reason=_('Base image failed safety check')) + try: + data_file = base_details.format_specific['data']['data-file'] + except (KeyError, TypeError, AttributeError): + data_file = None + if data_file is not None: + LOG.warning('Base image %s failed safety check', backing_file) + raise exception.InvalidDiskInfo( + reason=_('Base image failed safety check')) + cow_opts += ['backing_file=%s' % backing_file] cow_opts += ['backing_fmt=%s' % base_details.file_format] else: @@ -581,17 +611,31 @@ def get_default_machine_type(arch: str) -> ty.Optional[str]: def mdev_name2uuid(mdev_name: str) -> str: - """Convert an mdev name (of the form mdev_) to a - uuid (of the form 8-4-4-4-12). + """Convert an mdev name (of the form mdev_ or + mdev__) to a uuid + (of the form 8-4-4-4-12). + + :param mdev_name: the name of the mdev to parse the UUID from + :returns: string containing the uuid """ - return str(uuid.UUID(mdev_name[5:].replace('_', '-'))) + mdev_uuid = mdev_name[5:].replace('_', '-') + # Unconditionnally remove the PCI address from the name + mdev_uuid = mdev_uuid[:36] + return str(uuid.UUID(mdev_uuid)) + +def mdev_uuid2name(mdev_uuid: str, parent: str = None) -> str: + """Convert an mdev uuid (of the form 8-4-4-4-12) and optionally its parent + device to a name (of the form mdev_[_]). -def mdev_uuid2name(mdev_uuid: str) -> str: - """Convert an mdev uuid (of the form 8-4-4-4-12) to a name (of the form - mdev_). + :param mdev_uuid: the uuid of the mediated device + :param parent: the parent device id for the mediated device + :returns: name of the mdev to reference in libvirt """ - return "mdev_" + mdev_uuid.replace('-', '_') + name = "mdev_" + mdev_uuid.replace('-', '_') + if parent and parent.startswith('pci_'): + name = name + parent[4:] + return name def get_flags_by_flavor_specs(flavor: 'objects.Flavor') -> ty.Set[str]: diff --git a/nova/virt/libvirt/volume/fibrechannel.py b/nova/virt/libvirt/volume/fibrechannel.py index b50db3aa1c0..1f890c95c12 100644 --- a/nova/virt/libvirt/volume/fibrechannel.py +++ b/nova/virt/libvirt/volume/fibrechannel.py @@ -59,7 +59,7 @@ def connect_volume(self, connection_info, instance): connection_info['data']['multipath_id'] = \ device_info['multipath_id'] - def disconnect_volume(self, connection_info, instance): + def disconnect_volume(self, connection_info, instance, force=False): """Detach the volume from instance_name.""" LOG.debug("calling os-brick to detach FC Volume", instance=instance) @@ -69,11 +69,12 @@ def disconnect_volume(self, connection_info, instance): # the 2nd param of disconnect_volume and be consistent # with the rest of the connectors. self.connector.disconnect_volume(connection_info['data'], - connection_info['data']) + connection_info['data'], + force=force) LOG.debug("Disconnected FC Volume", instance=instance) super(LibvirtFibreChannelVolumeDriver, - self).disconnect_volume(connection_info, instance) + self).disconnect_volume(connection_info, instance, force=force) def extend_volume(self, connection_info, instance, requested_size): """Extend the volume.""" diff --git a/nova/virt/libvirt/volume/fs.py b/nova/virt/libvirt/volume/fs.py index 5fb9af4a520..992ef45016e 100644 --- a/nova/virt/libvirt/volume/fs.py +++ b/nova/virt/libvirt/volume/fs.py @@ -116,7 +116,7 @@ def connect_volume(self, connection_info, instance): connection_info['data']['device_path'] = \ self._get_device_path(connection_info) - def disconnect_volume(self, connection_info, instance): + def disconnect_volume(self, connection_info, instance, force=False): """Disconnect the volume.""" vol_name = connection_info['data']['name'] mountpoint = self._get_mount_path(connection_info) diff --git a/nova/virt/libvirt/volume/iscsi.py b/nova/virt/libvirt/volume/iscsi.py index 564bac14cc7..2b25972a495 100644 --- a/nova/virt/libvirt/volume/iscsi.py +++ b/nova/virt/libvirt/volume/iscsi.py @@ -66,19 +66,20 @@ def connect_volume(self, connection_info, instance): connection_info['data']['device_path'] = device_info['path'] - def disconnect_volume(self, connection_info, instance): + def disconnect_volume(self, connection_info, instance, force=False): """Detach the volume from instance_name.""" LOG.debug("calling os-brick to detach iSCSI Volume", instance=instance) try: - self.connector.disconnect_volume(connection_info['data'], None) + self.connector.disconnect_volume( + connection_info['data'], None, force=force) except os_brick_exception.VolumeDeviceNotFound as exc: LOG.warning('Ignoring VolumeDeviceNotFound: %s', exc) return LOG.debug("Disconnected iSCSI Volume", instance=instance) super(LibvirtISCSIVolumeDriver, - self).disconnect_volume(connection_info, instance) + self).disconnect_volume(connection_info, instance, force=force) def extend_volume(self, connection_info, instance, requested_size): """Extend the volume.""" diff --git a/nova/virt/libvirt/volume/lightos.py b/nova/virt/libvirt/volume/lightos.py index d6d393994e5..6a22bf6dc63 100644 --- a/nova/virt/libvirt/volume/lightos.py +++ b/nova/virt/libvirt/volume/lightos.py @@ -42,14 +42,15 @@ def connect_volume(self, connection_info, instance): LOG.debug("Connecting NVMe volume with device_info %s", device_info) connection_info['data']['device_path'] = device_info['path'] - def disconnect_volume(self, connection_info, instance): + def disconnect_volume(self, connection_info, instance, force=False): """Detach the volume from the instance.""" LOG.debug("Disconnecting NVMe disk. instance:%s, volume_id:%s", connection_info.get("instance", ""), connection_info.get("volume_id", "")) - self.connector.disconnect_volume(connection_info['data'], None) + self.connector.disconnect_volume( + connection_info['data'], None, force=force) super(LibvirtLightOSVolumeDriver, self).disconnect_volume( - connection_info, instance) + connection_info, instance, force=force) def extend_volume(self, connection_info, instance, requested_size=None): """Extend the volume.""" diff --git a/nova/virt/libvirt/volume/nvme.py b/nova/virt/libvirt/volume/nvme.py index 74365528122..e2977c3572b 100644 --- a/nova/virt/libvirt/volume/nvme.py +++ b/nova/virt/libvirt/volume/nvme.py @@ -45,13 +45,13 @@ def connect_volume(self, connection_info, instance): connection_info['data']['device_path'] = device_info['path'] - def disconnect_volume(self, connection_info, instance): + def disconnect_volume(self, connection_info, instance, force=False): """Detach the volume from the instance.""" LOG.debug("Disconnecting NVMe disk", instance=instance) self.connector.disconnect_volume( - connection_info['data'], None) + connection_info['data'], None, force=force) super(LibvirtNVMEVolumeDriver, - self).disconnect_volume(connection_info, instance) + self).disconnect_volume(connection_info, instance, force=force) def extend_volume(self, connection_info, instance, requested_size): """Extend the volume.""" diff --git a/nova/virt/libvirt/volume/quobyte.py b/nova/virt/libvirt/volume/quobyte.py index bb7a770e57e..2eb4bcfb428 100644 --- a/nova/virt/libvirt/volume/quobyte.py +++ b/nova/virt/libvirt/volume/quobyte.py @@ -189,7 +189,7 @@ def connect_volume(self, connection_info, instance): instance=instance) @utils.synchronized('connect_qb_volume') - def disconnect_volume(self, connection_info, instance): + def disconnect_volume(self, connection_info, instance, force=False): """Disconnect the volume.""" mount_path = self._get_mount_path(connection_info) diff --git a/nova/virt/libvirt/volume/scaleio.py b/nova/virt/libvirt/volume/scaleio.py index 7c414c2870f..04a9423e8ea 100644 --- a/nova/virt/libvirt/volume/scaleio.py +++ b/nova/virt/libvirt/volume/scaleio.py @@ -57,12 +57,13 @@ def connect_volume(self, connection_info, instance): instance=instance) connection_info['data']['device_path'] = device_info['path'] - def disconnect_volume(self, connection_info, instance): - self.connector.disconnect_volume(connection_info['data'], None) + def disconnect_volume(self, connection_info, instance, force=False): + self.connector.disconnect_volume( + connection_info['data'], None, force=force) LOG.debug("Disconnected volume", instance=instance) super(LibvirtScaleIOVolumeDriver, self).disconnect_volume( - connection_info, instance) + connection_info, instance, force=force) def extend_volume(self, connection_info, instance, requested_size): LOG.debug("calling os-brick to extend ScaleIO Volume", diff --git a/nova/virt/libvirt/volume/smbfs.py b/nova/virt/libvirt/volume/smbfs.py index d112af750cb..9de1ce23cd3 100644 --- a/nova/virt/libvirt/volume/smbfs.py +++ b/nova/virt/libvirt/volume/smbfs.py @@ -52,7 +52,7 @@ def connect_volume(self, connection_info, instance): device_path = self._get_device_path(connection_info) connection_info['data']['device_path'] = device_path - def disconnect_volume(self, connection_info, instance): + def disconnect_volume(self, connection_info, instance, force=False): """Disconnect the volume.""" smbfs_share = connection_info['data']['export'] mount_path = self._get_mount_path(connection_info) diff --git a/nova/virt/libvirt/volume/storpool.py b/nova/virt/libvirt/volume/storpool.py index 0e71221f5b2..e6dffca39a6 100644 --- a/nova/virt/libvirt/volume/storpool.py +++ b/nova/virt/libvirt/volume/storpool.py @@ -47,10 +47,11 @@ def connect_volume(self, connection_info, instance): device_info, instance=instance) connection_info['data']['device_path'] = device_info['path'] - def disconnect_volume(self, connection_info, instance): + def disconnect_volume(self, connection_info, instance, force=False): LOG.debug("Detaching StorPool volume %s", connection_info['data']['volume'], instance=instance) - self.connector.disconnect_volume(connection_info['data'], None) + self.connector.disconnect_volume( + connection_info['data'], None, force=force) LOG.debug("Detached StorPool volume", instance=instance) def extend_volume(self, connection_info, instance, requested_size): diff --git a/nova/virt/libvirt/volume/volume.py b/nova/virt/libvirt/volume/volume.py index 6d650c80e64..f76c3618b27 100644 --- a/nova/virt/libvirt/volume/volume.py +++ b/nova/virt/libvirt/volume/volume.py @@ -135,7 +135,7 @@ def connect_volume(self, connection_info, instance): """Connect the volume.""" pass - def disconnect_volume(self, connection_info, instance): + def disconnect_volume(self, connection_info, instance, force=False): """Disconnect the volume.""" pass diff --git a/nova/virt/libvirt/volume/vzstorage.py b/nova/virt/libvirt/volume/vzstorage.py index 85ffb450765..babfdef55c6 100644 --- a/nova/virt/libvirt/volume/vzstorage.py +++ b/nova/virt/libvirt/volume/vzstorage.py @@ -126,9 +126,10 @@ def _connect_volume(connection_info, instance): return _connect_volume(connection_info, instance) - def disconnect_volume(self, connection_info, instance): + def disconnect_volume(self, connection_info, instance, force=False): """Detach the volume from instance_name.""" LOG.debug("calling os-brick to detach Vzstorage Volume", instance=instance) - self.connector.disconnect_volume(connection_info['data'], None) + self.connector.disconnect_volume( + connection_info['data'], None, force=force) LOG.debug("Disconnected Vzstorage Volume", instance=instance) diff --git a/nova/volume/cinder.py b/nova/volume/cinder.py index bf1e455bba4..f5328148d24 100644 --- a/nova/volume/cinder.py +++ b/nova/volume/cinder.py @@ -91,12 +91,14 @@ def _get_auth(context): # from them generated from 'context.get_admin_context' # which only set is_admin=True but is without token. # So add load_auth_plugin when this condition appear. + user_auth = None if context.is_admin and not context.auth_token: if not _ADMIN_AUTH: _ADMIN_AUTH = _load_auth_plugin(CONF) - return _ADMIN_AUTH - else: - return service_auth.get_auth_plugin(context) + user_auth = _ADMIN_AUTH + + # When user_auth = None, user_auth will be extracted from the context. + return service_auth.get_auth_plugin(context, user_auth=user_auth) # NOTE(efried): Bug #1752152 @@ -888,19 +890,23 @@ def attachment_update(self, context, attachment_id, connector, @retrying.retry(stop_max_attempt_number=5, retry_on_exception=lambda e: (isinstance(e, cinder_exception.ClientException) and - e.code == 500)) + e.code in (500, 504))) def attachment_delete(self, context, attachment_id): try: cinderclient( context, '3.44', skip_version_check=True).attachments.delete( attachment_id) except cinder_exception.ClientException as ex: - with excutils.save_and_reraise_exception(): - LOG.error('Delete attachment failed for attachment ' - '%(id)s. Error: %(msg)s Code: %(code)s', - {'id': attachment_id, - 'msg': str(ex), - 'code': getattr(ex, 'code', None)}) + if ex.code == 404: + LOG.warning('Attachment %(id)s does not exist. Ignoring.', + {'id': attachment_id}) + else: + with excutils.save_and_reraise_exception(): + LOG.error('Delete attachment failed for attachment ' + '%(id)s. Error: %(msg)s Code: %(code)s', + {'id': attachment_id, + 'msg': str(ex), + 'code': getattr(ex, 'code', None)}) @translate_attachment_exception def attachment_complete(self, context, attachment_id): diff --git a/releasenotes/notes/bug-1942329-22b08fa4b322881d.yaml b/releasenotes/notes/bug-1942329-22b08fa4b322881d.yaml new file mode 100644 index 00000000000..496508ca13a --- /dev/null +++ b/releasenotes/notes/bug-1942329-22b08fa4b322881d.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + As a fix for `bug 1942329 `_ + nova now updates the MAC address of the ``direct-physical`` ports during + mova operations to reflect the MAC address of the physical device on the + destination host. Those servers that were created before this fix need to be + moved or the port needs to be detached and the re-attached to synchronize the + MAC address. diff --git a/releasenotes/notes/bug-1944619-fix-live-migration-rollback.yaml b/releasenotes/notes/bug-1944619-fix-live-migration-rollback.yaml new file mode 100644 index 00000000000..b6c68ed49f2 --- /dev/null +++ b/releasenotes/notes/bug-1944619-fix-live-migration-rollback.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + Instances with hardware offloaded ovs ports no longer lose connectivity + after failed live migrations. The driver.rollback_live_migration_at_source + function is no longer called during during pre_live_migration rollback + which previously resulted in connectivity loss following a failed live + migration. See `Bug 1944619`_ for more details. + + .. _Bug 1944619: https://bugs.launchpad.net/nova/+bug/1944619 diff --git a/releasenotes/notes/bug-1970383-segment-scheduling-permissions-92ba907b10a9eb1c.yaml b/releasenotes/notes/bug-1970383-segment-scheduling-permissions-92ba907b10a9eb1c.yaml new file mode 100644 index 00000000000..88495079e75 --- /dev/null +++ b/releasenotes/notes/bug-1970383-segment-scheduling-permissions-92ba907b10a9eb1c.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + `Bug #1970383 `_: Fixes a + permissions error when using the + 'query_placement_for_routed_network_aggregates' scheduler variable, which + caused a traceback on instance creation for non-admin users. diff --git a/releasenotes/notes/bug-1978444-db46df5f3d5ea19e.yaml b/releasenotes/notes/bug-1978444-db46df5f3d5ea19e.yaml new file mode 100644 index 00000000000..6c198040745 --- /dev/null +++ b/releasenotes/notes/bug-1978444-db46df5f3d5ea19e.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + `Bug #1978444 `_: Now nova + retries deleting a volume attachment in case Cinder API returns + ``504 Gateway Timeout``. Also, ``404 Not Found`` is now ignored and + leaves only a warning message. diff --git a/releasenotes/notes/bug-1981813-vnic-type-change-9f3e16fae885b57f.yaml b/releasenotes/notes/bug-1981813-vnic-type-change-9f3e16fae885b57f.yaml new file mode 100644 index 00000000000..a5a3b7c8c2c --- /dev/null +++ b/releasenotes/notes/bug-1981813-vnic-type-change-9f3e16fae885b57f.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + `Bug #1981813 `_: Now nova + detects if the ``vnic_type`` of a bound port has been changed in neutron + and leaves an ERROR message in the compute service log as such change on a + bound port is not supported. Also the restart of the nova-compute service + will not crash any more after such port change. Nova will log an ERROR and + skip the initialization of the instance with such port during the startup. diff --git a/releasenotes/notes/bug-1982284-libvirt-handle-no-ram-info-was-set-99784934ed80fd72.yaml b/releasenotes/notes/bug-1982284-libvirt-handle-no-ram-info-was-set-99784934ed80fd72.yaml new file mode 100644 index 00000000000..943aa99a436 --- /dev/null +++ b/releasenotes/notes/bug-1982284-libvirt-handle-no-ram-info-was-set-99784934ed80fd72.yaml @@ -0,0 +1,11 @@ +--- +other: + - | + A workaround has been added to the libvirt driver to catch and pass + migrations that were previously failing with the error: + + ``libvirt.libvirtError: internal error: migration was active, but no RAM info was set`` + + See `bug 1982284`_ for more details. + + .. _bug 1982284: https://bugs.launchpad.net/nova/+bug/1982284 diff --git a/releasenotes/notes/bug-1983753-update-requestspec-pci_request-for-resize-a3c6b0a979db723f.yaml b/releasenotes/notes/bug-1983753-update-requestspec-pci_request-for-resize-a3c6b0a979db723f.yaml new file mode 100644 index 00000000000..89edd12b3d9 --- /dev/null +++ b/releasenotes/notes/bug-1983753-update-requestspec-pci_request-for-resize-a3c6b0a979db723f.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + `Bug #1941005 `_ is fixed. + During resize Nova now uses the PCI requests from the new flavor to select + the destination host. diff --git a/releasenotes/notes/bug-2061701-ephemeral-disk-fs-label-504484c4522e6d6a.yaml b/releasenotes/notes/bug-2061701-ephemeral-disk-fs-label-504484c4522e6d6a.yaml new file mode 100644 index 00000000000..5f4c22ca248 --- /dev/null +++ b/releasenotes/notes/bug-2061701-ephemeral-disk-fs-label-504484c4522e6d6a.yaml @@ -0,0 +1,6 @@ +fixes: + - | + Fixed an issue where certain server actions could fail for servers with + ephemeral disks due to filesystem label name length limitations + (VFAT, XFS, ...). Filesystem label name generation has been fixed for these + cases. See https://launchpad.net/bugs/2061701 for more details. diff --git a/releasenotes/notes/fix-group-policy-validation-with-deleted-groups-4f685fd1d6b84192.yaml b/releasenotes/notes/fix-group-policy-validation-with-deleted-groups-4f685fd1d6b84192.yaml new file mode 100644 index 00000000000..7f7d42bd0e0 --- /dev/null +++ b/releasenotes/notes/fix-group-policy-validation-with-deleted-groups-4f685fd1d6b84192.yaml @@ -0,0 +1,13 @@ +--- +fixes: + - | + When the server group policy validation upcall is enabled + nova will assert that the policy is not violated on move operations + and initial instance creation. As noted in `bug 1890244`_, if a + server was created in a server group and that group was later deleted + the validation upcall would fail due to an uncaught excpetion if the + server group was deleted. This prevented evacuate and other move + operations form functioning. This has now been fixed and nova will + ignore deleted server groups. + + .. _bug 1890244: https://bugs.launchpad.net/nova/+bug/1890244 diff --git a/releasenotes/notes/ignore-instance-task-state-for-evacuation-e000f141d0153638.yaml b/releasenotes/notes/ignore-instance-task-state-for-evacuation-e000f141d0153638.yaml new file mode 100644 index 00000000000..46ebf0bd2d0 --- /dev/null +++ b/releasenotes/notes/ignore-instance-task-state-for-evacuation-e000f141d0153638.yaml @@ -0,0 +1,11 @@ +--- +fixes: + - | + If compute service is down in source node and user try to stop + instance, instance gets stuck at powering-off, hence evacuation fails with + msg: Cannot 'evacuate' instance while it is in + task_state powering-off. + It is now possible for evacuation to ignore the vm task state. + For more details see: `bug 1978983`_ + + .. _`bug 1978983`: https://bugs.launchpad.net/nova/+bug/1978983 \ No newline at end of file diff --git a/releasenotes/notes/nova-manage-image-property-bug-2078999-c493fc259d316c24.yaml b/releasenotes/notes/nova-manage-image-property-bug-2078999-c493fc259d316c24.yaml new file mode 100644 index 00000000000..03123855e0e --- /dev/null +++ b/releasenotes/notes/nova-manage-image-property-bug-2078999-c493fc259d316c24.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Before the `Bug 2078999 `_ was fixed, + the ``nova-manage image_property set`` command would update the image properties + embedded in the instance but would not update the ones in the request specs. This + led to an unexpected rollback of the image properties that were updated by the + command after an instance migration. diff --git a/releasenotes/notes/remove-default-cputune-shares-values-85d5ddf4b8e24eaa.yaml b/releasenotes/notes/remove-default-cputune-shares-values-85d5ddf4b8e24eaa.yaml new file mode 100644 index 00000000000..9dd0987bb8c --- /dev/null +++ b/releasenotes/notes/remove-default-cputune-shares-values-85d5ddf4b8e24eaa.yaml @@ -0,0 +1,15 @@ +upgrade: + - | + In the libvirt driver, the default value of the ```` + element has been removed, and is now left to libvirt to decide. This is + because allowed values are platform dependant, and the previous code was + not guaranteed to be supported on all platforms. If any of your flavors are + using the quota:cpu_shares extra spec, you may need to resize to a + supported value before upgrading. + + To facilitate the transition to no Nova default for ````, + its value will be removed during live migration unless a value is set in + the ``quota:cpu_shares`` extra spec. This can cause temporary CPU + starvation for the live migrated instance if other instances on the + destination host still have the old default ```` value. To + fix this, hard reboot, cold migrate, or live migrate the other instances. diff --git a/releasenotes/notes/rescue-volume-based-instance-c6e3fba236d90be7.yaml b/releasenotes/notes/rescue-volume-based-instance-c6e3fba236d90be7.yaml new file mode 100644 index 00000000000..7e80059b801 --- /dev/null +++ b/releasenotes/notes/rescue-volume-based-instance-c6e3fba236d90be7.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fix rescuing volume based instance by adding a check for 'hw_rescue_disk' + and 'hw_rescue_device' properties in image metadata before attempting + to rescue instance. diff --git a/releasenotes/notes/service-user-token-421d067c16257782.yaml b/releasenotes/notes/service-user-token-421d067c16257782.yaml new file mode 100644 index 00000000000..d3af14fbb85 --- /dev/null +++ b/releasenotes/notes/service-user-token-421d067c16257782.yaml @@ -0,0 +1,11 @@ +upgrade: + - | + Configuration of service user tokens is now **required** for all Nova services + to ensure security of block-storage volume data. + + All Nova configuration files must configure the ``[service_user]`` section as + described in the `documentation`__. + + See https://bugs.launchpad.net/nova/+bug/2004555 for more details. + + __ https://docs.openstack.org/nova/latest/admin/configuration/service-user-token.html diff --git a/releasenotes/notes/skip-compare-cpu-on-dest-6ae419ddd61fd0f8.yaml b/releasenotes/notes/skip-compare-cpu-on-dest-6ae419ddd61fd0f8.yaml new file mode 100644 index 00000000000..e7cd4041b16 --- /dev/null +++ b/releasenotes/notes/skip-compare-cpu-on-dest-6ae419ddd61fd0f8.yaml @@ -0,0 +1,24 @@ +--- +issues: + - | + Nova's use of libvirt's compareCPU() API served its purpose over the + years, but its design limitations break live migration in subtle + ways. For example, the compareCPU() API compares against the host + physical CPUID. Some of the features from this CPUID aren not + exposed by KVM, and then there are some features that KVM emulates + that are not in the host CPUID. The latter can cause bogus live + migration failures. + + With QEMU >=2.9 and libvirt >= 4.4.0, libvirt will do the right + thing in terms of CPU compatibility checks on the destination host + during live migration. Nova satisfies these minimum version + requirements by a good margin. So, this workaround provides a way to + skip the CPU comparison check on the destination host before + migrating a guest, and let libvirt handle it correctly. + + This workaround will be deprecated and removed once Nova replaces + the older libvirt APIs with their newer counterparts. The work is + being tracked via this `blueprint + cpu-selection-with-hypervisor-consideration`_. + + .. _blueprint cpu-selection-with-hypervisor-consideration: https://blueprints.launchpad.net/nova/+spec/cpu-selection-with-hypervisor-consideration diff --git a/releasenotes/notes/skip-hypervisor-version-check-on-lm-a87f2dcb4f8bf0f2.yaml b/releasenotes/notes/skip-hypervisor-version-check-on-lm-a87f2dcb4f8bf0f2.yaml new file mode 100644 index 00000000000..00fe6a24c70 --- /dev/null +++ b/releasenotes/notes/skip-hypervisor-version-check-on-lm-a87f2dcb4f8bf0f2.yaml @@ -0,0 +1,13 @@ +--- +feature: + - | + Adds a workaround that allows one to disable hypervisor + version-check on live migration. This workaround option can be + useful in certain scenarios when upgrading. E.g. if you want to + relocate all instances off a compute node due to an emergency + hardware issue, and you only have another old compute node ready at + the time. + + To enable this, use the config attribute + ``[workarounds]skip_hypervisor_version_check_on_lm=True`` in + ``nova.conf``. The option defaults to ``False``. diff --git a/releasenotes/notes/vdpa-move-ops-a7b3799807807a92.yaml b/releasenotes/notes/vdpa-move-ops-a7b3799807807a92.yaml new file mode 100644 index 00000000000..2580f73d35b --- /dev/null +++ b/releasenotes/notes/vdpa-move-ops-a7b3799807807a92.yaml @@ -0,0 +1,11 @@ +--- +fixes: + - | + When vDPA was first introduced move operations were implemented in the code + but untested either in a real environment or in functional tests. Due to + this gap nova elected to block move operations for instance with vDPA + devices. All move operations except for live migration have now been tested + and found to indeed work so the API blocks have now been removed and + functional tests introduced. Other operations such as suspend and + live migration require code changes to support and will be enabled as new + features in the future. diff --git a/tools/check-cherry-picks.sh b/tools/check-cherry-picks.sh index 46cef8c2250..fe75867e59f 100755 --- a/tools/check-cherry-picks.sh +++ b/tools/check-cherry-picks.sh @@ -1,7 +1,8 @@ #!/bin/sh # # A tool to check the cherry-pick hashes from the current git commit message -# to verify that they're all on either master or stable/ branches +# to verify that they're all on either master, stable/ or unmaintained/ +# branches # commit_hash="" @@ -14,7 +15,7 @@ if [ $parent_number -eq 2 ]; then commit_hash=$(git show --format='%P' --quiet | awk '{print $NF}') fi -if git show --format='%aE' HEAD~ --quiet | grep -qi 'infra-root@openstack.org'; then +if git show --format='%aE' --quiet $commit_hash | grep -qi 'infra-root@openstack.org'; then echo 'Bot generated change; ignoring' exit 0 fi @@ -23,17 +24,20 @@ hashes=$(git show --format='%b' --quiet $commit_hash | sed -nr 's/^.cherry picke checked=0 branches+="" for hash in $hashes; do - branch=$(git branch -a --contains "$hash" 2>/dev/null| grep -oE '(master|stable/[a-z]+)') + branch=$(git branch -a --contains "$hash" 2>/dev/null| grep -oE '(master|stable/[a-z0-9.]+|unmaintained/[a-z0-9.]+)') if [ $? -ne 0 ]; then - echo "Cherry pick hash $hash not on any master or stable branches" - exit 1 + branch=$(git tag --contains "$hash" 2>/dev/null| grep -oE '([0-9.]+-eol)') + if [ $? -ne 0 ]; then + echo "Cherry pick hash $hash not on any master, stable, unmaintained or EOL'd branches" + exit 1 + fi fi branches+=" $branch" checked=$(($checked + 1)) done if [ $checked -eq 0 ]; then - if ! grep -q '^defaultbranch=stable/' .gitreview; then + if ! grep -qE '^defaultbranch=(stable|unmaintained)/' .gitreview; then echo "Checked $checked cherry-pick hashes: OK" exit 0 else diff --git a/tox.ini b/tox.ini index 2875f3374ad..8df61ef43bb 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,11 @@ envlist = py39,functional,pep8 # env and ignore basepython inherited from [testenv] if we set # ignore_basepython_conflict. ignore_basepython_conflict = True +# Cap setuptools via virtualenv to prevent compatibility issue with yoga +# branch's upper constraint of 'packaging' package (21.3). +requires = + virtualenv<20.26.4 + tox<4 [testenv] basepython = python3 @@ -26,7 +31,7 @@ setenv = # TODO(stephenfin): Remove once we bump our upper-constraint to SQLAlchemy 2.0 SQLALCHEMY_WARN_20=1 deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/yoga} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt extras = @@ -227,7 +232,8 @@ description = # Note that we don't use {[testenv]deps} for deps here because we don't want # to install (test-)requirements.txt for docs. deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/yoga} + -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt extras = commands = @@ -389,10 +395,3 @@ deps = bindep extras = commands = bindep test - -[testenv:lower-constraints] -usedevelop = False -deps = - -c{toxinidir}/lower-constraints.txt - -r{toxinidir}/test-requirements.txt - -r{toxinidir}/requirements.txt