Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config restore: samba share exports not consistently restored #2847 #2864

Conversation

phillxnet
Copy link
Member

Account for Share ID native transform requirement re SMB share export. Cross-references, within config-backup file, Share ID used in each SMB export to retrieve prior Share name. Share name is then used to retrieve native DB Share ID for SMB share export restore process.

Assumes Share name continuity across config-backup/restore process.

Includes:

  • Incidental additions of more type hinting.
  • Additional docstrings.

Fixes #2847

…ckstor#2847

Account for Share ID native transform requirement re SMB share export.
Cross-references, within config-backup file, Share ID used in each
SMB export to retrieve prior Share name. Share name is then used
to retrieve native DB Share ID for SMB share export restore process.

Assumes Share name continuity across config-backup/restore process.

Includes:
- Incidental additions of more type hinting.
- Additional docstrings.
@phillxnet
Copy link
Member Author

phillxnet commented Jul 5, 2024

Testing

"Backup current config" was used on a system with two SMB shares exported. This restored (replayed) find on the same system as expected.

The config backup file was then downloaded, unzipped, and edited to change the ID of one of the associated SMB export shares (both it's canonical storageadmin.share, and its associated storageadmin.sambashare entries) so that it no longer tallied to the local DB instance:

Before

SMB export

{"model": "storageadmin.sambashare", "pk": 1, "fields": {"share": 3, "path": "/mnt2/samba-export", "comment": "Samba-Export", "browsable": "yes", "read_only": "no", "guest_ok": "no", "shadow_copy": false, "time_machine": false, "snapshot_prefix": null}},
{"model": "storageadmin.sambashare", "pk": 2, "fields": {"share": 4, "path": "/mnt2/samba-export2", "comment": "Samba-Export", "browsable": "no", "read_only": "no", "guest_ok": "no", "shadow_copy": false, "time_machine": false, "snapshot_prefix": null}}

Shares

{"model": "storageadmin.share", "pk": 3, "fields": {"pool": 1, "qgroup": "0/262", "pqgroup": "2015/44", "name": "samba-export", "uuid": null, "size": 1048576, "owner": "root", "group": "root", "perms": "755", "toc": "2024-07-04T13:41:07.078Z", "subvol_name": "samba-export", "replica": false, "compression_algo": "no", "rusage": 16, "eusage": 16, "pqgroup_rusage": 16, "pqgroup_eusage": 16}},
{"model": "storageadmin.share", "pk": 4, "fields": {"pool": 1, "qgroup": "0/263", "pqgroup": "2015/45", "name": "samba-export2", "uuid": null, "size": 1048576, "owner": "root", "group": "root", "perms": "755", "toc": "2024-07-04T13:41:07.102Z", "subvol_name": "samba-export2", "replica": false, "compression_algo": "no", "rusage": 16, "eusage": 16, "pqgroup_rusage": 16, "pqgroup_eusage": 16}}

After (Share ID 3 to 13 change)

SMB export

{"model": "storageadmin.sambashare", "pk": 1, "fields": {"share": 13, "path": "/mnt2/samba-export", "comment": "Samba-Export", "browsable": "yes", "read_only": "no", "guest_ok": "no", "shadow_copy": false, "time_machine": false, "snapshot_prefix": null}},
{"model": "storageadmin.sambashare", "pk": 2, "fields": {"share": 4, "path": "/mnt2/samba-export2", "comment": "Samba-Export", "browsable": "no", "read_only": "no", "guest_ok": "no", "shadow_copy": false, "time_machine": false, "snapshot_prefix": null}}

Shares

{"model": "storageadmin.share", "pk": 13, "fields": {"pool": 1, "qgroup": "0/262", "pqgroup": "2015/44", "name": "samba-export", "uuid": null, "size": 1048576, "owner": "root", "group": "root", "perms": "755", "toc": "2024-07-04T13:41:07.078Z", "subvol_name": "samba-export", "replica": false, "compression_algo": "no", "rusage": 16, "eusage": 16, "pqgroup_rusage": 16, "pqgroup_eusage": 16}},
{"model": "storageadmin.share", "pk": 4, "fields": {"pool": 1, "qgroup": "0/263", "pqgroup": "2015/45", "name": "samba-export2", "uuid": null, "size": 1048576, "owner": "root", "group": "root", "perms": "755", "toc": "2024-07-04T13:41:07.102Z", "subvol_name": "samba-export2", "replica": false, "compression_algo": "no", "rusage": 16, "eusage": 16, "pqgroup_rusage": 16, "pqgroup_eusage": 16}}

The resulting file was then uploaded and re-played after both SMB share exports were first deleted:

[05/Jul/2024 14:12:10] INFO [storageadmin.views.config_backup:109] Started restoring Samba exports.
[05/Jul/2024 14:12:10] DEBUG [storageadmin.views.config_backup:115] conf_file_exports=[{'share': 13, 'path': '/mnt2/samba-export', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None}, {'share': 4, 'path': '/mnt2/samba-export2', 'comment': 'Samba-Export', 'browsable': 'no', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None}]
[05/Jul/2024 14:12:10] DEBUG [storageadmin.views.config_backup:118] native_exports=[{'share': 13, 'path': '/mnt2/samba-export', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [3]}, {'share': 4, 'path': '/mnt2/samba-export2', 'comment': 'Samba-Export', 'browsable': 'no', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [4]}]
[05/Jul/2024 14:12:10] DEBUG [storageadmin.views.samba:133] Set the following Samba admin_users: []
[05/Jul/2024 14:12:10] DEBUG [storageadmin.views.samba:133] Set the following Samba admin_users: []
[05/Jul/2024 14:12:11] DEBUG [system.osi:262] Running command: /usr/bin/systemctl restart avahi-daemon
[05/Jul/2024 14:12:11] DEBUG [system.osi:262] Running command: /usr/bin/systemctl restart smb
[05/Jul/2024 14:12:11] DEBUG [system.osi:262] Running command: /usr/bin/systemctl restart nmb
[05/Jul/2024 14:12:11] INFO [storageadmin.views.config_backup:55] Non Dictionary payload
[05/Jul/2024 14:12:11] INFO [storageadmin.views.config_backup:56] Successfully created resource: https://localhost/api/samba. Payload: [{'share': 13, 'path': '/mnt2/samba-export', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [3]}, {'share': 4, 'path': '/mnt2/samba-export2', 'comment': 'Samba-Export', 'browsable': 'no', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [4]}]

N.B. from the above debug log, the 3 to 13 Share ID (by-hand edit) is evident as we maintain the original field of "share" as the SMB export creation itself only looks to the transitioned Share ID (by name match to target system) within the new field (type list) dictionary entry of shares: [3]. I.e. the artificially modified Share ID (3 to 13) within the config back-up file (to mimic a prior DB instance) was successfully transitioned to the 'native' share ID of 3 via Share.name match to the existing installs DB.

N.B. we currently enforce install wide unique Share.name; irrespective of Pool/Pools.

@phillxnet phillxnet marked this pull request as ready for review July 5, 2024 13:44
@phillxnet
Copy link
Member Author

Here we focus on the restore of the SMB exports, the Rock-on element of the restore mentioned in the associated issue has not been addressed. Changing the PR name accordingly.

@phillxnet phillxnet changed the title Config restore: samba shares and rockons not consistently restored #2847 Config restore: samba share export not consistently restored #2847 Jul 5, 2024
@phillxnet
Copy link
Member Author

phillxnet commented Jul 5, 2024

Testing continued

An rpm was build from the product of this PR. The prior install was used to create multiple shares and associated SMB exports. That install did not have the ROOT pool imported, so had no `home' share (the default for newer installs). A config-back file was created and downloaded from this prior install.

On the resulting new/fresh 5.0.11-2864 rpm derived install the ROOT pool was imported before the data pool (which had the prior SMB exported shares): ensuring share ID's would be inconsistent between the prior installs DB dump (config-backup file contents) and the new -2864 rpm derived install DB. Although specific share import order could also affect these.

Debug mode was enabled on the new install:

# cd /opt/rockstor/
# poetry run debug-mode ON
DEBUG flag is now set to True

and the prior config-backup file was uploaded: but not yet applied.
The system was then rebooted for good measure.

The test restore of the SMB exports was successful,however the debug log indicated no actual share ID transitions were requiried:

[05/Jul/2024 16:46:56] INFO [storageadmin.views.config_backup:109] Started restoring Samba exports.
[05/Jul/2024 16:46:56] DEBUG [storageadmin.views.config_backup:115] conf_file_exports=[{'share': 4, 'path': '/mnt2/samba-export2', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None}, {'share': 5, 'path': '/mnt2/samba-export3', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None}, {'share': 7, 'path': '/mnt2/samba-export4', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None}, {'share': 8, 'path': '/mnt2/samba-export5', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None}, {'share': 6, 'path': '/mnt2/samba-export1', 'comment': 'Samba-Export', 'browsable': 'no', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None}]
[05/Jul/2024 16:46:56] DEBUG [storageadmin.views.config_backup:118] native_exports=[{'share': 4, 'path': '/mnt2/samba-export2', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [4]}, {'share': 5, 'path': '/mnt2/samba-export3', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [5]}, {'share': 7, 'path': '/mnt2/samba-export4', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [7]}, {'share': 8, 'path': '/mnt2/samba-export5', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [8]}, {'share': 6, 'path': '/mnt2/samba-export1', 'comment': 'Samba-Export', 'browsable': 'no', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [6]}]
[05/Jul/2024 16:46:56] DEBUG [storageadmin.views.samba:133] Set the following Samba admin_users: []
[05/Jul/2024 16:46:56] DEBUG [storageadmin.views.samba:133] Set the following Samba admin_users: []
[05/Jul/2024 16:46:56] DEBUG [storageadmin.views.samba:133] Set the following Samba admin_users: []
[05/Jul/2024 16:46:56] DEBUG [storageadmin.views.samba:133] Set the following Samba admin_users: []
[05/Jul/2024 16:46:56] DEBUG [storageadmin.views.samba:133] Set the following Samba admin_users: []
[05/Jul/2024 16:46:57] DEBUG [system.osi:262] Running command: /usr/bin/systemctl restart avahi-daemon
[05/Jul/2024 16:46:57] DEBUG [system.osi:262] Running command: /usr/bin/systemctl restart smb
[05/Jul/2024 16:46:57] DEBUG [system.osi:262] Running command: /usr/bin/systemctl restart nmb
[05/Jul/2024 16:46:57] INFO [storageadmin.views.config_backup:55] Non Dictionary payload
[05/Jul/2024 16:46:57] INFO [storageadmin.views.config_backup:56] Successfully created resource: https://localhost/api/samba. Payload: [{'share': 4, 'path': '/mnt2/samba-export2', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [4]}, {'share': 5, 'path': '/mnt2/samba-export3', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [5]}, {'share': 7, 'path': '/mnt2/samba-export4', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [7]}, {'share': 8, 'path': '/mnt2/samba-export5', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [8]}, {'share': 6, 'path': '/mnt2/samba-export1', 'comment': 'Samba-Export', 'browsable': 'no', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [6]}]

I.e. all share ID's matched the counterpart 'shares' added by the new transition mechanism to the native_exports.

The above test was repeated after a hard-reset to wipe the prior DB and this time the ROOT pool was not imported first. We then have the following restore debug log relevant to this issue:

[05/Jul/2024 17:07:56] INFO [storageadmin.views.config_backup:109] Started restoring Samba exports.
[05/Jul/2024 17:07:56] DEBUG [storageadmin.views.config_backup:115] conf_file_exports=[{'share': 4, 'path': '/mnt2/samba-export2', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None}, {'share': 5, 'path': '/mnt2/samba-export3', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None}, {'share': 7, 'path': '/mnt2/samba-export4', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None}, {'share': 8, 'path': '/mnt2/samba-export5', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None}, {'share': 6, 'path': '/mnt2/samba-export1', 'comment': 'Samba-Export', 'browsable': 'no', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None}]
[05/Jul/2024 17:07:56] DEBUG [storageadmin.views.config_backup:118] native_exports=[{'share': 4, 'path': '/mnt2/samba-export2', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [3]}, {'share': 5, 'path': '/mnt2/samba-export3', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [4]}, {'share': 7, 'path': '/mnt2/samba-export4', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [6]}, {'share': 8, 'path': '/mnt2/samba-export5', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [7]}, {'share': 6, 'path': '/mnt2/samba-export1', 'comment': 'Samba-Export', 'browsable': 'no', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [5]}]
[05/Jul/2024 17:07:56] DEBUG [storageadmin.views.samba:133] Set the following Samba admin_users: []
[05/Jul/2024 17:07:56] DEBUG [storageadmin.views.samba:133] Set the following Samba admin_users: []
[05/Jul/2024 17:07:56] DEBUG [storageadmin.views.samba:133] Set the following Samba admin_users: []
[05/Jul/2024 17:07:57] DEBUG [storageadmin.views.samba:133] Set the following Samba admin_users: []
[05/Jul/2024 17:07:57] DEBUG [storageadmin.views.samba:133] Set the following Samba admin_users: []
[05/Jul/2024 17:07:57] DEBUG [system.osi:262] Running command: /usr/bin/systemctl restart avahi-daemon
[05/Jul/2024 17:07:57] DEBUG [system.osi:262] Running command: /usr/bin/systemctl restart smb
[05/Jul/2024 17:07:57] DEBUG [system.osi:262] Running command: /usr/bin/systemctl restart nmb
[05/Jul/2024 17:07:57] INFO [storageadmin.views.config_backup:55] Non Dictionary payload
[05/Jul/2024 17:07:57] INFO [storageadmin.views.config_backup:56] Successfully created resource: https://localhost/api/samba. Payload: [{'share': 4, 'path': '/mnt2/samba-export2', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [3]}, {'share': 5, 'path': '/mnt2/samba-export3', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [4]}, {'share': 7, 'path': '/mnt2/samba-export4', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [6]}, {'share': 8, 'path': '/mnt2/samba-export5', 'comment': 'Samba-Export', 'browsable': 'yes', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [7]}, {'share': 6, 'path': '/mnt2/samba-export1', 'comment': 'Samba-Export', 'browsable': 'no', 'read_only': 'no', 'guest_ok': 'no', 'shadow_copy': False, 'time_machine': False, 'snapshot_prefix': None, 'shares': [5]}]

In this second restore test we have succeeded in forcing a DB share ID disparity re prior dump and native import.

  • share: 4 transitioned to native shares: [3]
  • share: 5 transitioned to native shares: [4]
  • share: 7 transitioned to native shares: [6]
  • share: 8 transitioned to native shares: [7]
  • share: 6 transitioned to native shares: [5]

@phillxnet phillxnet changed the title Config restore: samba share export not consistently restored #2847 Config restore: samba share exports not consistently restored #2847 Jul 5, 2024
@FroggyFlox
Copy link
Member

@phillxnet,
I built from source on the same VM that I used for the description of the reproducer in #2847 and it fails to restore the same modified config backup that is described there. I see the following in the logs:

[06/Jul/2024 08:30:36] INFO [storageadmin.views.config_backup:109] Started restoring Samba exports.

==> var/log/huey.log <==
[2024-07-06 08:30:36,437] ERROR:huey:Worker-2:Unhandled exception in task ffa1ec65-c34d-42b6-8841-ad58776a1f42.
Traceback (most recent call last):
  File "/opt/rockstor/.venv/lib/python3.11/site-packages/huey/api.py", line 391, in _execute
    task_value = task.execute()
                 ^^^^^^^^^^^^^^
  File "/opt/rockstor/.venv/lib/python3.11/site-packages/huey/api.py", line 824, in execute
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/opt/rockstor/.venv/lib/python3.11/site-packages/huey/contrib/djhuey/__init__.py", line 136, in inner
    return fn(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^
  File "/opt/rockstor/.venv/lib/python3.11/site-packages/huey/api.py", line 922, in inner
    return fn(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^
  File "/opt/rockstor/src/rockstor/storageadmin/views/config_backup.py", line 610, in restore_config
    restore_samba_exports(sa_ml)
  File "/opt/rockstor/src/rockstor/storageadmin/views/config_backup.py", line 117, in restore_samba_exports
    native_exports.append(transform_samba_export_share_id(export, ml))
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/rockstor/src/rockstor/storageadmin/views/config_backup.py", line 261, in transform_samba_export_share_id
    native_share_id = get_target_share_id(get_sname(ml, conf_share_id))
                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/rockstor/src/rockstor/storageadmin/views/config_backup.py", line 584, in get_sname
    return sname
           ^^^^^
UnboundLocalError: cannot access local variable 'sname' where it is not associated with a value

==> var/log/rockstor.log <==
[06/Jul/2024 08:30:36] ERROR [storageadmin.tasks:101] Task [restore_config], id: ffa1ec65-c34d-42b6-8841-ad58776a1f42 failed, Args: (2,), Kwargs: {}, Exceptioncannot access local variable 'sname' where it is not associated with a value

I'm still looking into whether this is the result of improper testing on my end, though...

@FroggyFlox
Copy link
Member

I'm still looking into whether this is the result of improper testing on my end, though...

That's what it was: a failure to properly test on my end. I fixed my errors and now I can confirm it all restores as it should on my end too.

Sorry for the noise and thank you for the fix, @phillxnet!

@phillxnet
Copy link
Member Author

@FroggyFlox Thanks for the review. And yes, the testing for this can be tricky: my first (documented) attempt ended up with no transform required. That was a surprise. We are a little corner case on this one, but it looks like that corner is now a little less dark :).

I'll merge this ready to begin on the next issue/fix.

@phillxnet phillxnet merged commit 701b44c into rockstor:testing Jul 8, 2024
@phillxnet phillxnet deleted the 2847-Config-restore-samba-shares-and-rockons-not-consistently-restored branch July 8, 2024 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants