Compare commits

..

13 Commits

Author SHA1 Message Date
18b1232db4 Update master for stable/2026.1
Add file to the reno documentation build to show release notes for
stable/2026.1.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/2026.1.

Sem-Ver: feature
Change-Id: Icaf974beefa4e54680fa161370a54ffd9487c0ca
Signed-off-by: OpenStack Release Bot <infra-root@openstack.org>
Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/add_release_note_page.sh
2026-03-04 18:25:43 +00:00
Slawek Kaplonski
6e5eb4da33 [fwaas] Don't ignore missing fw related resources
Previously when fwaas osc plugin was going to look for firewall related
resources, like e.g. asking neutron for firewall group or policy was
done with ignore_missing=True which means that openstack SDK client
didn't raise exception when requested object was not found on the
server.
That led to the weird and unclear error message displayed by the
OpenStack client which said something about "'NoneType' object is not
subscriptable".

This patch adds "ignore_missing=True" to all those "find_*" methods used
in the fwaas osc plugin. That way proper exception is raised by the SDK
client and valid error message is displayed to the user by OSC.

Closes-bug: #2142458

Change-Id: I309a5dbf61c65d5837d4ea2b3235aa41269ae73d
Signed-off-by: Slawek Kaplonski <skaplons@redhat.com>
2026-02-26 11:04:47 +01:00
Slawek Kaplonski
f7c085005d [FWaaS] Remove client side protocol validation
There is no need to limit available choices for the firewall rule's
protocol on the client side. Neutron-fwaas plugin on the server side
will do the validation in the same way as for security group rules.
And for SG rules OSC is not validating nor limiting choices on the
client's side at all.

Closes-bug: #2142479

Change-Id: I8c02a2232601c2ab6655c458aa0365102b3b5e2d
Signed-off-by: Slawek Kaplonski <skaplons@redhat.com>
2026-02-23 16:57:58 +01:00
Takashi Kajinami
d9a3518f75 Bump flake8-import-order
The 1.18.2 release requires pkg_resources.

The dependency was removed in 1.19.0 .

Change-Id: I25106085e68dfc13a091af0e9e365287dac17308
Signed-off-by: Takashi Kajinami <kajinamit@oss.nttdata.com>
2026-02-21 00:18:54 +09:00
Zuul
efa0ccb944 Merge "Revert "Add Tap-as-a-Service client code"" 2025-12-12 17:29:15 +00:00
Zhan Zhang
8f72d77812 v2_0: Use 'bindings' when listing port bindings
This commit fixes a bug in v2_0 client's "list_port_bindings"
function, where it uses "port_bindings" to access Neutron's
response, instead of "bindings" [0].

[0]: https://docs.openstack.org/api-ref/network/v2/index.html#show-port-binding-of-a-port

Closes-Bug: #2130459
Change-Id: I32ef753ec212b55f698e3844e043f68b22992ead
Signed-off-by: Zhan Zhang <zzhang953@bloomberg.net>
2025-11-11 14:05:44 -05:00
Miro Tomaska
a991ac87c7 Revert "Add Tap-as-a-Service client code"
This reverts commit 01ffc4684a.

Reason for revert:
In the last PTG we decided that the best location for the stadium projects osc client code would be the openstackclient repo. A patch was proposed for tapaas, see depends-on link below. Other projects will follow

Change-Id: Iaad2080f0ef552a0c0a00635bea48130cfc327a4
Depends-On: https://review.opendev.org/c/openstack/python-openstackclient/+/963445
Signed-off-by: Miro Tomaska <mtomaska@redhat.com>
2025-11-06 21:39:01 +00:00
01d0553ca4 reno: Update master for unmaintained/2024.1
Update the 2024.1 release notes configuration to build from
unmaintained/2024.1.

Change-Id: Ic2592d22b36b22f3cf48a21d81bfb0b41b31ae86
Signed-off-by: OpenStack Release Bot <infra-root@openstack.org>
Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/change_reno_branch_to_unmaintained.sh
2025-10-31 12:06:40 +00:00
Zuul
66ccb4569d Merge "Add Tap-as-a-Service client code" 2025-09-19 16:22:01 +00:00
Zuul
1aee34b246 Merge "Drop unused os-client-config" 2025-09-15 20:36:02 +00:00
Mohammed Naser
01ffc4684a Add Tap-as-a-Service client code
This commit adds the Tap-as-a-Service client code as-is based
on the discussion that it can now be included into the Neutron
client[1].

[1]: https://lists.openstack.org/archives/list/openstack-discuss@lists.openstack.org/message/PCITF3UPLEEVQB7EVJ2MB6FLAI3RFYSR/

Change-Id: I82b3229b3f9ac7ef57324d0a611527c77d26c3b6
Signed-off-by: Mohammed Naser <mnaser@vexxhost.com>
2025-09-12 11:37:06 -04:00
2f170ce6aa Update master for stable/2025.2
Add file to the reno documentation build to show release notes for
stable/2025.2.

Use pbr instruction to increment the minor version number
automatically so that master versions are higher than the versions on
stable/2025.2.

Sem-Ver: feature
Change-Id: I97f7bba4f875b98d98f03ba60ee394a1aa64a3fd
Signed-off-by: OpenStack Release Bot <infra-root@openstack.org>
Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/add_release_note_page.sh
2025-09-04 13:44:40 +00:00
Takashi Kajinami
275924ecc8 Drop unused os-client-config
It was deprecated[1] after the code was merged into openstacksdk[2].
It's not actually used by any code in this repository.

[1] https://review.opendev.org/c/openstack/os-client-config/+/549307
[2] https://review.opendev.org/c/openstack/openstacksdk/+/518128

Change-Id: Ia93d2952ca09fc70a970a85789775441ed114d49
Signed-off-by: Takashi Kajinami <kajinamit@oss.nttdata.com>
2025-07-02 23:23:28 +09:00
13 changed files with 74 additions and 47 deletions

View File

@@ -2,4 +2,3 @@
host=review.opendev.org
port=29418
project=openstack/python-neutronclient.git
defaultbranch=stable/2025.2

View File

@@ -31,7 +31,6 @@
- openstack/keystoneauth
- openstack/neutron
- openstack/neutron-lib
- openstack/os-client-config
- openstack/python-cinderclient
- openstack/python-glanceclient
- openstack/python-ironicclient

View File

@@ -129,19 +129,19 @@ def _get_common_attrs(client_manager, parsed_args, is_create=True):
if (parsed_args.ingress_firewall_policy and
parsed_args.no_ingress_firewall_policy):
attrs['ingress_firewall_policy_id'] = client.find_firewall_policy(
parsed_args.ingress_firewall_policy)['id']
parsed_args.ingress_firewall_policy, ignore_missing=False)['id']
elif parsed_args.ingress_firewall_policy:
attrs['ingress_firewall_policy_id'] = client.find_firewall_policy(
parsed_args.ingress_firewall_policy)['id']
parsed_args.ingress_firewall_policy, ignore_missing=False)['id']
elif parsed_args.no_ingress_firewall_policy:
attrs['ingress_firewall_policy_id'] = None
if (parsed_args.egress_firewall_policy and
parsed_args.no_egress_firewall_policy):
attrs['egress_firewall_policy_id'] = client.find_firewall_policy(
parsed_args.egress_firewall_policy)['id']
parsed_args.egress_firewall_policy, ignore_missing=False)['id']
elif parsed_args.egress_firewall_policy:
attrs['egress_firewall_policy_id'] = client.find_firewall_policy(
parsed_args.egress_firewall_policy)['id']
parsed_args.egress_firewall_policy, ignore_missing=False)['id']
elif parsed_args.no_egress_firewall_policy:
attrs['egress_firewall_policy_id'] = None
if parsed_args.share:
@@ -165,7 +165,7 @@ def _get_common_attrs(client_manager, parsed_args, is_create=True):
ports.append(client.find_port(p)['id'])
if not is_create:
ports += client.find_firewall_group(
parsed_args.firewall_group)['ports']
parsed_args.firewall_group, ignore_missing=False)['ports']
attrs['ports'] = sorted(set(ports))
elif parsed_args.no_port:
attrs['ports'] = []
@@ -220,7 +220,8 @@ class DeleteFirewallGroup(command.Command):
result = 0
for fwg in parsed_args.firewall_group:
try:
fwg_id = client.find_firewall_group(fwg)['id']
fwg_id = client.find_firewall_group(
fwg, ignore_missing=False)['id']
client.delete_firewall_group(fwg_id)
except Exception as e:
result += 1
@@ -281,7 +282,8 @@ class SetFirewallGroup(command.Command):
def take_action(self, parsed_args):
client = self.app.client_manager.network
fwg_id = client.find_firewall_group(parsed_args.firewall_group)['id']
fwg_id = client.find_firewall_group(
parsed_args.firewall_group, ignore_missing=False)['id']
attrs = _get_common_attrs(self.app.client_manager, parsed_args,
is_create=False)
try:
@@ -305,7 +307,8 @@ class ShowFirewallGroup(command.ShowOne):
def take_action(self, parsed_args):
client = self.app.client_manager.network
fwg_id = client.find_firewall_group(parsed_args.firewall_group)['id']
fwg_id = client.find_firewall_group(
parsed_args.firewall_group, ignore_missing=False)['id']
obj = client.get_firewall_group(fwg_id)
display_columns, columns = utils.get_osc_show_columns_for_sdk_resource(
obj, _attr_map_dict, ['location', 'tenant_id'])
@@ -366,7 +369,7 @@ class UnsetFirewallGroup(command.Command):
attrs['admin_state_up'] = False
if parsed_args.port:
old = client.find_firewall_group(
parsed_args.firewall_group)['ports']
parsed_args.firewall_group, ignore_missing=False)['ports']
new = [client.find_port(r)['id'] for r in parsed_args.port]
attrs['ports'] = sorted(list(set(old) - set(new)))
if parsed_args.all_port:
@@ -375,7 +378,8 @@ class UnsetFirewallGroup(command.Command):
def take_action(self, parsed_args):
client = self.app.client_manager.network
fwg_id = client.find_firewall_group(parsed_args.firewall_group)['id']
fwg_id = client.find_firewall_group(
parsed_args.firewall_group, ignore_missing=False)['id']
attrs = self._get_attrs(client, parsed_args)
try:
client.update_firewall_group(fwg_id, **attrs)

View File

@@ -67,16 +67,18 @@ def _get_common_attrs(client_manager, parsed_args, is_create=True):
if parsed_args.firewall_rule and parsed_args.no_firewall_rule:
_firewall_rules = []
for f in parsed_args.firewall_rule:
_firewall_rules.append(client.find_firewall_rule(f)['id'])
_firewall_rules.append(
client.find_firewall_rule(f, ignore_missing=False)['id'])
attrs[const.FWRS] = _firewall_rules
elif parsed_args.firewall_rule:
rules = []
if not is_create:
foobar = client.find_firewall_policy(
parsed_args.firewall_policy)
parsed_args.firewall_policy, ignore_missing=False)
rules += foobar[const.FWRS]
for f in parsed_args.firewall_rule:
rules.append(client.find_firewall_rule(f)['id'])
rules.append(
client.find_firewall_rule(f, ignore_missing=False)['id'])
attrs[const.FWRS] = rules
elif parsed_args.no_firewall_rule:
attrs[const.FWRS] = []
@@ -173,7 +175,8 @@ class DeleteFirewallPolicy(command.Command):
result = 0
for fwp in parsed_args.firewall_policy:
try:
fwp_id = client.find_firewall_policy(fwp)['id']
fwp_id = client.find_firewall_policy(
fwp, ignore_missing=False)['id']
client.delete_firewall_policy(fwp_id)
except Exception as e:
result += 1
@@ -220,12 +223,12 @@ class FirewallPolicyInsertRule(command.Command):
if 'insert_before' in parsed_args:
if parsed_args.insert_before:
_insert_before = client.find_firewall_rule(
parsed_args.insert_before)['id']
parsed_args.insert_before, ignore_missing=False)['id']
_insert_after = ''
if 'insert_after' in parsed_args:
if parsed_args.insert_after:
_insert_after = client.find_firewall_rule(
parsed_args.insert_after)['id']
parsed_args.insert_after, ignore_missing=False)['id']
return {'firewall_rule_id': _rule_id,
'insert_before': _insert_before,
'insert_after': _insert_after}
@@ -233,7 +236,7 @@ class FirewallPolicyInsertRule(command.Command):
def take_action(self, parsed_args):
client = self.app.client_manager.network
policy_id = client.find_firewall_policy(
parsed_args.firewall_policy)['id']
parsed_args.firewall_policy, ignore_missing=False)['id']
body = self.args2body(parsed_args)
client.insert_rule_into_policy(policy_id, **body)
rule_id = body['firewall_rule_id']
@@ -261,7 +264,7 @@ class FirewallPolicyRemoveRule(command.Command):
def take_action(self, parsed_args):
client = self.app.client_manager.network
policy_id = client.find_firewall_policy(
parsed_args.firewall_policy)['id']
parsed_args.firewall_policy, ignore_missing=False)['id']
fwr_id = _get_required_firewall_rule(client, parsed_args)
body = {'firewall_rule_id': fwr_id}
client.remove_rule_from_policy(policy_id, **body)
@@ -322,7 +325,7 @@ class SetFirewallPolicy(command.Command):
def take_action(self, parsed_args):
client = self.app.client_manager.network
fwp_id = client.find_firewall_policy(
parsed_args.firewall_policy)['id']
parsed_args.firewall_policy, ignore_missing=False)['id']
attrs = _get_common_attrs(self.app.client_manager,
parsed_args, is_create=False)
try:
@@ -347,7 +350,7 @@ class ShowFirewallPolicy(command.ShowOne):
def take_action(self, parsed_args):
client = self.app.client_manager.network
fwp_id = client.find_firewall_policy(
parsed_args.firewall_policy)['id']
parsed_args.firewall_policy, ignore_missing=False)['id']
obj = client.get_firewall_policy(fwp_id)
display_columns, columns = utils.get_osc_show_columns_for_sdk_resource(
obj, _attr_map_dict, ['location', 'tenant_id'])
@@ -359,7 +362,8 @@ def _get_required_firewall_rule(client, parsed_args):
if not parsed_args.firewall_rule:
msg = (_("Firewall rule (name or ID) is required."))
raise exceptions.CommandError(msg)
return client.find_firewall_rule(parsed_args.firewall_rule)['id']
return client.find_firewall_rule(
parsed_args.firewall_rule, ignore_missing=False)['id']
class UnsetFirewallPolicy(command.Command):
@@ -399,10 +403,11 @@ class UnsetFirewallPolicy(command.Command):
if parsed_args.firewall_rule:
current = client.find_firewall_policy(
parsed_args.firewall_policy)[const.FWRS]
parsed_args.firewall_policy, ignore_missing=False)[const.FWRS]
removed = []
for f in set(parsed_args.firewall_rule):
removed.append(client.find_firewall_rule(f)['id'])
removed.append(
client.find_firewall_rule(f, ignore_missing=False)['id'])
attrs[const.FWRS] = [r for r in current if r not in removed]
if parsed_args.all_firewall_rule:
attrs[const.FWRS] = []
@@ -415,7 +420,7 @@ class UnsetFirewallPolicy(command.Command):
def take_action(self, parsed_args):
client = self.app.client_manager.network
fwp_id = client.find_firewall_policy(
parsed_args.firewall_policy)['id']
parsed_args.firewall_policy, ignore_missing=False)['id']
attrs = self._get_attrs(self.app.client_manager, parsed_args)
try:
client.update_firewall_policy(fwp_id, **attrs)

View File

@@ -86,9 +86,12 @@ def _get_common_parser(parser):
help=_('Description of the firewall rule'))
parser.add_argument(
'--protocol',
choices=['tcp', 'udp', 'icmp', 'any'],
type=nc_utils.convert_to_lowercase,
help=_('Protocol for the firewall rule'))
help=_('IP protocol (ah, dccp, egp, esp, gre, icmp, igmp, '
'ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt, ipv6-opts, '
'ipv6-route, ospf, pgm, rsvp, sctp, tcp, udp, udplite, '
'vrrp and integer representations [0-255] or any; '
'default: any (all protocols))'))
parser.add_argument(
'--action',
choices=['allow', 'deny', 'reject'],
@@ -226,12 +229,12 @@ def _get_common_attrs(client_manager, parsed_args, is_create=True):
attrs['shared'] = False
if parsed_args.source_firewall_group:
attrs['source_firewall_group_id'] = client.find_firewall_group(
parsed_args.source_firewall_group)['id']
parsed_args.source_firewall_group, ignore_missing=False)['id']
if parsed_args.no_source_firewall_group:
attrs['source_firewall_group_id'] = None
if parsed_args.destination_firewall_group:
attrs['destination_firewall_group_id'] = client.find_firewall_group(
parsed_args.destination_firewall_group)['id']
parsed_args.destination_firewall_group, ignore_missing=False)['id']
if parsed_args.no_destination_firewall_group:
attrs['destination_firewall_group_id'] = None
return attrs
@@ -281,7 +284,8 @@ class DeleteFirewallRule(command.Command):
result = 0
for fwr in parsed_args.firewall_rule:
try:
fwr_id = client.find_firewall_rule(fwr)['id']
fwr_id = client.find_firewall_rule(
fwr, ignore_missing=False)['id']
client.delete_firewall_rule(fwr_id)
except Exception as e:
result += 1
@@ -358,7 +362,8 @@ class SetFirewallRule(command.Command):
client = self.app.client_manager.network
attrs = _get_common_attrs(self.app.client_manager,
parsed_args, is_create=False)
fwr_id = client.find_firewall_rule(parsed_args.firewall_rule)['id']
fwr_id = client.find_firewall_rule(
parsed_args.firewall_rule, ignore_missing=False)['id']
try:
client.update_firewall_rule(fwr_id, **attrs)
except Exception as e:
@@ -380,7 +385,8 @@ class ShowFirewallRule(command.ShowOne):
def take_action(self, parsed_args):
client = self.app.client_manager.network
fwr_id = client.find_firewall_rule(parsed_args.firewall_rule)['id']
fwr_id = client.find_firewall_rule(
parsed_args.firewall_rule, ignore_missing=False)['id']
obj = client.get_firewall_rule(fwr_id)
display_columns, columns = utils.get_osc_show_columns_for_sdk_resource(
obj, _attr_map_dict, ['location', 'tenant_id'])
@@ -458,7 +464,8 @@ class UnsetFirewallRule(command.Command):
def take_action(self, parsed_args):
client = self.app.client_manager.network
attrs = self._get_attrs(self.app.client_manager, parsed_args)
fwr_id = client.find_firewall_rule(parsed_args.firewall_rule)['id']
fwr_id = client.find_firewall_rule(
parsed_args.firewall_rule, ignore_missing=False)['id']
try:
client.update_firewall_rule(fwr_id, **attrs)
except Exception as e:

View File

@@ -215,7 +215,7 @@ class TestCreateFirewallGroup(TestFirewallGroup, common.TestCreateFWaaS):
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
headers, data = self.cmd.take_action(parsed_args)
self.networkclient.find_firewall_policy.assert_called_once_with(
ingress_policy)
ingress_policy, ignore_missing=False)
self.check_results(headers, data, request)
@@ -236,7 +236,7 @@ class TestCreateFirewallGroup(TestFirewallGroup, common.TestCreateFWaaS):
headers, data = self.cmd.take_action(parsed_args)
self.networkclient.find_firewall_policy.assert_called_once_with(
egress_policy)
egress_policy, ignore_missing=False)
self.check_results(headers, data, request)
def test_create_with_all_params(self):
@@ -398,15 +398,15 @@ class TestSetFirewallGroup(TestFirewallGroup, common.TestSetFWaaS):
# 1. Find specified firewall_group
if self.networkclient.find_firewall_group.call_count == 1:
self.networkclient.find_firewall_group.assert_called_with(
target)
target, ignore_missing=False)
# 2. Find specified 'ingress_firewall_policy'
if self.networkclient.find_firewall_policy.call_count == 1:
self.networkclient.find_firewall_policy.assert_called_with(
ingress_policy)
ingress_policy, ignore_missing=False)
# 3. Find specified 'ingress_firewall_policy'
if self.networkclient.find_firewall_policy.call_count == 2:
self.networkclient.find_firewall_policy.assert_called_with(
egress_policy)
egress_policy, ignore_missing=False)
return {'id': args[0]}
self.networkclient.find_firewall_group.side_effect = _mock_fwg_policy
@@ -439,7 +439,7 @@ class TestSetFirewallGroup(TestFirewallGroup, common.TestSetFWaaS):
# 1. Find specified firewall_group
if self.networkclient.find_firewall_group.call_count in [1, 2]:
self.networkclient.find_firewall_group.assert_called_with(
target)
target, ignore_missing=False)
return {'id': args[0], 'ports': _fwg['ports']}
# 2. Find specified 'port' #1
if self.networkclient.find_port.call_count == 1:
@@ -687,7 +687,7 @@ class TestUnsetFirewallGroup(TestFirewallGroup, common.TestUnsetFWaaS):
# 1. Find specified firewall_group
if self.networkclient.find_firewall_group.call_count in [1, 2]:
self.networkclient.find_firewall_group.assert_called_with(
target)
target, ignore_missing=False)
return {'id': args[0], 'ports': _fwg['ports']}
# 2. Find specified firewall_group and refer 'ports' attribute
if self.networkclient.find_port.call_count == 2:

View File

@@ -3,4 +3,4 @@
===========================
.. release-notes::
:branch: stable/2024.1
:branch: unmaintained/2024.1

View File

@@ -0,0 +1,6 @@
===========================
2025.2 Series Release Notes
===========================
.. release-notes::
:branch: stable/2025.2

View File

@@ -0,0 +1,6 @@
===========================
2026.1 Series Release Notes
===========================
.. release-notes::
:branch: stable/2026.1

View File

@@ -6,6 +6,8 @@
:maxdepth: 1
unreleased
2026.1
2025.2
2025.1
2024.2
2024.1

View File

@@ -11,7 +11,6 @@ oslo.i18n>=3.15.3 # Apache-2.0
oslo.log>=3.36.0 # Apache-2.0
oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0
oslo.utils>=3.33.0 # Apache-2.0
os-client-config>=1.28.0 # Apache-2.0
keystoneauth1>=3.8.0 # Apache-2.0
# keystoneclient is used only by neutronclient.osc.utils
# TODO(amotoki): Drop this after osc.utils has no dependency on keystoneclient

View File

@@ -3,7 +3,7 @@ hacking>=6.1.0,<6.2.0 # Apache-2.0
bandit!=1.6.0,>=1.1.0 # Apache-2.0
coverage!=4.4,>=4.0 # Apache-2.0
fixtures>=3.0.0 # Apache-2.0/BSD
flake8-import-order>=0.18.0,<0.19.0 # LGPLv3
flake8-import-order>=0.19.0,<0.20.0 # LGPLv3
oslotest>=3.2.0 # Apache-2.0
osprofiler>=2.3.0 # Apache-2.0
python-openstackclient>=3.12.0 # Apache-2.0

View File

@@ -12,7 +12,7 @@ setenv = VIRTUAL_ENV={envdir}
LC_ALL=C
PYTHONWARNINGS=default::DeprecationWarning
usedevelop = True
deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2025.2}
deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
-r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
# Delete bytecodes from normal directories before running tests.
@@ -46,7 +46,7 @@ commands =
[testenv:docs]
deps =
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2025.2}
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
-r{toxinidir}/doc/requirements.txt
commands = sphinx-build -W -b html doc/source doc/build/html
@@ -60,7 +60,7 @@ commands =
[testenv:releasenotes]
deps =
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2025.2}
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
-r{toxinidir}/doc/requirements.txt
commands = sphinx-build -a -E -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html