├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── doc ├── examples │ └── sas_discover_counters_tape.svg ├── man │ ├── Makefile │ └── man1 │ │ ├── Makefile │ │ ├── sas_counters.1 │ │ ├── sas_devices.1 │ │ ├── sas_discover.1 │ │ └── ses_report.1 └── txt │ ├── sas_counters.txt │ ├── sas_devices.txt │ ├── sas_discover.txt │ └── ses_report.txt ├── mkdeb.sh ├── mkrpm_el.sh ├── mkrpm_fedora.sh ├── sasutils-el7.spec ├── sasutils.spec ├── sasutils ├── __init__.py ├── cli │ ├── __init__.py │ ├── sas_counters.py │ ├── sas_devices.py │ ├── sas_discover.py │ ├── sas_mpath_snic_alias.py │ ├── sas_sd_snic_alias.py │ ├── sas_st_snic_alias.py │ └── ses_report.py ├── sas.py ├── scsi.py ├── ses.py ├── smp.py ├── sysfs.py └── vpd.py ├── setup.py └── tests ├── gen_sysfs_testenv.py └── sysfs.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | *.tar.gz 27 | deb_dist/ 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # IPython Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | 92 | # IDE 93 | .idea/ -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include doc/man/man1/* 3 | include doc/txt/* 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | sasutils is a set of command-line tools and a Python library to ease the administration of Serial Attached SCSI (SAS) storage networks. 2 | 3 | .. image:: https://img.shields.io/pypi/v/sasutils.svg 4 | :target: https://pypi.python.org/pypi/sasutils/ 5 | 6 | .. image:: https://img.shields.io/pypi/pyversions/sasutils.svg 7 | :target: https://pypi.python.org/pypi/sasutils/ 8 | :alt: Supported Python Versions 9 | 10 | .. image:: https://img.shields.io/pypi/l/sasutils.svg 11 | :target: https://pypi.python.org/pypi/sasutils/ 12 | :alt: License 13 | 14 | sasutils command-line tools 15 | =========================== 16 | 17 | * sas_counters 18 | * sas_devices 19 | * sas_discover 20 | * ses_report 21 | 22 | Also, a few "zeroconf" udev scripts for use in udev rules that create friendly device aliases using SES-2 subenclosure nicknames. 23 | 24 | * sas_mpath_snic_alias 25 | * sas_sd_snic_alias 26 | * sas_st_snic_alias 27 | 28 | .. note:: 29 | 30 | While **sasutils** gets most of the system data from sysfs (/sys), `sg_ses` (available in sg3_utils or sg3-utils) 31 | and `smp_discover` (available in smp_utils or smp-utils) are required for some SES features to work. 32 | 33 | .. warning:: 34 | 35 | **sasutils** is known to be broken with the new SAS 24Gb/s Broadcom driver mpi3mr. This driver is currently not 36 | creating sas objects (sas_host, etc.) in sysfs, which are used by sasutils. Suggestions are welcome. 37 | 38 | 39 | sas_counters 40 | ------------ 41 | 42 | **sas_counters** reports SAS/SES/SD I/O and phy error counters and provides SAS topology information in an output suitable for Carbon/Graphite. 43 | This command also supports SES-2 nicknames as seen in the example below (``io1-sassw1`` is the nickname of a SAS switch and ``io1-jbod1-0`` is the nickname of a JBOD SIM). 44 | 45 | .. code-block:: 46 | 47 | $ sas_counters 48 | ... 49 | oak-io1-s1.SAS9300-8e.0x500605b00ab01234.Switch184.io1-sassw1.JB4602_SIM_0.io1-jbod1-0.bays.41.ST8000NM0075.0x5000c50084c79876.ioerr_cnt 2 1487457378 50 | oak-io1-s1.SAS9300-8e.0x500605b00ab01234.Switch184.io1-sassw1.JB4602_SIM_0.io1-jbod1-0.bays.41.ST8000NM0075.0x5000c50084c79876.iodone_cnt 7154904 1487457378 51 | oak-io1-s1.SAS9300-8e.0x500605b00ab01234.Switch184.io1-sassw1.JB4602_SIM_0.io1-jbod1-0.bays.41.ST8000NM0075.0x5000c50084c79876.iorequest_cnt 7154906 1487457378 52 | ... 53 | oak-io1-s1.SAS9300-8e.0x500605b00ab05678.Switch184.io1-sassw2.phys.15.invalid_dword_count 5 1487457378 54 | oak-io1-s1.SAS9300-8e.0x500605b00ab05678.Switch184.io1-sassw2.phys.15.loss_of_dword_sync_count 1 1487457378 55 | oak-io1-s1.SAS9300-8e.0x500605b00ab05678.Switch184.io1-sassw2.phys.15.phy_reset_problem_count 0 1487457378 56 | oak-io1-s1.SAS9300-8e.0x500605b00ab05678.Switch184.io1-sassw2.phys.15.running_disparity_error_count 1 1487457378 57 | ... 58 | 59 | 60 | sas_discover 61 | ------------ 62 | 63 | Display SAS topology. By default, **sas_discover** tries to fold common devices (like disks). Use ``-v``, ``-vv`` or ``-vvv`` and ``--addr`` to display more details. 64 | Below is an example with a large topology with multiple SAS HBAs, SAS switches and SAS JBODs. 65 | 66 | .. code-block:: 67 | 68 | $ sas_discover -v 69 | oak-io8-s2 70 | |--host1 HBA 9500-16e 71 | | `--8x--expander-1:0 ASTEK 72 | | |--1x--end_device-1:0:0 73 | | | `--enclosure io8-sassw2 ASTEK 74 | | |--4x--expander-1:1 HGST 75 | | | |--1x--end_device-1:1:0 76 | | | | `--enclosure io8-jbod1 HGST 77 | | | |--10x--expander-1:9 HGST 78 | | | | `-- 50 x end_device -- disk 79 | | | `--10x--expander-1:10 HGST 80 | | | `-- 51 x end_device -- disk 81 | | |--4x--expander-1:2 HGST 82 | | | |--1x--end_device-1:2:0 83 | | | | `--enclosure io8-jbod2 HGST 84 | | | |--10x--expander-1:11 HGST 85 | | | | `-- 51 x end_device -- disk 86 | | | `--10x--expander-1:12 HGST 87 | | | `-- 51 x end_device -- disk 88 | | |--4x--expander-1:3 HGST 89 | | | |--1x--end_device-1:3:0 90 | | | | `--enclosure io8-jbod3 HGST 91 | | | |--10x--expander-1:13 HGST 92 | | | | `-- 51 x end_device -- disk 93 | | | `--10x--expander-1:14 HGST 94 | | | `-- 51 x end_device -- disk 95 | | |--4x--expander-1:4 HGST 96 | | | |--1x--end_device-1:4:0 97 | | | | `--enclosure io8-jbod4 HGST 98 | | | |--10x--expander-1:15 HGST 99 | | | | `-- 51 x end_device -- disk 100 | | | `--10x--expander-1:16 HGST 101 | | | `-- 51 x end_device -- disk 102 | | |--4x--expander-1:5 HGST 103 | | | |--1x--end_device-1:5:0 104 | | | | `--enclosure io8-jbod5 HGST 105 | | | |--10x--expander-1:17 HGST 106 | | | | `-- 51 x end_device -- disk 107 | | | `--10x--expander-1:18 HGST 108 | | | `-- 51 x end_device -- disk 109 | | |--4x--expander-1:6 HGST 110 | | | |--1x--end_device-1:6:0 111 | | | | `--enclosure io8-jbod6 HGST 112 | | | |--10x--expander-1:19 HGST 113 | | | | `-- 51 x end_device -- disk 114 | | | `--10x--expander-1:20 HGST 115 | | | `-- 51 x end_device -- disk 116 | | |--4x--expander-1:7 HGST 117 | | | |--1x--end_device-1:7:0 118 | | | | `--enclosure io8-jbod7 HGST 119 | | | |--10x--expander-1:21 HGST 120 | | | | `-- 51 x end_device -- disk 121 | | | `--10x--expander-1:22 HGST 122 | | | `-- 51 x end_device -- disk 123 | | `--4x--expander-1:8 HGST 124 | | |--1x--end_device-1:8:0 125 | | | `--enclosure io8-jbod8 HGST 126 | | |--10x--expander-1:23 HGST 127 | | | `-- 51 x end_device -- disk 128 | | `--10x--expander-1:24 HGST 129 | | `-- 51 x end_device -- disk 130 | `--host10 HBA 9500-16e 131 | `--8x--expander-10:0 ASTEK 132 | |--1x--end_device-10:0:0 133 | | `--enclosure io8-sassw1 ASTEK 134 | |--4x--expander-10:1 HGST 135 | | |--1x--end_device-10:1:0 136 | | | `--enclosure io8-jbod1 HGST 137 | | |--10x--expander-10:9 HGST 138 | | | `-- 50 x end_device -- disk 139 | | `--10x--expander-10:10 HGST 140 | | `-- 51 x end_device -- disk 141 | |--4x--expander-10:2 HGST 142 | | |--1x--end_device-10:2:0 143 | | | `--enclosure io8-jbod2 HGST 144 | | |--10x--expander-10:11 HGST 145 | | | `-- 51 x end_device -- disk 146 | | `--10x--expander-10:12 HGST 147 | | `-- 51 x end_device -- disk 148 | |--4x--expander-10:3 HGST 149 | | |--1x--end_device-10:3:0 150 | | | `--enclosure io8-jbod3 HGST 151 | | |--10x--expander-10:13 HGST 152 | | | `-- 51 x end_device -- disk 153 | | `--10x--expander-10:14 HGST 154 | | `-- 51 x end_device -- disk 155 | |--4x--expander-10:4 HGST 156 | | |--1x--end_device-10:4:0 157 | | | `--enclosure io8-jbod4 HGST 158 | | |--10x--expander-10:15 HGST 159 | | | `-- 51 x end_device -- disk 160 | | `--10x--expander-10:16 HGST 161 | | `-- 51 x end_device -- disk 162 | |--4x--expander-10:5 HGST 163 | | |--1x--end_device-10:5:0 164 | | | `--enclosure io8-jbod5 HGST 165 | | |--10x--expander-10:17 HGST 166 | | | `-- 51 x end_device -- disk 167 | | `--10x--expander-10:18 HGST 168 | | `-- 51 x end_device -- disk 169 | |--4x--expander-10:6 HGST 170 | | |--1x--end_device-10:6:0 171 | | | `--enclosure io8-jbod6 HGST 172 | | |--10x--expander-10:19 HGST 173 | | | `-- 51 x end_device -- disk 174 | | `--10x--expander-10:20 HGST 175 | | `-- 51 x end_device -- disk 176 | |--4x--expander-10:7 HGST 177 | | |--1x--end_device-10:7:0 178 | | | `--enclosure io8-jbod7 HGST 179 | | |--10x--expander-10:21 HGST 180 | | | `-- 51 x end_device -- disk 181 | | `--10x--expander-10:22 HGST 182 | | `-- 51 x end_device -- disk 183 | `--4x--expander-10:8 HGST 184 | |--1x--end_device-10:8:0 185 | | `--enclosure io8-jbod8 HGST 186 | |--10x--expander-10:23 HGST 187 | | `-- 51 x end_device -- disk 188 | `--10x--expander-10:24 HGST 189 | `-- 51 x end_device -- disk 190 | 191 | 192 | Use ``sas_discover --counters`` to display the number of SCSI commands issued (`req`), completed or rejected (`done`) and the ones that completed with an error (`error`). 193 | 194 | .. image:: https://raw.githubusercontent.com/stanford-rc/sasutils/master/doc/examples/sas_discover_counters_tape.svg 195 | 196 | 197 | sas_devices 198 | ----------- 199 | 200 | Zeroconf tool that scans SAS devices and resolves associated enclosures. Useful to quickly check cabling and hardware setup. 201 | 202 | When used with -v, **sas_devices** will also display all disk devices with serial numbers. 203 | 204 | The following example shows a proper detection of a 60-disk JBOD with 2 SIMs/IOMs (an "enclosure group"). 205 | 206 | .. code-block:: 207 | 208 | $ sas_devices 209 | Found 2 SAS hosts 210 | Found 4 SAS expanders 211 | Found 1 enclosure groups 212 | Enclosure group: [io1-jbod1-0][io1-jbod1-1] 213 | NUM VENDOR MODEL REV SIZE PATHS 214 | 60 x SEAGATE ST8000NM0075 E004 8.0TB 2 215 | Total: 60 block devices in enclosure group 216 | 217 | 218 | The following example shows a proper detection of four Seagate Exos E JBOFs with 15.4TB SSDs. Note that 2 IOMs are detected for each JBOF and they have the same SES-2 nickname (this is normal with this hardware). 219 | 220 | .. code-block:: 221 | 222 | $ sas_devices 223 | Found 2 SAS hosts 224 | Found 8 SAS expanders 225 | Found 4 enclosure groups 226 | Enclosure group: [io1-jbof4][io1-jbof4] 227 | NUM VENDOR MODEL REV SIZE PATHS 228 | 24 x SEAGATE XS15360SE70084 0003 15.4TB 2 229 | Total: 24 block devices in enclosure group 230 | Enclosure group: [io1-jbof2][io1-jbof2] 231 | NUM VENDOR MODEL REV SIZE PATHS 232 | 24 x SEAGATE XS15360SE70084 0003 15.4TB 2 233 | Total: 24 block devices in enclosure group 234 | Enclosure group: [io1-jbof3][io1-jbof3] 235 | NUM VENDOR MODEL REV SIZE PATHS 236 | 24 x SEAGATE XS15360SE70084 0003 15.4TB 2 237 | Total: 24 block devices in enclosure group 238 | Enclosure group: [io1-jbof1][io1-jbof1] 239 | NUM VENDOR MODEL REV SIZE PATHS 240 | 24 x SEAGATE XS15360SE70084 0003 15.4TB 2 241 | Total: 24 block devices in enclosure group 242 | 243 | 244 | ses_report 245 | ---------- 246 | 247 | SES status and environmental metrics. 248 | 249 | Used with -c, this command will find all enclosures and then use SES-2 nicknames and use sg_ses to output results suitable for Carbon/Graphite. 250 | 251 | .. code-block:: 252 | 253 | $ ses_report -c --prefix=datacenter.stanford 254 | datacenter.stanford.io1-sassw1.Cooling.Left_Fan.speed_rpm 19560 1476486766 255 | datacenter.stanford.io1-sassw1.Cooling.Right_Fan.speed_rpm 19080 1476486766 256 | datacenter.stanford.io1-sassw1.Cooling.Center_Fan.speed_rpm 19490 1476486766 257 | ... 258 | 259 | Use -s to get the status of all detected SES Element Descriptors. 260 | 261 | .. code-block:: 262 | 263 | # ses_report -s --prefix=datacenter.stanford | grep SIM 264 | datacenter.stanford.io1-jbod1-0.Enclosure_services_controller_electronics.SIM_00 OK 265 | datacenter.stanford.io1-jbod1-0.Enclosure_services_controller_electronics.SIM_01 OK 266 | datacenter.stanford.io1-jbod1-0.SAS_expander.SAS_Expander_SIM_0 OK 267 | datacenter.stanford.io1-jbod1-0.SAS_expander.SAS_Expander_ISIM_2 OK 268 | datacenter.stanford.io1-jbod1-0.SAS_expander.SAS_Expander_ISIM_0 OK 269 | datacenter.stanford.io1-jbod1-1.Enclosure_services_controller_electronics.SIM_00 OK 270 | datacenter.stanford.io1-jbod1-1.Enclosure_services_controller_electronics.SIM_01 OK 271 | datacenter.stanford.io1-jbod1-1.SAS_expander.SAS_Expander_SIM_1 OK 272 | datacenter.stanford.io1-jbod1-1.SAS_expander.SAS_Expander_ISIM_3 OK 273 | datacenter.stanford.io1-jbod1-1.SAS_expander.SAS_Expander_ISIM_1 OK 274 | 275 | .. warning:: 276 | 277 | **ses_report** requires a recent version of *sg3_utils* and won't work with the version shipped with CentOS 6 for example. 278 | 279 | 280 | sas_sd_snic_alias and sas_st_snic_alias 281 | --------------------------------------- 282 | 283 | Generate udev aliases using the SES-2 subenclosure nickname and bay identifier of each device. 284 | These scripts can also be used as examples and adapted to your specific needs. 285 | 286 | For example, for block devices, add the following to your udev rules: 287 | 288 | .. code-block:: 289 | 290 | KERNEL=="sd*", PROGRAM="/usr/bin/sas_sd_snic_alias %k", SYMLINK+="%c" 291 | 292 | Or, for SAS tape drives behind SAS switches (that act as enclosures): 293 | 294 | .. code-block:: 295 | 296 | KERNEL=="st*", PROGRAM="/usr/bin/sas_st_snic_alias %k", SYMLINK+="%c" 297 | 298 | This should generate udev aliases made of the device subenclosure nickname followed by the bay identifier. In the following case, *io1-jbod1-0* is the subenclosure nickname (here SIM 0 of JBOD #1). 299 | 300 | .. code-block:: 301 | 302 | $ ls -l /dev/io1-jbod1-0-bay26 303 | lrwxrwxrwx 1 root root 4 Oct 14 21:00 /dev/io1-jbod1-0-bay26 -> sdab 304 | 305 | .. note:: 306 | 307 | Use `sg_ses --nickname=...` to define SES-2 subenclosure nicknames. 308 | 309 | sas_mpath_snic_alias 310 | -------------------- 311 | 312 | This utility is very similar to **sas_sd_snic_alias** but only accepts device-mapper devices. Add the following line to your udev rules: 313 | 314 | .. code-block:: 315 | 316 | KERNEL=="dm-[0-9]*", PROGRAM="/usr/bin/sas_mpath_snic_alias %k", SYMLINK+="mapper/%c" 317 | 318 | This will result in useful symlinks. 319 | 320 | .. code-block:: 321 | 322 | $ ls -l /dev/mapper/io1-jbod1-bay26 323 | lrwxrwxrwx 1 root root 8 Oct 14 21:00 /dev/mapper/io1-jbod1-bay26 -> ../dm-31 324 | 325 | .. note:: 326 | 327 | For **sas_mpath_snic_alias** to work with a JBOD having two SIMs, both enclosure nicknames should have a common prefix (eg. "myjbodX-") that will be automatically used. 328 | 329 | 330 | sasutils Python library 331 | ======================= 332 | 333 | Documentation will be available on the `wiki`_. 334 | 335 | * the following example will list all SAS hosts (controllers) found in sysfs 336 | 337 | .. code-block:: python 338 | 339 | from sasutils.sas import SASHost 340 | from sasutils.sysfs import sysfs 341 | 342 | # sysfs is a helper to walk through sysfs (/sys) 343 | for node in sysfs.node('class').node('sas_host'): 344 | 345 | # Instantiate SASHost with the sas_host sysfs device class 346 | host = SASHost(node.node('device')) 347 | 348 | # To get its sysfs name, use: 349 | print(host.name) 350 | # To get attributes from scsi_host, use: 351 | print(' %s' % host.scsi_host.attrs.host_sas_address) 352 | print(' %s' % host.scsi_host.attrs.version_fw) 353 | 354 | * See also https://github.com/stanford-rc/sasutils/wiki/Code-snippets 355 | 356 | :Author: Stephane Thiell - Stanford Research Computing Center 357 | 358 | .. _wiki: https://github.com/stanford-rc/sasutils/wiki 359 | -------------------------------------------------------------------------------- /doc/man/Makefile: -------------------------------------------------------------------------------- 1 | SUBDIRS = man1 2 | 3 | .PHONY: all $(SUBDIRS) 4 | 5 | all: $(SUBDIRS) 6 | 7 | $(SUBDIRS): 8 | $(MAKE) -C $@ force 9 | -------------------------------------------------------------------------------- /doc/man/man1/Makefile: -------------------------------------------------------------------------------- 1 | SRCDIR = ../../txt 2 | SOURCES := $(SRCDIR)/sas_counters.txt \ 3 | $(SRCDIR)/sas_devices.txt \ 4 | $(SRCDIR)/sas_discover.txt \ 5 | $(SRCDIR)/ses_report.txt 6 | OBJECTS := $(SOURCES:$(SRCDIR)/%.txt=%.1) 7 | 8 | %.1: ../../txt/%.txt 9 | rst2man $< $@ 10 | 11 | all: $(OBJECTS) 12 | 13 | force: clean all 14 | 15 | clean: 16 | @rm -v $(OBJECTS) 17 | -------------------------------------------------------------------------------- /doc/man/man1/sas_counters.1: -------------------------------------------------------------------------------- 1 | .\" Man page generated from reStructuredText. 2 | . 3 | .TH SAS_COUNTERS 1 "2024-11-11" "0.6.1" "sasutils" 4 | .SH NAME 5 | sas_counters \- show Serial Attached SCSI (SAS) counters 6 | . 7 | .nr rst2man-indent-level 0 8 | . 9 | .de1 rstReportMargin 10 | \\$1 \\n[an-margin] 11 | level \\n[rst2man-indent-level] 12 | level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] 13 | - 14 | \\n[rst2man-indent0] 15 | \\n[rst2man-indent1] 16 | \\n[rst2man-indent2] 17 | .. 18 | .de1 INDENT 19 | .\" .rstReportMargin pre: 20 | . RS \\$1 21 | . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] 22 | . nr rst2man-indent-level +1 23 | .\" .rstReportMargin post: 24 | .. 25 | .de UNINDENT 26 | . RE 27 | .\" indent \\n[an-margin] 28 | .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] 29 | .nr rst2man-indent-level -1 30 | .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] 31 | .in \\n[rst2man-indent\\n[rst2man-indent-level]]u 32 | .. 33 | .SH SYNOPSIS 34 | .sp 35 | \fBsas_counters [\-h] [\-\-prefix PREFIX]\fP 36 | .SH DESCRIPTION 37 | .sp 38 | \fBsas_counters\fP reports Serial Attached SCSI (SAS), SCSI Enclosure Services 39 | (SES), SCSI devices I/O and phy error counters organized in an output suitable 40 | for Carbon/Graphite with SAS topology information. 41 | .SH OPTIONS 42 | .INDENT 0.0 43 | .TP 44 | .B optional arguments: 45 | .INDENT 7.0 46 | .TP 47 | .B \-h\fP,\fB \-\-help 48 | show this help message and exit 49 | .TP 50 | .BI \-\-prefix \ PREFIX 51 | carbon prefix (example: "datacenter.cluster", default is 52 | "sasutils.sas_counters") 53 | .UNINDENT 54 | .UNINDENT 55 | .SH EXIT STATUS 56 | .sp 57 | An exit status of zero indicates success of the command, and failure otherwise. 58 | .SH SEE ALSO 59 | .sp 60 | \fBsas_devices\fP(1), \fBsas_discover\fP(1), \fBses_report\fP(1) 61 | .SH BUG REPORTS 62 | .sp 63 | Use the following URL to submit a bug report or feedback: 64 | .INDENT 0.0 65 | .INDENT 3.5 66 | \fI\%https://github.com/stanford\-rc/sasutils/issues\fP 67 | .UNINDENT 68 | .UNINDENT 69 | .SH AUTHOR 70 | Stephane Thiell 71 | .SH COPYRIGHT 72 | Apache License Version 2.0 73 | .\" Generated by docutils manpage writer. 74 | . 75 | -------------------------------------------------------------------------------- /doc/man/man1/sas_devices.1: -------------------------------------------------------------------------------- 1 | .\" Man page generated from reStructuredText. 2 | . 3 | .TH SAS_DEVICES 1 "2024-11-11" "0.6.1" "sasutils" 4 | .SH NAME 5 | sas_devices \- show Serial Attached SCSI (SAS) enclosures and devices 6 | . 7 | .nr rst2man-indent-level 0 8 | . 9 | .de1 rstReportMargin 10 | \\$1 \\n[an-margin] 11 | level \\n[rst2man-indent-level] 12 | level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] 13 | - 14 | \\n[rst2man-indent0] 15 | \\n[rst2man-indent1] 16 | \\n[rst2man-indent2] 17 | .. 18 | .de1 INDENT 19 | .\" .rstReportMargin pre: 20 | . RS \\$1 21 | . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] 22 | . nr rst2man-indent-level +1 23 | .\" .rstReportMargin post: 24 | .. 25 | .de UNINDENT 26 | . RE 27 | .\" indent \\n[an-margin] 28 | .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] 29 | .nr rst2man-indent-level -1 30 | .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] 31 | .in \\n[rst2man-indent\\n[rst2man-indent-level]]u 32 | .. 33 | .SH SYNOPSIS 34 | .sp 35 | \fBsas_devices [\-h] [\-vv]\fP 36 | .SH DESCRIPTION 37 | .sp 38 | \fBsas_devices\fP shows SAS devices and automatically resolves multiple paths and 39 | common associated enclosures (no configuration required). Useful to check 40 | cabling and hardware setup. When used with \fI\-v\fP, \fBsas_devices\fP will also 41 | display all disk devices with serial numbers. Adding a second \fI\-v\fP will display 42 | additional information like some sysfs paths involved. 43 | .SH OPTIONS 44 | .INDENT 0.0 45 | .TP 46 | .B optional arguments: 47 | .INDENT 7.0 48 | .TP 49 | .B \-h\fP,\fB \-\-help 50 | show this help message and exit 51 | .TP 52 | .B \-v\fP,\fB \-\-verbose 53 | verbosity level, repeat multiple times! 54 | .UNINDENT 55 | .UNINDENT 56 | .SH EXIT STATUS 57 | .sp 58 | An exit status of zero indicates success of the command, and failure otherwise. 59 | .SH SEE ALSO 60 | .sp 61 | \fBsas_counters\fP(1), \fBsas_discover\fP(1), \fBses_report\fP(1) 62 | .SH BUG REPORTS 63 | .sp 64 | Use the following URL to submit a bug report or feedback: 65 | .INDENT 0.0 66 | .INDENT 3.5 67 | \fI\%https://github.com/stanford\-rc/sasutils/issues\fP 68 | .UNINDENT 69 | .UNINDENT 70 | .SH AUTHOR 71 | Stephane Thiell 72 | .SH COPYRIGHT 73 | Apache License Version 2.0 74 | .\" Generated by docutils manpage writer. 75 | . 76 | -------------------------------------------------------------------------------- /doc/man/man1/sas_discover.1: -------------------------------------------------------------------------------- 1 | .\" Man page generated from reStructuredText. 2 | . 3 | .TH SAS_DISCOVER 1 "2024-11-11" "0.6.1" "sasutils" 4 | .SH NAME 5 | sas_discover \- display Serial Attached SCSI (SAS) topology 6 | . 7 | .nr rst2man-indent-level 0 8 | . 9 | .de1 rstReportMargin 10 | \\$1 \\n[an-margin] 11 | level \\n[rst2man-indent-level] 12 | level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] 13 | - 14 | \\n[rst2man-indent0] 15 | \\n[rst2man-indent1] 16 | \\n[rst2man-indent2] 17 | .. 18 | .de1 INDENT 19 | .\" .rstReportMargin pre: 20 | . RS \\$1 21 | . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] 22 | . nr rst2man-indent-level +1 23 | .\" .rstReportMargin post: 24 | .. 25 | .de UNINDENT 26 | . RE 27 | .\" indent \\n[an-margin] 28 | .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] 29 | .nr rst2man-indent-level -1 30 | .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] 31 | .in \\n[rst2man-indent\\n[rst2man-indent-level]]u 32 | .. 33 | .SH SYNOPSIS 34 | .sp 35 | \fBsas_discover [\-h] [\-\-verbose] [\-\-addr] [\-\-devices] [\-\-counters]\fP 36 | .SH DESCRIPTION 37 | .sp 38 | \fBsas_discover\fP displays the SAS topology from the host initiator(s), through 39 | any SAS expanders up to the SAS targets. Each link usually represents a SAS 40 | port connection, whether it uses one or more PHYs. The number of PHYs per port 41 | is explicitly shown if greater than one (eg. "\-\-4x\-\-" for a 4\-PHY wide port). 42 | Please note that by default, \fBsas_discover\fP tries to fold common devices 43 | (like disks), like in the example below: 44 | .INDENT 0.0 45 | .TP 46 | .B \-\- 60 x end_device \-\- disk 47 | 60 x 1\-PHY links, each connected to a SAS end_device target of SCSI type \fIdisk\fP 48 | .UNINDENT 49 | .sp 50 | Host initiators, expanders and end devices are labelled with the same name used 51 | in sysfs. If \fI\-v\fP is provided, more descriptive information will be displayed, 52 | but the topology will stay folded whenever possible. \fBsas_discover \-v\fP will 53 | also print SES\-2 subenclosure nicknames, which are useful to identify JBODs/SIMs 54 | and external SAS switches connected to the topology. 55 | .sp 56 | Use \fI\-vv\fP to unfold the topology and display more information (eg. model, size, 57 | bay identifier in enclosure). 58 | .sp 59 | Use \fI\-vvv\fP to display additional low\-level information for each SAS end device. 60 | .sp 61 | Add \fI\-\-addr\fP to also display the SAS address for each SAS component found in the 62 | topology. 63 | .SH OPTIONS 64 | .INDENT 0.0 65 | .TP 66 | .B optional arguments: 67 | .INDENT 7.0 68 | .TP 69 | .B \-h\fP,\fB \-\-help 70 | show this help message and exit 71 | .TP 72 | .B \-\-verbose\fP,\fB \-v 73 | Verbosity level, repeat multiple times! 74 | .TP 75 | .B \-\-addr 76 | Print SAS addresses 77 | .TP 78 | .B \-\-devices 79 | Print associated devices 80 | .TP 81 | .B \-\-counters 82 | Print I/O counters 83 | .UNINDENT 84 | .UNINDENT 85 | .SH EXIT STATUS 86 | .sp 87 | An exit status of zero indicates success of the command, and failure otherwise. 88 | .SH SEE ALSO 89 | .sp 90 | \fBsas_counters\fP(1), \fBsas_devices\fP(1), \fBses_report\fP(1) 91 | .SH BUG REPORTS 92 | .sp 93 | Use the following URL to submit a bug report or feedback: 94 | .INDENT 0.0 95 | .INDENT 3.5 96 | \fI\%https://github.com/stanford\-rc/sasutils/issues\fP 97 | .UNINDENT 98 | .UNINDENT 99 | .SH AUTHOR 100 | Stephane Thiell 101 | .SH COPYRIGHT 102 | Apache License Version 2.0 103 | .\" Generated by docutils manpage writer. 104 | . 105 | -------------------------------------------------------------------------------- /doc/man/man1/ses_report.1: -------------------------------------------------------------------------------- 1 | .\" Man page generated from reStructuredText. 2 | . 3 | .TH SES_REPORT 1 "2024-11-11" "0.6.1" "sasutils" 4 | .SH NAME 5 | ses_report \- SCSI Enclosure Services (SES) status and metrics reporting utility 6 | . 7 | .nr rst2man-indent-level 0 8 | . 9 | .de1 rstReportMargin 10 | \\$1 \\n[an-margin] 11 | level \\n[rst2man-indent-level] 12 | level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] 13 | - 14 | \\n[rst2man-indent0] 15 | \\n[rst2man-indent1] 16 | \\n[rst2man-indent2] 17 | .. 18 | .de1 INDENT 19 | .\" .rstReportMargin pre: 20 | . RS \\$1 21 | . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] 22 | . nr rst2man-indent-level +1 23 | .\" .rstReportMargin post: 24 | .. 25 | .de UNINDENT 26 | . RE 27 | .\" indent \\n[an-margin] 28 | .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] 29 | .nr rst2man-indent-level -1 30 | .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] 31 | .in \\n[rst2man-indent\\n[rst2man-indent-level]]u 32 | .. 33 | .SH SYNOPSIS 34 | .sp 35 | \fBses_report [\-h] [\-d] (\-c | \-s) [\-\-prefix PREFIX] [\-j]\fP 36 | .SH DESCRIPTION 37 | .sp 38 | SES status and environmental metrics. 39 | .sp 40 | Used with \fI\-c\fP, \fBses_report\fP will search for enclosure environmental metrics 41 | and then output results in a format suitable for Carbon/Graphite. 42 | .sp 43 | Alternatively, you can Use \fI\-s\fP to get the status of all detected SES Element 44 | Descriptors. 45 | .sp 46 | \fBses_report\fP has support for SES\-2 enclosure nickname. 47 | .SH OPTIONS 48 | .INDENT 0.0 49 | .TP 50 | .B optional arguments: 51 | .INDENT 7.0 52 | .TP 53 | .B \-h\fP,\fB \-\-help 54 | show this help message and exit 55 | .TP 56 | .B \-d\fP,\fB \-\-debug 57 | enable debugging 58 | .TP 59 | .B \-c\fP,\fB \-\-carbon 60 | output SES Element descriptors metrics in a format suitable 61 | for Carbon/Graphite 62 | .TP 63 | .B \-s\fP,\fB \-\-status 64 | output status found in SES Element descriptors 65 | .UNINDENT 66 | .TP 67 | .B output options: 68 | .INDENT 7.0 69 | .TP 70 | .BI \-\-prefix \ PREFIX 71 | carbon prefix (example: "datacenter.cluster", default is 72 | "sasutils.ses_report") 73 | .TP 74 | .B \-j\fP,\fB \-\-json 75 | alternative JSON output mode 76 | .UNINDENT 77 | .UNINDENT 78 | .SH EXIT STATUS 79 | .sp 80 | An exit status of zero indicates success of the command, and failure otherwise. 81 | .SH SEE ALSO 82 | .sp 83 | \fBsas_counters\fP(1), \fBsas_devices\fP(1), \fBsas_discover\fP(1) 84 | .SH BUG REPORTS 85 | .sp 86 | Use the following URL to submit a bug report or feedback: 87 | .INDENT 0.0 88 | .INDENT 3.5 89 | \fI\%https://github.com/stanford\-rc/sasutils/issues\fP 90 | .UNINDENT 91 | .UNINDENT 92 | .SH AUTHOR 93 | Stephane Thiell 94 | .SH COPYRIGHT 95 | Apache License Version 2.0 96 | .\" Generated by docutils manpage writer. 97 | . 98 | -------------------------------------------------------------------------------- /doc/txt/sas_counters.txt: -------------------------------------------------------------------------------- 1 | ============ 2 | sas_counters 3 | ============ 4 | 5 | ---------------------------------------- 6 | show Serial Attached SCSI (SAS) counters 7 | ---------------------------------------- 8 | 9 | :Author: Stephane Thiell 10 | :Date: 2024-11-11 11 | :Copyright: Apache License Version 2.0 12 | :Version: 0.6.1 13 | :Manual section: 1 14 | :Manual group: sasutils 15 | 16 | 17 | SYNOPSIS 18 | ======== 19 | 20 | ``sas_counters [-h] [--prefix PREFIX]`` 21 | 22 | DESCRIPTION 23 | =========== 24 | 25 | ``sas_counters`` reports Serial Attached SCSI (SAS), SCSI Enclosure Services 26 | (SES), SCSI devices I/O and phy error counters organized in an output suitable 27 | for Carbon/Graphite with SAS topology information. 28 | 29 | OPTIONS 30 | ======= 31 | 32 | optional arguments: 33 | -h, --help show this help message and exit 34 | --prefix PREFIX carbon prefix (example: "datacenter.cluster", default is 35 | "sasutils.sas_counters") 36 | 37 | EXIT STATUS 38 | =========== 39 | 40 | An exit status of zero indicates success of the command, and failure otherwise. 41 | 42 | SEE ALSO 43 | ======== 44 | 45 | ``sas_devices``\(1), ``sas_discover``\(1), ``ses_report``\(1) 46 | 47 | BUG REPORTS 48 | =========== 49 | 50 | Use the following URL to submit a bug report or feedback: 51 | 52 | https://github.com/stanford-rc/sasutils/issues 53 | -------------------------------------------------------------------------------- /doc/txt/sas_devices.txt: -------------------------------------------------------------------------------- 1 | =========== 2 | sas_devices 3 | =========== 4 | 5 | ------------------------------------------------------ 6 | show Serial Attached SCSI (SAS) enclosures and devices 7 | ------------------------------------------------------ 8 | 9 | :Author: Stephane Thiell 10 | :Date: 2024-11-11 11 | :Copyright: Apache License Version 2.0 12 | :Version: 0.6.1 13 | :Manual section: 1 14 | :Manual group: sasutils 15 | 16 | 17 | SYNOPSIS 18 | ======== 19 | 20 | ``sas_devices [-h] [-vv]`` 21 | 22 | DESCRIPTION 23 | =========== 24 | 25 | 26 | ``sas_devices`` shows SAS devices and automatically resolves multiple paths and 27 | common associated enclosures (no configuration required). Useful to check 28 | cabling and hardware setup. When used with `-v`, ``sas_devices`` will also 29 | display all disk devices with serial numbers. Adding a second `-v` will display 30 | additional information like some sysfs paths involved. 31 | 32 | OPTIONS 33 | ======= 34 | 35 | optional arguments: 36 | -h, --help show this help message and exit 37 | -v, --verbose verbosity level, repeat multiple times! 38 | 39 | EXIT STATUS 40 | =========== 41 | 42 | An exit status of zero indicates success of the command, and failure otherwise. 43 | 44 | SEE ALSO 45 | ======== 46 | 47 | ``sas_counters``\(1), ``sas_discover``\(1), ``ses_report``\(1) 48 | 49 | BUG REPORTS 50 | =========== 51 | 52 | Use the following URL to submit a bug report or feedback: 53 | 54 | https://github.com/stanford-rc/sasutils/issues 55 | -------------------------------------------------------------------------------- /doc/txt/sas_discover.txt: -------------------------------------------------------------------------------- 1 | ============ 2 | sas_discover 3 | ============ 4 | 5 | ------------------------------------------- 6 | display Serial Attached SCSI (SAS) topology 7 | ------------------------------------------- 8 | 9 | :Author: Stephane Thiell 10 | :Date: 2024-11-11 11 | :Copyright: Apache License Version 2.0 12 | :Version: 0.6.1 13 | :Manual section: 1 14 | :Manual group: sasutils 15 | 16 | 17 | SYNOPSIS 18 | ======== 19 | 20 | ``sas_discover [-h] [--verbose] [--addr] [--devices] [--counters]`` 21 | 22 | DESCRIPTION 23 | =========== 24 | 25 | ``sas_discover`` displays the SAS topology from the host initiator(s), through 26 | any SAS expanders up to the SAS targets. Each link usually represents a SAS 27 | port connection, whether it uses one or more PHYs. The number of PHYs per port 28 | is explicitly shown if greater than one (eg. "--4x--" for a 4-PHY wide port). 29 | Please note that by default, ``sas_discover`` tries to fold common devices 30 | (like disks), like in the example below: 31 | 32 | :-- 60 x end_device -- disk: 33 | 60 x 1-PHY links, each connected to a SAS end_device target of SCSI type *disk* 34 | 35 | Host initiators, expanders and end devices are labelled with the same name used 36 | in sysfs. If `-v` is provided, more descriptive information will be displayed, 37 | but the topology will stay folded whenever possible. ``sas_discover -v`` will 38 | also print SES-2 subenclosure nicknames, which are useful to identify JBODs/SIMs 39 | and external SAS switches connected to the topology. 40 | 41 | Use `-vv` to unfold the topology and display more information (eg. model, size, 42 | bay identifier in enclosure). 43 | 44 | Use `-vvv` to display additional low-level information for each SAS end device. 45 | 46 | Add `--addr` to also display the SAS address for each SAS component found in the 47 | topology. 48 | 49 | OPTIONS 50 | ======= 51 | 52 | optional arguments: 53 | -h, --help show this help message and exit 54 | --verbose, -v Verbosity level, repeat multiple times! 55 | --addr Print SAS addresses 56 | --devices Print associated devices 57 | --counters Print I/O counters 58 | 59 | 60 | EXIT STATUS 61 | =========== 62 | 63 | An exit status of zero indicates success of the command, and failure otherwise. 64 | 65 | SEE ALSO 66 | ======== 67 | 68 | ``sas_counters``\(1), ``sas_devices``\(1), ``ses_report``\(1) 69 | 70 | BUG REPORTS 71 | =========== 72 | 73 | Use the following URL to submit a bug report or feedback: 74 | 75 | https://github.com/stanford-rc/sasutils/issues 76 | -------------------------------------------------------------------------------- /doc/txt/ses_report.txt: -------------------------------------------------------------------------------- 1 | ========== 2 | ses_report 3 | ========== 4 | 5 | ------------------------------------------------------------------ 6 | SCSI Enclosure Services (SES) status and metrics reporting utility 7 | ------------------------------------------------------------------ 8 | 9 | :Author: Stephane Thiell 10 | :Date: 2024-11-11 11 | :Copyright: Apache License Version 2.0 12 | :Version: 0.6.1 13 | :Manual section: 1 14 | :Manual group: sasutils 15 | 16 | 17 | SYNOPSIS 18 | ======== 19 | 20 | ``ses_report [-h] [-d] (-c | -s) [--prefix PREFIX] [-j]`` 21 | 22 | DESCRIPTION 23 | =========== 24 | 25 | SES status and environmental metrics. 26 | 27 | Used with `-c`, ``ses_report`` will search for enclosure environmental metrics 28 | and then output results in a format suitable for Carbon/Graphite. 29 | 30 | Alternatively, you can Use `-s` to get the status of all detected SES Element 31 | Descriptors. 32 | 33 | ``ses_report`` has support for SES-2 enclosure nickname. 34 | 35 | OPTIONS 36 | ======= 37 | 38 | optional arguments: 39 | -h, --help show this help message and exit 40 | -d, --debug enable debugging 41 | -c, --carbon output SES Element descriptors metrics in a format suitable 42 | for Carbon/Graphite 43 | -s, --status output status found in SES Element descriptors 44 | 45 | output options: 46 | --prefix PREFIX carbon prefix (example: "datacenter.cluster", default is 47 | "sasutils.ses_report") 48 | -j, --json alternative JSON output mode 49 | 50 | EXIT STATUS 51 | =========== 52 | 53 | An exit status of zero indicates success of the command, and failure otherwise. 54 | 55 | SEE ALSO 56 | ======== 57 | 58 | ``sas_counters``\(1), ``sas_devices``\(1), ``sas_discover``\(1) 59 | 60 | BUG REPORTS 61 | =========== 62 | 63 | Use the following URL to submit a bug report or feedback: 64 | 65 | https://github.com/stanford-rc/sasutils/issues 66 | -------------------------------------------------------------------------------- /mkdeb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # mkdeb.sh: create a .deb package 3 | # Requires: python3-setuptools python3-stdeb 4 | 5 | python3 setup.py --command-packages=stdeb.command bdist_deb 6 | -------------------------------------------------------------------------------- /mkrpm_el.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Build RPM for RHEL/CentOS 3 | # eg. ./mkrpm_el.sh el7 4 | dist=$1 5 | [ -z "$dist" ] && echo "$0 {dist}" && exit 1 6 | spectool -g -R sasutils-$dist.spec 7 | rpmbuild --define "dist .$dist" -ba sasutils-$dist.spec 8 | -------------------------------------------------------------------------------- /mkrpm_fedora.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Build RPM for Fedora 3 | # eg. ./mkrpm_fedora.sh fc16 4 | dist=$1 5 | [ -z "$dist" ] && echo "$0 {dist}" && exit 1 6 | spectool -g -R sasutils.spec 7 | rpmbuild --define "dist .$dist" -ba sasutils.spec 8 | -------------------------------------------------------------------------------- /sasutils-el7.spec: -------------------------------------------------------------------------------- 1 | Name: sasutils 2 | Version: 0.6.1 3 | Release: 1%{?dist} 4 | Summary: Serial Attached SCSI (SAS) utilities 5 | 6 | License: ASL 2.0 7 | URL: https://github.com/stanford-rc/sasutils 8 | Source0: https://github.com/stanford-rc/sasutils/archive/v%{version}/%{name}-%{version}.tar.gz 9 | 10 | BuildArch: noarch 11 | BuildRequires: python%{python3_pkgversion}-devel 12 | BuildRequires: python%{python3_pkgversion}-setuptools 13 | Requires: python%{python3_pkgversion}-setuptools 14 | Requires: sg3_utils 15 | Requires: smp_utils 16 | 17 | %{?python_provide:%python_provide python-sasutils} 18 | 19 | %description 20 | sasutils is a set of command-line tools and a Python library to ease the 21 | administration of Serial Attached SCSI (SAS) fabrics. 22 | 23 | %prep 24 | %setup -q 25 | 26 | %build 27 | %py3_build 28 | 29 | %install 30 | %py3_install 31 | install -d %{buildroot}/%{_mandir}/man1 32 | install -p -m 0644 doc/man/man1/sas_counters.1 %{buildroot}/%{_mandir}/man1/ 33 | install -p -m 0644 doc/man/man1/sas_devices.1 %{buildroot}/%{_mandir}/man1/ 34 | install -p -m 0644 doc/man/man1/sas_discover.1 %{buildroot}/%{_mandir}/man1/ 35 | install -p -m 0644 doc/man/man1/ses_report.1 %{buildroot}/%{_mandir}/man1/ 36 | 37 | %files 38 | %{_bindir}/sas_counters 39 | %{_bindir}/sas_devices 40 | %{_bindir}/sas_discover 41 | %{_bindir}/sas_mpath_snic_alias 42 | %{_bindir}/sas_sd_snic_alias 43 | %{_bindir}/sas_st_snic_alias 44 | %{_bindir}/ses_report 45 | %{python3_sitelib}/sasutils/ 46 | %{python3_sitelib}/sasutils-*-py%{python3_version}.egg-info 47 | %{_mandir}/man1/sas_counters.1* 48 | %{_mandir}/man1/sas_devices.1* 49 | %{_mandir}/man1/sas_discover.1* 50 | %{_mandir}/man1/ses_report.1* 51 | %doc README.rst 52 | %license LICENSE.txt 53 | 54 | %changelog 55 | * Mon Nov 11 2024 Stephane Thiell 0.6.1-1 56 | - update version 57 | 58 | * Fri Nov 8 2024 Stephane Thiell 0.6.0-1 59 | - update version 60 | 61 | * Sun Oct 1 2023 Stephane Thiell 0.5.0-1 62 | - update version 63 | 64 | * Thu Feb 16 2023 Stephane Thiell 0.4.0-1 65 | - update version 66 | 67 | * Mon Nov 14 2022 Stephane Thiell 0.3.13-1 68 | - update version 69 | 70 | * Mon Nov 15 2021 Stephane Thiell 0.3.12-1 71 | - update version 72 | 73 | * Fri Nov 12 2021 Stephane Thiell 0.3.11-1 74 | - update version 75 | 76 | * Sun Dec 08 2019 Stephane Thiell 0.3.10-1 77 | - update version 78 | - update Source to download from GitHub directly 79 | 80 | * Tue Aug 29 2017 Stephane Thiell 0.3.9-1 81 | - update version 82 | 83 | * Tue Aug 29 2017 Stephane Thiell 0.3.8-3 84 | - build for Python 3.4 in EPEL7 85 | 86 | * Tue Aug 22 2017 Stephane Thiell 0.3.8-2 87 | - always remove shebang from Python modules 88 | - removed unwanted Group tag 89 | - removed useless/duplicate Provides tag 90 | 91 | * Fri Aug 18 2017 Stephane Thiell 0.3.8-1 92 | - added man pages 93 | 94 | * Wed Aug 16 2017 Stephane Thiell 0.3.5-1 95 | - packaging improvements 96 | 97 | * Tue Jul 4 2017 Stephane Thiell 0.3.4-1 98 | - build against python3 only 99 | - install LICENSE.txt file 100 | - use python_provide macro and update to follow Fedora packaging guidelines 101 | 102 | * Sat May 20 2017 Stephane Thiell 0.3.3-1 103 | - update version (bug fixes) 104 | 105 | * Wed Mar 29 2017 Mikhail Lesin 0.3.2-1 106 | - Python 3 port 107 | - DM support 108 | - 4K devices sizefix 109 | 110 | * Mon Feb 20 2017 Stephane Thiell 0.3.1-1 111 | - update version 112 | 113 | * Sun Feb 19 2017 Stephane Thiell 0.3.0-1 114 | - update version 115 | 116 | * Fri Dec 9 2016 Stephane Thiell 0.2.5-1 117 | - update version 118 | 119 | * Mon Dec 5 2016 Stephane Thiell 0.2.4-1 120 | - update version 121 | 122 | * Tue Nov 8 2016 Stephane Thiell 0.2.3-1 123 | - update version 124 | 125 | * Mon Oct 31 2016 Stephane Thiell 0.2.1-1 126 | - update version 127 | 128 | * Mon Oct 17 2016 Stephane Thiell 0.1.7-1 129 | - inception 130 | -------------------------------------------------------------------------------- /sasutils.spec: -------------------------------------------------------------------------------- 1 | Name: sasutils 2 | Version: 0.6.1 3 | Release: 1%{?dist} 4 | Summary: Serial Attached SCSI (SAS) utilities 5 | 6 | License: ASL 2.0 7 | URL: https://github.com/stanford-rc/sasutils 8 | Source0: https://github.com/stanford-rc/sasutils/archive/v%{version}/%{name}-%{version}.tar.gz 9 | 10 | BuildArch: noarch 11 | BuildRequires: python3-devel 12 | BuildRequires: python3-setuptools 13 | Requires: python3-setuptools 14 | Requires: sg3_utils 15 | Requires: smp_utils 16 | 17 | %{?python_provide:%python_provide python-sasutils} 18 | 19 | %description 20 | sasutils is a set of command-line tools and a Python library to ease the 21 | administration of Serial Attached SCSI (SAS) fabrics. 22 | 23 | %prep 24 | %setup -q 25 | 26 | %build 27 | %py3_build 28 | 29 | %install 30 | %py3_install 31 | install -d %{buildroot}/%{_mandir}/man1 32 | install -p -m 0644 doc/man/man1/sas_counters.1 %{buildroot}/%{_mandir}/man1/ 33 | install -p -m 0644 doc/man/man1/sas_devices.1 %{buildroot}/%{_mandir}/man1/ 34 | install -p -m 0644 doc/man/man1/sas_discover.1 %{buildroot}/%{_mandir}/man1/ 35 | install -p -m 0644 doc/man/man1/ses_report.1 %{buildroot}/%{_mandir}/man1/ 36 | 37 | %files 38 | %{_bindir}/sas_counters 39 | %{_bindir}/sas_devices 40 | %{_bindir}/sas_discover 41 | %{_bindir}/sas_mpath_snic_alias 42 | %{_bindir}/sas_sd_snic_alias 43 | %{_bindir}/sas_st_snic_alias 44 | %{_bindir}/ses_report 45 | %{python3_sitelib}/sasutils/ 46 | %{python3_sitelib}/sasutils-*-py%{python3_version}.egg-info 47 | %{_mandir}/man1/sas_counters.1* 48 | %{_mandir}/man1/sas_devices.1* 49 | %{_mandir}/man1/sas_discover.1* 50 | %{_mandir}/man1/ses_report.1* 51 | %doc README.rst 52 | %license LICENSE.txt 53 | 54 | %changelog 55 | * Mon Nov 11 2024 Stephane Thiell 0.6.1-1 56 | - update version 57 | 58 | * Fri Nov 8 2024 Stephane Thiell 0.6.0-1 59 | - update version 60 | 61 | * Sun Oct 1 2023 Stephane Thiell 0.5.0-1 62 | - update version 63 | 64 | * Thu Feb 16 2023 Stephane Thiell 0.4.0-1 65 | - update version 66 | 67 | * Mon Nov 14 2022 Stephane Thiell 0.3.13-1 68 | - update version 69 | 70 | * Mon Nov 15 2021 Stephane Thiell 0.3.12-1 71 | - update version 72 | 73 | * Fri Nov 12 2021 Stephane Thiell 0.3.11-1 74 | - update version 75 | 76 | * Sun Dec 08 2019 Stephane Thiell 0.3.10-1 77 | - update version 78 | - update Source to download from GitHub directly 79 | 80 | * Tue Aug 29 2017 Stephane Thiell 0.3.9-1 81 | - update version 82 | 83 | * Tue Aug 22 2017 Stephane Thiell 0.3.8-2 84 | - always remove shebang from Python modules 85 | - removed unwanted Group tag 86 | - removed useless/duplicate Provides tag 87 | 88 | * Fri Aug 18 2017 Stephane Thiell 0.3.8-1 89 | - added man pages 90 | 91 | * Wed Aug 16 2017 Stephane Thiell 0.3.5-1 92 | - packaging improvements 93 | 94 | * Tue Jul 4 2017 Stephane Thiell 0.3.4-1 95 | - build against python3 only 96 | - install LICENSE.txt file 97 | - use python_provide macro and update to follow Fedora packaging guidelines 98 | 99 | * Sat May 20 2017 Stephane Thiell 0.3.3-1 100 | - update version (bug fixes) 101 | 102 | * Wed Mar 29 2017 Mikhail Lesin 0.3.2-1 103 | - Python 3 port 104 | - DM support 105 | - 4K devices sizefix 106 | 107 | * Mon Feb 20 2017 Stephane Thiell 0.3.1-1 108 | - update version 109 | 110 | * Sun Feb 19 2017 Stephane Thiell 0.3.0-1 111 | - update version 112 | 113 | * Fri Dec 9 2016 Stephane Thiell 0.2.5-1 114 | - update version 115 | 116 | * Mon Dec 5 2016 Stephane Thiell 0.2.4-1 117 | - update version 118 | 119 | * Tue Nov 8 2016 Stephane Thiell 0.2.3-1 120 | - update version 121 | 122 | * Mon Oct 31 2016 Stephane Thiell 0.2.1-1 123 | - update version 124 | 125 | * Mon Oct 17 2016 Stephane Thiell 0.1.7-1 126 | - inception 127 | -------------------------------------------------------------------------------- /sasutils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanford-rc/sasutils/fd8e57de6b1a15b0b7ea219ed5c440fb64ed4d30/sasutils/__init__.py -------------------------------------------------------------------------------- /sasutils/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanford-rc/sasutils/fd8e57de6b1a15b0b7ea219ed5c440fb64ed4d30/sasutils/cli/__init__.py -------------------------------------------------------------------------------- /sasutils/cli/sas_counters.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2017 Board of Trustees, Leland Stanford Jr. University 3 | # Written by Stephane Thiell 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | import argparse 19 | import socket 20 | import sys 21 | import time 22 | 23 | from sasutils.sas import SASHost 24 | from sasutils.ses import ses_get_snic_nickname 25 | from sasutils.scsi import MAP_TYPES 26 | from sasutils.sysfs import sysfs 27 | 28 | 29 | class SDNode(object): 30 | def __init__(self, baseobj, name=None, parent=None, prefix=''): 31 | self.name = name 32 | self.parent = parent 33 | self.baseobj = baseobj 34 | self.prefix = prefix 35 | self.children = [] 36 | self.nickname = None 37 | self.resolve() 38 | 39 | def resolve(self): 40 | pass 41 | 42 | def __str__(self): 43 | return self.name or '' 44 | 45 | def bottomup(self): 46 | path = [] 47 | if self.name: 48 | path.append(str(self)) 49 | if isinstance(self.parent, SDNode): 50 | path += self.parent.bottomup() 51 | elif self.prefix: 52 | path.append(self.prefix) 53 | return path 54 | 55 | def print_counter(self, key, value): 56 | path = self.bottomup() 57 | path.reverse() 58 | keybase = '.'.join(path).replace(' ', '_') 59 | # some counters in sysfs are hex numbers 60 | try: 61 | if value.startswith('0x'): 62 | value = int(value, 16) 63 | except AttributeError: 64 | pass 65 | print('%s.%s %s %d' % (keybase, key, value, time.time())) 66 | 67 | def add_child(self, sdclass, parent, baseobj, name=None): 68 | if not name: 69 | baseobjname = baseobj.name # mandatory when name not provided 70 | else: 71 | baseobjname = name 72 | self.children.append(sdclass(baseobj, baseobjname, parent)) 73 | 74 | def print_tree(self): 75 | for child in self.children: 76 | child.print_tree() 77 | 78 | 79 | class SDRootNode(SDNode): 80 | def resolve(self): 81 | for obj in self.baseobj: 82 | sas_host = SASHost(obj.node('device')) 83 | self.add_child(SDHostNode, self, sas_host) 84 | 85 | 86 | class SDHostNode(SDNode): 87 | def resolve(self): 88 | 89 | def portsortfunc(port_n): 90 | """helper sort function to return expanders first, then order by 91 | scsi device type and bay identifier""" 92 | if len(port_n.expanders) > 0: 93 | return [1, 0, 0] 94 | sortv = [0, 0, 0] # exp?, -type, bay 95 | try: 96 | if len(port_n.end_devices) > 0: 97 | if len(port_n.end_devices[0].targets) > 0: 98 | sortv[1] = -int(port_n.end_devices[0].targets[0].attrs 99 | .type) 100 | sortv[2] = int(port_n.end_devices[0].sas_device.attrs 101 | .bay_identifier) 102 | except (RuntimeError, ValueError): 103 | pass 104 | return sortv 105 | 106 | for port in sorted(self.baseobj.ports, key=portsortfunc): 107 | for expander in port.expanders: 108 | self.add_child(SDExpanderNode, self, expander) 109 | for end_device in port.end_devices: 110 | self.add_child(SDEndDeviceNode, self, end_device) 111 | 112 | # Phy counters 113 | for phy in self.baseobj.phys: 114 | phyid = phy.attrs.phy_identifier 115 | for key in ('invalid_dword_count', 'loss_of_dword_sync_count', 116 | 'phy_reset_problem_count', 117 | 'running_disparity_error_count'): 118 | # Extra sg or block dev info for convenience – when possible! 119 | extra = 'no_port' 120 | # Resolve block device (if any) from phy 121 | # - do not assume a phy has a port 122 | # - a port may not have any end_devices 123 | # - print block device first if available, then sg device 124 | if phy.port: 125 | extra = 'no_dev' 126 | if phy.port.end_devices: 127 | extra = 'no_target' 128 | tgts = phy.port.end_devices[0].targets 129 | if tgts: # usually true but let's be robust 130 | tgt = tgts[0] 131 | if tgt.block: 132 | extra = tgt.block.name 133 | else: 134 | extra = tgt.scsi_generic.sg_name 135 | phykey = 'phys.%s.%s.%s' % (phyid, extra, key) 136 | try: 137 | self.print_counter(phykey, phy.attrs.get(key)) 138 | except AttributeError as exc: 139 | print('%s: %s' % (phy, exc), file=sys.stderr) 140 | 141 | def __str__(self): 142 | board = self.baseobj.scsi_host.attrs.get('board_name', 'UNKNOWN_BOARD') 143 | addr = self.baseobj.scsi_host.attrs['host_sas_address'] 144 | return '.'.join((board, addr)) 145 | 146 | 147 | class SDExpanderNode(SDHostNode): 148 | def __str__(self): 149 | expander = self.baseobj 150 | if self.nickname: 151 | nick = self.nickname 152 | else: 153 | nick = 'expander_%s' % expander.sas_device.attrs['sas_address'] 154 | return '.'.join((expander.attrs.get('product_id', 'UNKNOWN'), nick)) 155 | 156 | 157 | class SDEndDeviceNode(SDNode): 158 | def resolve(self): 159 | for target in self.baseobj.targets: 160 | self.add_child(SDSCSIDeviceNode, self, target) 161 | 162 | def __str__(self): 163 | sas_end_device = self.baseobj 164 | 165 | bay = None 166 | try: 167 | bay = int(sas_end_device.sas_device.attrs.bay_identifier) 168 | except (AttributeError, ValueError): 169 | pass 170 | 171 | if bay is None: 172 | return 'no-bay.%s' % sas_end_device.name 173 | 174 | return 'bays.%s' % bay 175 | 176 | 177 | class SDSCSIDeviceNode(SDNode): 178 | def resolve(self): 179 | # Display device errors (work with both ses and sd drivers) 180 | scsi_device = self.baseobj 181 | self.print_counter('ioerr_cnt', scsi_device.attrs['ioerr_cnt']) 182 | self.print_counter('iodone_cnt', scsi_device.attrs['iodone_cnt']) 183 | self.print_counter('iorequest_cnt', scsi_device.attrs['iorequest_cnt']) 184 | 185 | def __str__(self): 186 | return self.get_scsi_device_info(self.baseobj) 187 | 188 | def get_scsi_device_info(self, scsi_device): 189 | # print(scsi_device.sysfsnode.path) 190 | dev_info = '.'.join((scsi_device.attrs.get('model', 'MODEL_UNKNOWN'), 191 | scsi_device.attrs['sas_address'])) 192 | 193 | scsi_type = scsi_device.attrs.type 194 | unknown_type = 'unknown[%s]' % scsi_type 195 | try: 196 | dev_type = MAP_TYPES.get(int(scsi_type), unknown_type) 197 | except ValueError: 198 | dev_type = 'unknown scsi type' 199 | 200 | if dev_type == 'enclosure': 201 | sg = scsi_device.scsi_generic 202 | snic = ses_get_snic_nickname(sg.name) 203 | # automatically resolve parent expander nickname 204 | self.parent.parent.nickname = snic 205 | if snic: 206 | return '.'.join((scsi_device.attrs.get('model', 207 | 'MODEL_UNKNOWN'), snic)) 208 | 209 | return dev_info 210 | 211 | 212 | def main(): 213 | """console_scripts entry point for sas_counters command-line.""" 214 | parser = argparse.ArgumentParser() 215 | parser.add_argument('--prefix', action='store', 216 | default='sasutils.sas_counters', 217 | help='carbon prefix (example: "datacenter.cluster",' 218 | ' default is "sasutils.sas_counters")') 219 | pargs = parser.parse_args() 220 | pfx = pargs.prefix.strip('.') 221 | try: 222 | # print short hostname as tree root node 223 | root_name = socket.gethostname().split('.')[0] 224 | root_obj = sysfs.node('class').node('sas_host') 225 | SDRootNode(root_obj, name=root_name, prefix=pfx).print_tree() 226 | except IOError: 227 | pass 228 | except KeyError as err: 229 | print("Not found: %s" % err, file=sys.stderr) 230 | 231 | 232 | if __name__ == '__main__': 233 | main() 234 | -------------------------------------------------------------------------------- /sasutils/cli/sas_devices.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2016, 2017, 2023 3 | # The Board of Trustees of the Leland Stanford Junior University 4 | # Written by Stephane Thiell 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | 19 | import argparse 20 | from collections import namedtuple, OrderedDict 21 | from itertools import groupby 22 | from operator import attrgetter 23 | from string import Formatter 24 | import re 25 | import struct 26 | import sys 27 | import time 28 | 29 | from sasutils.sas import SASHost, SASExpander, SASEndDevice 30 | from sasutils.scsi import EnclosureDevice, strtype, TYPE_ENCLOSURE 31 | from sasutils.ses import ses_get_snic_nickname 32 | from sasutils.sysfs import sysfs 33 | from sasutils.vpd import vpd_decode_pg83_lu, vpd_get_page83_lu 34 | from sasutils.vpd import vpd_get_page80_sn 35 | 36 | 37 | # header keywords 38 | FMT_MAP = { 'bay': 'BAY', 39 | 'blkdevs': 'BLOCK_DEVS', 40 | 'dm': 'DEVICE_MAPPER', 41 | 'model': 'MODEL', 42 | 'paths': 'PATHS', 43 | 'rev': 'REV', 44 | 'sgdevs': 'SG_DEVS', 45 | 'stdevs': 'ST_DEVS', 46 | 'size': 'SIZE', 47 | 'sn': 'SERIAL_NUMBER', 48 | 'snic': 'NICKNAME', 49 | 'state': 'STATE', 50 | 'target': 'TARGET', 51 | 'timeout': 'TIMEOUT', 52 | 'type': 'TYPE', 53 | 'vendor': 'VENDOR', 54 | 'wwid': 'WWID' } 55 | 56 | # default format string 57 | DEF_FMT = '{type:>10} {vendor:>12} {model:>16} {rev:>6} {size:>7} ' \ 58 | '{paths:>6} {state:>9}' 59 | 60 | # default format string in verbose mode 61 | DEF_FMT_VERB = '{bay:>3} {type:>10} {wwid:>24} {snic:>16} {dm:>18} ' \ 62 | '{blkdevs:>12} {stdevs:>8} {sgdevs:>12} {paths:>5} ' \ 63 | '{vendor:>8} {model:>16} {sn:>20} {rev:>8} {size:>7} ' \ 64 | '{target:>10} {state:>8}' 65 | 66 | 67 | class SASDevicesCLI(object): 68 | """Main class for sas_devises command-line interface.""" 69 | 70 | def __init__(self): 71 | parser = argparse.ArgumentParser() 72 | parser.add_argument('--quiet', '-q', action='store_true', 73 | help='Straight to the point') 74 | parser.add_argument('--verbose', '-v', action='count', default=0, 75 | help='Verbosity level, repeat multiple times!') 76 | parser.add_argument('--format', '-o', action='store', default=DEF_FMT, 77 | help='Specify the information to be displayed' \ 78 | ' (default: "%s" or "%s" with -v)' % (DEF_FMT, DEF_FMT_VERB)) 79 | self.args = parser.parse_args() 80 | if self.args.verbose > 0 and self.args.format is DEF_FMT: 81 | self.args.format = DEF_FMT_VERB 82 | 83 | if self.args.verbose > 1: 84 | print('FORMAT: "%s"' % self.args.format) 85 | 86 | self.fields = OrderedDict() 87 | 88 | for _, field, fmt_spec, _ in Formatter().parse(self.args.format): 89 | if field: 90 | if field in FMT_MAP: 91 | self.fields[field] = fmt_spec 92 | else: 93 | parser.error('Unknown format field "%s"' % field) 94 | if not self.fields: 95 | parser.error('No valid field found in format string') 96 | 97 | def print_hosts(self, sysfsnode): 98 | total = len(sysfsnode) 99 | tslen = len(str(total)) 100 | maxlen = num = 0 101 | sas_hosts = [] 102 | for sas_host in sysfsnode: 103 | num += 1 104 | if not self.args.quiet: 105 | towrite = '%s: %*d/%*d\r' % (sysfsnode, tslen, num, tslen, total) 106 | maxlen = max(len(towrite), maxlen) 107 | sys.stderr.write(towrite) 108 | sas_hosts.append(SASHost(sas_host.node('device'))) 109 | if not self.args.quiet: 110 | sys.stderr.write(' ' * maxlen + '\r') 111 | msgstr = "Found %d SAS hosts" % len(sas_hosts) 112 | if self.args.verbose > 1: 113 | print("%s: %s" % (msgstr, 114 | ','.join(host.name for host in sas_hosts))) 115 | elif self.args.verbose > 0: 116 | print(msgstr) 117 | 118 | def print_expanders(self, sysfsnode): 119 | total = len(sysfsnode) 120 | tslen = len(str(total)) 121 | maxlen = num = 0 122 | sas_expanders = [] 123 | for expander in sysfsnode: 124 | num += 1 125 | if not self.args.quiet: 126 | towrite = '%s: %*d/%*d\r' % (sysfsnode, tslen, num, tslen, total) 127 | maxlen = max(len(towrite), maxlen) 128 | sys.stderr.write(towrite) 129 | sas_expanders.append(SASExpander(expander.node('device'))) 130 | 131 | # Find unique expander thanks to their sas_address 132 | attrname = 'sas_device.attrs.sas_address' 133 | # Sort the expander list before using groupby() 134 | sas_expanders = sorted(sas_expanders, key=attrgetter(attrname)) 135 | # Group expanders by SAS address 136 | num_exp = 0 137 | for addr, expgroup in groupby(sas_expanders, attrgetter(attrname)): 138 | if self.args.verbose > 1: 139 | exps = list(expgroup) 140 | explist = ','.join(exp.name for exp in exps) 141 | print('SAS expander %s x%d (%s)' % (addr, len(exps), explist)) 142 | num_exp += 1 143 | 144 | if not self.args.quiet: 145 | sys.stderr.write(' ' * maxlen + '\r') 146 | if self.args.verbose > 0: 147 | print("Found %d SAS expanders" % num_exp) 148 | 149 | def _get_dev_attrs(self, sas_end_device, scsi_device): 150 | res = {} 151 | 152 | # scsi_device 153 | 154 | for key in ('model', 'rev', 'state', 'timeout', 'vendor'): 155 | res[key] = '' 156 | if key in self.fields: 157 | try: 158 | res[key] = getattr(scsi_device.attrs, key) 159 | except AttributeError as exc: 160 | print('ERROR: %s: %s' % (scsi_device, exc), file=sys.stderr) 161 | res[key] = '' 162 | 163 | if 'target' in self.fields: 164 | res['target'] = '' 165 | try: 166 | res['target'] = str(scsi_device.sysfsnode) 167 | except AttributeError as exc: 168 | print('ERROR: %s: %s' % (scsi_device, exc), file=sys.stderr) 169 | res['target'] = '' 170 | 171 | if 'type' in self.fields: 172 | res['type'] = '' 173 | try: 174 | res['type'] = scsi_device.strtype 175 | except AttributeError as exc: 176 | print('ERROR: %s: %s' % (scsi_device, exc), file=sys.stderr) 177 | res['type'] = '' 178 | 179 | # size of block device 180 | if 'size' in self.fields: 181 | res['size'] = '' 182 | # Size of block device 183 | if scsi_device.block: 184 | try: 185 | blk_sz = scsi_device.block.sizebytes() 186 | if blk_sz >= 1e12: 187 | blk_sz_info = "%.1fTB" % (blk_sz / 1e12) 188 | else: 189 | blk_sz_info = "%.1fGB" % (blk_sz / 1e9) 190 | res['size'] = blk_sz_info 191 | except AttributeError as exc: 192 | print('ERROR: %s: %s' % (scsi_device, exc), file=sys.stderr) 193 | res['size'] = '' 194 | 195 | # Device Mapper name 196 | if 'dm' in self.fields: 197 | res['dm'] = '' 198 | if scsi_device.block: 199 | try: 200 | res['dm'] = scsi_device.block.dm() 201 | except (AttributeError, ValueError): 202 | pass 203 | 204 | # Bay identifier 205 | if 'bay' in self.fields: 206 | res['bay'] = '' 207 | try: 208 | res['bay'] = int(sas_end_device.sas_device.attrs.bay_identifier) 209 | except (AttributeError, ValueError): 210 | pass 211 | 212 | if 'sn' in self.fields: 213 | res['sn'] = '' 214 | # Serial number 215 | try: 216 | pg80 = scsi_device.attrs.vpd_pg80 217 | res['sn'] = pg80[4:].decode("utf-8", errors='backslashreplace') 218 | except AttributeError: 219 | if scsi_device.block: 220 | pg80 = vpd_get_page80_sn(scsi_device.block.name) 221 | res['sn'] = pg80 222 | res['sn'] = res['sn'].strip() 223 | 224 | # SES Subenclosure nickname 225 | if 'snic' in self.fields: 226 | snic = None 227 | if int(scsi_device.attrs.type) == TYPE_ENCLOSURE: 228 | snic = ses_get_snic_nickname(scsi_device.scsi_generic.name) 229 | res['snic'] = snic or '' 230 | 231 | return res 232 | 233 | def _get_devlist_attrs(self, wwid, devlist, maxpaths=None): 234 | # use the first device for the following common attributes 235 | res = self._get_dev_attrs(*devlist[0]) 236 | 237 | if 'wwid' in self.fields: 238 | res['wwid'] = wwid 239 | 240 | if 'blkdevs' in self.fields: 241 | res['blkdevs'] = ','.join(scsi_device.block.name 242 | for sas, scsi_device in devlist 243 | if scsi_device.block) 244 | if 'sgdevs' in self.fields: 245 | res['sgdevs'] = ','.join(scsi_device.scsi_generic.sg_name 246 | for sas, scsi_device in devlist) 247 | if 'stdevs' in self.fields: 248 | res['stdevs'] = ','.join(scsi_device.tape.name 249 | for sas, scsi_device in devlist 250 | if scsi_device.tape) 251 | 252 | if 'paths' in self.fields: 253 | # Number of paths 254 | paths = "%d" % len(devlist) 255 | if maxpaths and len(devlist) < maxpaths: 256 | paths += "*" 257 | res['paths'] = paths 258 | 259 | return res 260 | 261 | def print_end_devices(self, sysfsnode): 262 | total = len(sysfsnode) 263 | tslen = len(str(total)) 264 | maxlen = num = 0 265 | 266 | # This code is ugly and should be rewritten... 267 | devmap = {} # LU -> list of (SASEndDevice, SCSIDevice) 268 | 269 | for node in sysfsnode: 270 | num += 1 271 | if not self.args.quiet: 272 | towrite = '%s: %*d/%*d\r' % (sysfsnode, tslen, num, tslen, total) 273 | maxlen = max(len(towrite), maxlen) 274 | sys.stderr.write(towrite) 275 | 276 | sas_end_device = SASEndDevice(node.node('device')) 277 | 278 | for scsi_device in sas_end_device.targets: 279 | if self.args.verbose > 1: 280 | print("Device: %s" % scsi_device.sysfsnode.path) 281 | 282 | wwid = scsi_device.attrs.wwid 283 | if not wwid: 284 | if scsi_device.block: 285 | try: 286 | try: 287 | pg83 = bytes(scsi_device.attrs.vpd_pg83) 288 | except TypeError: 289 | pg83 = bytes(scsi_device.attrs.vpd_pg83, 290 | encoding='utf-8') 291 | wwid = vpd_decode_pg83_lu(pg83) 292 | except (AttributeError, struct.error): 293 | wwid = vpd_get_page83_lu(scsi_device.block.name) 294 | except TypeError: 295 | wwid = 'Error-%s' % scsi_device.block.name 296 | print('Error: %s vpd_pg83="%s"' % (scsi_device.block, 297 | scsi_device.attrs.vpd_pg83), file=sys.stderr) 298 | 299 | devmap.setdefault(wwid, []).append((sas_end_device, 300 | scsi_device)) 301 | else: 302 | print('Error: no wwid for %s' % scsi_device.sysfsnode.path, 303 | file=sys.stderr) 304 | wwid = 'Error-%s' % scsi_device.sysfsnode 305 | if wwid.startswith('[Errno'): 306 | wwid = wwid.split(']')[0] + ']' 307 | devmap.setdefault(wwid, []).append((sas_end_device, scsi_device)) 308 | 309 | if not self.args.quiet: 310 | sys.stderr.write(' ' * maxlen + '\r') 311 | if self.args.verbose > 0: 312 | print("Found %d SAS end devices" % num) 313 | 314 | # list of set of enclosure 315 | encgroups = [] 316 | 317 | for lu, dev_list in devmap.items(): 318 | encs = set() 319 | for sas_ed, scsi_device in dev_list: 320 | if scsi_device.array_device: 321 | # 'enclosure_device' symlink is present (preferred method) 322 | encs.add(scsi_device.array_device.enclosure) 323 | if self.args.verbose > 1: 324 | print("Info: found enclosure device %s for device %s" \ 325 | % (scsi_device.array_device.enclosure.sysfsnode.path, 326 | scsi_device.sysfsnode.path)) 327 | else: 328 | encs.add(None) 329 | if self.args.verbose > 1: 330 | print("Info: no enclosure symlink set for %s in %s" % 331 | (scsi_device.name, scsi_device.sysfsnode.path)) 332 | done = False 333 | for encset in encgroups: 334 | if not encset.isdisjoint(encs): 335 | encset.update(encs) 336 | done = True 337 | break 338 | if not done: 339 | encgroups.append(encs) 340 | 341 | sys.stderr.write(' ' * maxlen + '\r') 342 | num_encgroups = len([enc for enc in encgroups if list(enc)[0]]) 343 | if self.args.verbose > 0: 344 | if num_encgroups > 0: 345 | print("Resolved %d enclosure groups" % num_encgroups) 346 | else: 347 | print("No enclosure found") 348 | 349 | for encset in encgroups: 350 | encinfolist = [] 351 | 352 | def kfun(o): 353 | if o: 354 | return int(re.sub("\D", "", o.scsi_generic.name)) 355 | else: 356 | return -1 357 | 358 | def enclosure_finder(arg): 359 | _lu, _dev_list = arg 360 | for _sas_ed, _scsi_device in _dev_list: 361 | if _scsi_device.array_device: 362 | # 'enclosure_device' symlink is present 363 | if _scsi_device.array_device.enclosure in encset: 364 | return True 365 | return False 366 | 367 | def enclosure_absent(arg): 368 | _lu, _dev_list = arg 369 | for _sas_ed, _scsi_device in _dev_list: 370 | if _scsi_device.array_device: 371 | # 'enclosure_device' symlink is present 372 | if _scsi_device.array_device.enclosure: 373 | return False 374 | return True 375 | 376 | has_orphans = False 377 | for enc in sorted(encset, key=kfun): 378 | if enc: 379 | snic = ses_get_snic_nickname(enc.scsi_generic.name) 380 | if snic: 381 | if self.args.verbose > 0: 382 | encinfolist.append('[%s:%s, addr: %s]' % 383 | (enc.scsi_generic.name, 384 | snic, enc.attrs.sas_address)) 385 | else: 386 | encinfolist.append('[%s:%s]' % (enc.scsi_generic.name, 387 | snic)) 388 | else: 389 | if self.args.verbose > 0: 390 | vals = (enc.scsi_generic.name, enc.attrs.vendor, 391 | enc.attrs.model, enc.attrs.sas_address) 392 | encinfolist.append('[%s:%s %s, addr: %s]' % vals) 393 | else: 394 | vals = (enc.attrs.vendor, enc.attrs.model, 395 | enc.attrs.sas_address) 396 | encinfolist.append('[%s %s, addr: %s]' % vals) 397 | else: 398 | has_orphans = True 399 | 400 | encdevs = [] 401 | if has_orphans: 402 | encdevs = list(filter(enclosure_absent, devmap.items())) 403 | 404 | if not encdevs: 405 | encdevs = list(filter(enclosure_finder, devmap.items())) 406 | 407 | maxpaths = max(len(devs) for lu, devs in encdevs) 408 | 409 | def kfun(o): 410 | try: 411 | return int(o[1][0][0].sas_device.attrs.bay_identifier) 412 | except ValueError: 413 | return -1 414 | 415 | cnt = 0 416 | grp_format = self.args.format 417 | field_trckr = {} 418 | folded = {} 419 | for lu, devlist in sorted(encdevs, key=kfun): 420 | devinfo = self._get_devlist_attrs(lu, devlist) 421 | # try to regroup devices by getting common attributes 422 | devinfo['paths'] = len(devlist) 423 | folded_key = namedtuple('FoldedDict', 424 | devinfo.keys())(**devinfo) 425 | folded.setdefault(folded_key, []).append(devlist) 426 | used = False 427 | for field in self.fields: 428 | if devinfo.get(field): 429 | field_trckr[field] = field_trckr.setdefault(field, 0) + 1 430 | if not used: 431 | used = True 432 | cnt += 1 433 | 434 | # remove unused columns 435 | for field in self.fields: 436 | fcnt = field_trckr.get(field, 0) 437 | if fcnt == 0: 438 | grp_format = re.sub('\s*\{%s.*?\}\s*' % field, ' ', 439 | grp_format) 440 | 441 | if cnt > 0: 442 | if has_orphans: 443 | print("*** Standalone devices") 444 | else: 445 | print("*** Enclosure group: %s" % ''.join(encinfolist)) 446 | 447 | hdrfmt = ' ' + grp_format 448 | print(hdrfmt.format(**FMT_MAP)) 449 | 450 | for t, v in folded.items(): 451 | if maxpaths and t.paths < maxpaths: 452 | pathstr = '%s*' % t.paths 453 | else: 454 | pathstr = '%s ' % t.paths 455 | 456 | infostr = grp_format.format(**t._asdict()) 457 | 458 | if self.args.verbose > 1: 459 | for devlist in v: 460 | for _, scsi_dev in devlist: 461 | print("Info: %s: %s" % (scsi_dev.__class__.__name__, 462 | scsi_dev.sysfsnode.path)) 463 | 464 | if len(v) > 1: 465 | print('%3d x %s' % (len(v), infostr)) 466 | else: 467 | print(' %s' % infostr) 468 | 469 | if not self.args.quiet: 470 | print("Total: %d devices" % cnt) 471 | 472 | 473 | def main(): 474 | """console_scripts entry point for sas_devices command-line.""" 475 | 476 | sas_devices_cli = SASDevicesCLI() 477 | 478 | try: 479 | root = sysfs.node('class').node('sas_host') 480 | sas_devices_cli.print_hosts(root) 481 | root = sysfs.node('class').node('sas_expander') 482 | sas_devices_cli.print_expanders(root) 483 | root = sysfs.node('class').node('sas_end_device') 484 | sas_devices_cli.print_end_devices(root) 485 | except KeyError as err: 486 | print("Not found: %s" % err, file=sys.stderr) 487 | 488 | 489 | if __name__ == '__main__': 490 | main() 491 | -------------------------------------------------------------------------------- /sasutils/cli/sas_discover.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2016, 2017 3 | # The Board of Trustees of the Leland Stanford Junior University 4 | # Written by Stephane Thiell 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | 19 | import argparse 20 | import ast 21 | from itertools import groupby 22 | import socket 23 | import sys 24 | 25 | from collections import Counter 26 | from sasutils.sas import SASHost 27 | from sasutils.ses import ses_get_snic_nickname 28 | from sasutils.scsi import TYPE_ENCLOSURE 29 | from sasutils.sysfs import sysfs 30 | 31 | 32 | def format_attrs(attrlist, attrs): 33 | """filter keys to avoid SysfsObject cache miss on all attrs""" 34 | attr_fmt = ('%s: {%s}' % t for t in attrlist) 35 | iargs = dict((k, attrs.get(k, 'N/A')) for _, k in attrlist) 36 | return ', '.join(attr_fmt).format(**iargs) 37 | 38 | 39 | class SDNode(object): 40 | gatherme = False 41 | 42 | def __init__(self, name, baseobj, nphys=0, speedstr='', depth=0, disp=None, 43 | prinfo=None): 44 | self.name = name 45 | self.baseobj = baseobj 46 | self.children = [] 47 | self.speedstr = speedstr 48 | self.nphys = nphys 49 | self.depth = depth 50 | self.disp = disp 51 | self.prinfo = prinfo or [] 52 | self.proffset = 0 # prompt offset; derived classes may override 53 | self._prompt = None 54 | self.resolve() 55 | 56 | def resolve(self): 57 | pass 58 | 59 | def __str__(self): 60 | return self.name 61 | 62 | def gathergrp(self): 63 | return None 64 | 65 | # 66 | # Helpers for text-based representation of the tree topology 67 | # 68 | def gen_prompt(self, prinfo=None): 69 | prompt = [] 70 | if not prinfo: 71 | prinfo = self.prinfo 72 | pilen = len(prinfo) 73 | for index, (plink, plen) in enumerate(prinfo): 74 | if plink == ' ': 75 | prompt.append(' ' * plen + plink + ' ') 76 | else: 77 | if index == pilen - 1: 78 | prompt.append(' ' * plen + plink + '--') 79 | else: 80 | prompt.append(' ' * plen + plink + ' ') 81 | if plink == '`': 82 | prinfo[index] = (' ', plen) 83 | return ''.join(prompt) 84 | 85 | def adv_prompt(self, offset=0, last=False): 86 | self.prompt # ensure current prompt is generated 87 | if last: 88 | plink = '`' 89 | else: 90 | plink = '|' 91 | return self.prinfo + [(plink, offset)] 92 | 93 | @property 94 | def prompt(self): 95 | if not self._prompt: 96 | self._prompt = self.gen_prompt() 97 | return self._prompt 98 | 99 | def add_child(self, sdclass, baseobj, name=None, nphys=0, speedstr='', last=False): 100 | if not name: 101 | baseobjname = baseobj.name # mandatory when name not provided 102 | else: 103 | baseobjname = name 104 | self.children.append(sdclass(baseobjname, baseobj, nphys, speedstr, 105 | self.depth + 1, self.disp, 106 | self.adv_prompt(self.proffset, last))) 107 | 108 | def print_tree(self): 109 | print('%s%s' % (self.prompt, self)) 110 | if self.children and all(child.gatherme for child in self.children): 111 | self.print_children_gathered() 112 | else: 113 | for child in self.children: 114 | child.print_tree() 115 | 116 | def print_children_gathered(self): 117 | self.children = sorted(self.children, key=lambda x: x.gathergrp()) 118 | groups = [(group, list(children)) for group, children 119 | in groupby(self.children, lambda x: x.gathergrp())] 120 | 121 | for index, (group, children) in enumerate(groups): 122 | last = bool(index == len(groups) - 1) 123 | prompt = self.gen_prompt(self.adv_prompt(last=last)) 124 | speed_info = '' 125 | if self.disp.get('verbose') > 0: 126 | speeds = [] 127 | counts = Counter(child.speedstr for child in children) 128 | for speed in counts: 129 | if counts[speed] == 1: 130 | speeds.append(speed) 131 | else: 132 | speeds.append('%d x %s' % (counts[speed], speed)) 133 | 134 | speed_info = ' (%s)' % ', '.join(speeds) 135 | print('%s %2d x %s%s' % (prompt, len(list(children)), group, 136 | speed_info)) 137 | 138 | class SDRootNode(SDNode): 139 | def resolve(self): 140 | sas_hosts = list(self.baseobj) 141 | for index, obj in enumerate(sas_hosts): 142 | sas_host = SASHost(obj.node('device')) 143 | last = bool(index == len(sas_hosts) - 1) 144 | self.add_child(SDHostNode, sas_host, last=last) 145 | 146 | 147 | class SDHostNode(SDNode): 148 | def resolve(self): 149 | 150 | def portsortfunc(p): 151 | """helper sort function to return expanders first, then order by 152 | scsi device type and bay identifier""" 153 | if len(p.expanders) > 0: 154 | return [1, 0, 0] 155 | sortv = [0, 0, 0] # exp?, -type, bay 156 | try: 157 | if len(p.end_devices) > 0: 158 | if len(p.end_devices[0].targets) > 0: 159 | sortv[1] = -int(p.end_devices[0].targets[0].attrs.type) 160 | sortv[2] = int(p.end_devices[0].sas_device.attrs 161 | .bay_identifier) 162 | except (AttributeError, IndexError, ValueError): 163 | pass 164 | return sortv 165 | 166 | ports = sorted(self.baseobj.ports, key=portsortfunc) 167 | for index, port in enumerate(ports): 168 | nphys = len(port.phys) 169 | try: 170 | speedstr = self._port_phys_linkrate(port) 171 | except AttributeError as exc: 172 | speedstr = 'ERROR: %s' % exc 173 | last = bool(index == len(self.baseobj.ports) - 1) 174 | for expander in port.expanders: 175 | self.add_child(SDExpanderNode, expander, nphys=nphys, speedstr=speedstr, last=last) 176 | for end_device in port.end_devices: 177 | self.add_child(SDEndDeviceNode, end_device, nphys=nphys, speedstr=speedstr, 178 | last=last) 179 | 180 | def __str__(self): 181 | verb = self.disp.get('verbose') 182 | 183 | info_fmt = [] 184 | if verb > 1: 185 | info_fmt += ['board: {board_name} {board_assembly} ' 186 | '{board_tracer}', 'product: {version_product}', 187 | 'bios: {version_bios}', 'fw: {version_fw}'] 188 | elif verb > 0: 189 | info_fmt.append('{board_name}') 190 | if self.disp.get('addr'): 191 | info_fmt.append('addr: {host_sas_address}') 192 | 193 | ikeys = ('board_name', 'board_assembly', 'board_tracer', 194 | 'host_sas_address', 'version_product', 'version_bios', 195 | 'version_fw') 196 | 197 | # sanitize values for display 198 | iargs = dict((k, self.baseobj.scsi_host.attrs.get(k, 'N/A')) 199 | for k in ikeys) 200 | 201 | return '%s %s' % (self.name, ', '.join(info_fmt).format(**iargs)) 202 | 203 | def _port_phys_linkrate(self, port): 204 | """Get linkrate for all port phys as string""" 205 | speeds = [] 206 | counts = Counter(phy.attrs.negotiated_linkrate for phy in port.phys) 207 | for speed in counts: 208 | if sys.stdout.isatty(): 209 | if counts[speed] != len(port.phys): 210 | fmt = "\033[91m{}\033[0m" 211 | else: 212 | fmt = "\033[92m{}\033[0m" 213 | else: 214 | fmt = "{}" 215 | speeds.append(fmt.format('%d x %s' % (counts[speed], speed))) 216 | return ', '.join(speeds) 217 | 218 | 219 | class SDExpanderNode(SDHostNode): 220 | def resolve(self): 221 | linkinfo = '%dx--' % self.nphys 222 | self.proffset = len(linkinfo) 223 | SDHostNode.resolve(self) 224 | 225 | def __str__(self): 226 | verb = self.disp.get('verbose') 227 | expander = self.baseobj 228 | 229 | if verb > 1: 230 | exp_info = ' ' + format_attrs((('vendor', 'vendor_id'), 231 | ('product', 'product_id'), 232 | ('rev', 'product_rev')), 233 | expander.attrs) 234 | speed_info = ' (%s)' % self.speedstr 235 | elif verb > 0: 236 | exp_info = ' ' + expander.attrs.get('vendor_id', 'N/A') 237 | speed_info = ' (%s)' % self.speedstr 238 | else: 239 | exp_info = '' 240 | speed_info = '' 241 | 242 | linkinfo = '%dx--' % self.nphys 243 | 244 | if self.disp['addr']: 245 | dev_info = ' ' + format_attrs((('addr', 'sas_address'),), 246 | expander.sas_device.attrs) 247 | else: 248 | dev_info = '' 249 | 250 | return '%s%s%s%s%s' % (linkinfo, expander.name, exp_info, dev_info, speed_info) 251 | 252 | 253 | class SDEndDeviceNode(SDNode): 254 | @property 255 | def gatherme(self): 256 | return (self.disp['verbose'] < 2 and not self.disp.get('addr') 257 | and not self.disp.get('counters') \ 258 | and not self.disp.get('devices')) and len(self.children) <= 1 259 | 260 | def gathergrp(self): 261 | if self.children: 262 | # special case: if child is an enclosure, we want to print more info 263 | child = self.children[0] 264 | undergrp = child.gathergrp() 265 | if undergrp == 'enclosure': 266 | undergrp = str(child) 267 | else: 268 | undergrp = self.name 269 | try: 270 | device_type = self.baseobj.sas_device.attrs.device_type 271 | device_type = device_type.replace(' ', '_') 272 | except AttributeError: 273 | device_type = 'unknown' 274 | 275 | return '%s -- %s' % (device_type, undergrp) 276 | 277 | def resolve(self): 278 | linkinfo = '%dx--' % self.nphys 279 | self.proffset = len(linkinfo) 280 | for index, target in enumerate(self.baseobj.targets): 281 | last = bool(index == len(self.baseobj.targets) - 1) 282 | self.add_child(SDSCSIDeviceNode, target, last=last, speedstr=self.speedstr) 283 | 284 | def __str__(self): 285 | verb = self.disp.get('verbose') 286 | linkinfo = '%dx--' % self.nphys 287 | sas_end_device = self.baseobj 288 | 289 | bay = None 290 | speed_info = '' 291 | if verb > 0: 292 | speed_info = " (%s)" % self.speedstr 293 | 294 | if verb > 1: 295 | try: 296 | bay = int(sas_end_device.sas_device.attrs.bay_identifier) 297 | except (AttributeError, ValueError): 298 | pass 299 | 300 | if bay is None: 301 | istr = '%s%s%s' % (linkinfo, sas_end_device.name, speed_info) 302 | else: 303 | istr = '%s%s%s bay: %d' % (linkinfo, sas_end_device.name, speed_info, bay) 304 | 305 | if self.disp.get('addr'): 306 | addr = sas_end_device.sas_device.attrs.sas_address 307 | istr = '%s addr: %s' % (istr, addr) 308 | 309 | return istr 310 | 311 | 312 | class SDSCSIDeviceNode(SDNode): 313 | # Note: additional instance attribute dinfo defined in resolve() 314 | 315 | @property 316 | def gatherme(self): 317 | if self.disp['verbose'] >= 1 or self.disp.get('addr') or \ 318 | self.disp.get('counters') or self.disp.get('devices'): 319 | return False 320 | 321 | # Do not gather enclosure 322 | try: 323 | return int(self.baseobj.attrs.type) != TYPE_ENCLOSURE 324 | except ValueError: 325 | return False 326 | 327 | def gathergrp(self): 328 | # gather group is our single child scsi type string 329 | return self.baseobj.strtype 330 | 331 | def resolve(self): 332 | verb = self.disp.get('verbose') 333 | self.dinfo, qinfo = self.get_scsi_device_info(self.baseobj, verb > 2) 334 | if qinfo: 335 | # spawn optional "block queue" tree leaves 336 | for index, (qattr, qval) in enumerate(qinfo.items()): 337 | last = bool(index == len(qinfo) - 1) 338 | self.add_child(SDBlockQueueNode, qval, name=qattr, last=last) 339 | 340 | def __str__(self): 341 | return self.dinfo # defined in resolve() 342 | 343 | def get_scsi_device_info(self, scsi_device, want_queue_attrs=False): 344 | verb = self.disp.get('verbose') 345 | dev_info_fmt = [] 346 | if verb == 1: 347 | dev_info_fmt.append('{vendor}') 348 | elif verb > 1: 349 | dev_info_fmt.append('vendor: {vendor}') 350 | if verb > 1: 351 | dev_info_fmt += ['model: {model}', 'rev: {rev}'] 352 | if self.disp.get('addr'): 353 | dev_info_fmt.append('addr: {sas_address}') 354 | if self.disp.get('counters'): 355 | dev_info_fmt.append('IO:{{req: {iorequest_cnt}') 356 | dev_info_fmt.append('done: {iodone_cnt}') 357 | dev_info_fmt.append('error: {ioerr_cnt}}}') 358 | 359 | ikeys = ( 'vendor', 'model', 'rev', 'sas_address' ) 360 | iargs = dict((k, scsi_device.attrs.get(k, 'N/A')) for k in ikeys) 361 | if self.disp.get('counters'): 362 | iokeys = ('ioerr_cnt', 'iodone_cnt', 'iorequest_cnt') 363 | for key in iokeys: 364 | iargs[key] = ast.literal_eval(scsi_device.attrs.get(key, 0)) 365 | 366 | dev_info = ' '.join(dev_info_fmt).format(**iargs) 367 | 368 | dev_sg = '' 369 | if self.disp.get('devices'): 370 | sg = scsi_device.scsi_generic 371 | if sg: 372 | dev_sg = '[%s]' % sg.name 373 | 374 | scsi_type = scsi_device.attrs.type 375 | dev_type = scsi_device.strtype 376 | 377 | if scsi_device.block: 378 | block = scsi_device.block 379 | 380 | size = block.sizebytes() 381 | if size >= 1e12: 382 | blk_info = "size: %.1fTB" % (size / 1e12) 383 | else: 384 | blk_info = "size: %.1fGB" % (size / 1e9) 385 | 386 | queue_attr_info = {} 387 | 388 | if want_queue_attrs: 389 | # -vvv: print all queue attributes 390 | queue_attr_info = dict(scsi_device.block.queue.attrs) 391 | 392 | return ' '.join( 393 | (dev_type, dev_info, blk_info, dev_sg)), queue_attr_info 394 | elif dev_type == 'enclosure': 395 | if verb > 0: 396 | sg = scsi_device.scsi_generic 397 | snic = ses_get_snic_nickname(sg.name) 398 | if snic: 399 | dev_type += ' %s' % snic 400 | 401 | return ' '.join((dev_type, dev_info, dev_sg)), {} 402 | 403 | 404 | class SDBlockQueueNode(SDNode): 405 | gatherme = False 406 | 407 | def __str__(self): 408 | return 'queue.%s: %s' % (self.name, self.baseobj) 409 | 410 | 411 | def main(): 412 | """console_scripts entry point for sas_discover command-line.""" 413 | parser = argparse.ArgumentParser() 414 | parser.add_argument('--verbose', '-v', action='count', default=0, 415 | help='Verbosity level, repeat multiple times!') 416 | parser.add_argument('--addr', action='store_true', default=False, 417 | help='Print SAS addresses') 418 | parser.add_argument('--devices', action='store_true', default=False, 419 | help='Print associated devices') 420 | parser.add_argument('--counters', action='store_true', default=False, 421 | help='Print I/O counters') 422 | pargs = parser.parse_args() 423 | 424 | try: 425 | # print short hostname as tree root node 426 | root_name = socket.gethostname().split('.')[0] 427 | root_obj = sysfs.node('class').node('sas_host') 428 | disp = {'verbose': pargs.verbose, 'addr': pargs.addr, 429 | 'devices': pargs.devices, 'counters': pargs.counters} 430 | root = SDRootNode(name=root_name, baseobj=root_obj, disp=disp) 431 | root.print_tree() 432 | except IOError: 433 | pass 434 | except KeyError as err: 435 | print("Not found: %s" % err, file=sys.stderr) 436 | 437 | 438 | if __name__ == '__main__': 439 | main() 440 | -------------------------------------------------------------------------------- /sasutils/cli/sas_mpath_snic_alias.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2016, 2017, 2023 3 | # The Board of Trustees of the Leland Stanford Junior University 4 | # Written by Stephane Thiell 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """Build useful udev aliases for dm-multipath SAS array devices. 19 | 20 | Each alias is built from the associated device SES-2 enclosure nickname 21 | (must be set) and the array device bay identifier. 22 | 23 | Example of udev rule: 24 | 25 | KERNEL=="dm-[0-9]*", PROGRAM="/usr/bin/sas_mpath_snic_alias %k", SYMLINK+="mapper/%c" 26 | """ 27 | 28 | import logging 29 | import os 30 | import sys 31 | 32 | from sasutils.sas import SASBlockDevice 33 | from sasutils.ses import ses_get_snic_nickname 34 | from sasutils.sysfs import sysfs 35 | 36 | ALIAS_FORMAT = '{nickname}-bay{bay_identifier:02d}' 37 | 38 | 39 | def sas_mpath_snic_alias(dmdev): 40 | """Use sasutils to get the alias name from the dm device.""" 41 | 42 | # Principle: 43 | # scan slaves block devices, for each: 44 | # get bay identifier 45 | # get enclosure device and its sg name 46 | # get snic (subenclosure nickname) 47 | # sanity: check if the bay identifers are the same 48 | # find common snic part and return result 49 | 50 | snics = [] 51 | bayids = [] 52 | 53 | # dm's underlying sd* devices can easily be found in 'slaves' 54 | for node in sysfs.node('block').node(dmdev).node('slaves'): 55 | 56 | # Instantiate a block device object with SAS attributes 57 | blkdev = SASBlockDevice(node.node('device')) 58 | sasdev = blkdev.end_device.sas_device 59 | wwid = '%s_unknown' % dmdev 60 | 61 | # 'enclosure_device' symlink is present (preferred method) 62 | # Use array_device and enclosure to retrieve the ses sg name 63 | ses_sg = blkdev.scsi_device.array_device.enclosure.scsi_generic.sg_name 64 | try: 65 | # Use the wwid of the enclosure to create enclosure-specifc 66 | # aliases if an enclosure nickname is not set 67 | wwid = blkdev.scsi_device.array_device.enclosure.attrs.wwid 68 | except AttributeError: 69 | pass 70 | 71 | # Retrieve bay_identifier from matching sas_device 72 | bayids.append(int(sasdev.attrs.bay_identifier)) 73 | 74 | # Get subenclosure nickname 75 | snic = ses_get_snic_nickname(ses_sg) or wwid 76 | snics.append(snic) 77 | 78 | if not bayids or not snics: 79 | return 80 | 81 | # assert that bay ids are the same... 82 | bay = bayids[0] 83 | assert bayids.count(bay) == len(bayids) 84 | 85 | snic = os.path.commonprefix(snics) 86 | snic = snic.rstrip('-_ ').replace(' ', '_') 87 | 88 | return ALIAS_FORMAT.format(nickname=snic, bay_identifier=bay) 89 | 90 | 91 | def main(): 92 | """Entry point for sas_mpath_snic_alias command-line.""" 93 | if len(sys.argv) != 2: 94 | print('Usage: %s ' % sys.argv[0], file=sys.stderr) 95 | sys.exit(1) 96 | try: 97 | result = sas_mpath_snic_alias(sys.argv[1]) 98 | if result: 99 | print(result) 100 | except KeyError as err: 101 | print("Not found: {0}".format(err), file=sys.stderr) 102 | sys.exit(1) 103 | 104 | 105 | if __name__ == '__main__': 106 | main() 107 | -------------------------------------------------------------------------------- /sasutils/cli/sas_sd_snic_alias.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2016, 2017, 2023 3 | # The Board of Trustees of the Leland Stanford Junior University 4 | # Written by Stephane Thiell 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """Build a useful udev alias from a SAS (array) block device. 19 | 20 | The alias is built from the associated enclosure nickname (must be set) 21 | and the array device bay identifier. 22 | 23 | Example of udev rule: 24 | 25 | KERNEL=="sd*", PROGRAM="/usr/bin/sas_sd_snic_alias %k", SYMLINK+="%c" 26 | """ 27 | 28 | import logging 29 | import sys 30 | 31 | from sasutils.sas import SASBlockDevice 32 | from sasutils.ses import ses_get_snic_nickname 33 | from sasutils.sysfs import sysfs 34 | 35 | ALIAS_FORMAT = '{nickname}-bay{bay_identifier:02d}' 36 | 37 | 38 | def sas_sd_snic_alias(blkdev): 39 | """Use sasutils library to get the alias name from the block device.""" 40 | 41 | # Instantiate SASBlockDevice object from block device sysfs node 42 | # eg. /sys/block/sdx/device 43 | blkdev = SASBlockDevice(sysfs.node('block').node(blkdev).node('device')) 44 | sasdev = blkdev.end_device.sas_device 45 | wwid = '%s_unknown' % blkdev.name 46 | 47 | # 'enclosure_device' symlink is present (preferred method) 48 | # Use array_device and enclosure to retrieve the ses sg name 49 | ses_sg = blkdev.scsi_device.array_device.enclosure.scsi_generic.sg_name 50 | try: 51 | # Use the wwid of the enclosure to create enclosure-specifc 52 | # aliases if an enclosure nickname is not set 53 | wwid = blkdev.scsi_device.array_device.enclosure.attrs.wwid 54 | except AttributeError: 55 | pass 56 | 57 | # Retrieve bay_identifier from matching sas_device 58 | bay = int(sasdev.attrs.bay_identifier) 59 | 60 | # Get subenclosure nickname 61 | snic = ses_get_snic_nickname(ses_sg) or wwid 62 | 63 | return ALIAS_FORMAT.format(nickname=snic, bay_identifier=bay) 64 | 65 | 66 | def main(): 67 | """Entry point for sas_sd_snic_alias command-line.""" 68 | if len(sys.argv) != 2: 69 | print('Usage: %s ' % sys.argv[0], file=sys.stderr) 70 | sys.exit(1) 71 | try: 72 | result = sas_sd_snic_alias(sys.argv[1]) 73 | if result: 74 | print(result) 75 | except KeyError as err: 76 | print("Not found: {0}".format(err), file=sys.stderr) 77 | sys.exit(1) 78 | 79 | 80 | if __name__ == '__main__': 81 | main() 82 | -------------------------------------------------------------------------------- /sasutils/cli/sas_st_snic_alias.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2023 3 | # The Board of Trustees of the Leland Stanford Junior University 4 | # Written by Stephane Thiell 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """Build a useful udev alias for a SAS tape drive attached to a SAS switch. 19 | 20 | The alias is built from the associated enclosure nickname (must be set) 21 | of the attached SAS switch and the array device bay identifier provided by 22 | sysfs. Feel free to take this script as an example and adapt to your needs. 23 | 24 | Example of udev rule: 25 | 26 | KERNEL=="st*", PROGRAM="/usr/bin/sas_st_snic_alias %k", SYMLINK+="%c" 27 | """ 28 | 29 | import logging 30 | import sys 31 | 32 | from sasutils.sas import SASTapeDevice 33 | from sasutils.ses import ses_get_snic_nickname 34 | from sasutils.sysfs import sysfs 35 | 36 | ALIAS_FORMAT = 'st-{nickname}-bay{bay_identifier:02d}' 37 | 38 | 39 | def sas_st_snic_alias(stdev): 40 | """Use sasutils library to get the alias name from the tape device.""" 41 | 42 | # Instantiate SCSITapeDevice object from scsi_tape device sysfs node 43 | # eg. /sys/class/scsi_tape/st7 44 | tapedev = SASTapeDevice(sysfs.node('class').node('scsi_tape') \ 45 | .node(stdev).node('device')) 46 | sasdev = tapedev.end_device.sas_device 47 | 48 | wwid = '%s_unknown' % tapedev.name 49 | 50 | # 'enclosure_device' symlink is present (preferred method) 51 | # Use array_device and enclosure to retrieve the ses sg name 52 | ses_sg = tapedev.scsi_device.array_device.enclosure.scsi_generic.sg_name 53 | try: 54 | # Use the wwid of the enclosure to create enclosure-specifc 55 | # aliases if an enclosure nickname is not set 56 | wwid = tapedev.scsi_device.array_device.enclosure.attrs.wwid 57 | except AttributeError: 58 | pass 59 | 60 | # Retrieve bay_identifier from matching sas_device 61 | bay = int(sasdev.attrs.bay_identifier) 62 | 63 | # Get subenclosure nickname 64 | snic = ses_get_snic_nickname(ses_sg) or wwid 65 | 66 | return ALIAS_FORMAT.format(nickname=snic, bay_identifier=bay) 67 | 68 | 69 | def main(): 70 | """Entry point for sas_st_snic_alias command-line.""" 71 | if len(sys.argv) != 2: 72 | print('Usage: %s ' % sys.argv[0], file=sys.stderr) 73 | sys.exit(1) 74 | try: 75 | result = sas_st_snic_alias(sys.argv[1]) 76 | if result: 77 | print(result) 78 | except KeyError as err: 79 | print("Not found: {0}".format(err), file=sys.stderr) 80 | sys.exit(1) 81 | 82 | 83 | if __name__ == '__main__': 84 | main() 85 | -------------------------------------------------------------------------------- /sasutils/cli/ses_report.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2016, 2017 3 | # The Board of Trustees of the Leland Stanford Junior University 4 | # Written by Stephane Thiell 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | 19 | """ 20 | ses_report - SES status and metrics reporting utility 21 | """ 22 | 23 | import argparse 24 | import json 25 | import logging 26 | import time 27 | import sys 28 | 29 | from sasutils.scsi import EnclosureDevice 30 | from sasutils.ses import ses_get_ed_metrics, ses_get_ed_status 31 | from sasutils.ses import ses_get_snic_nickname 32 | from sasutils.sysfs import sysfs 33 | 34 | 35 | def _init_argparser(): 36 | """Initialize argparser object for ses_report command-line.""" 37 | desc = 'SES status and metrics reporting utility (part of sasutils).' 38 | parser = argparse.ArgumentParser(description=desc) 39 | parser.add_argument('-d', '--debug', action="store_true", 40 | help='enable debugging') 41 | 42 | group = parser.add_mutually_exclusive_group(required=True) 43 | group.add_argument('-c', '--carbon', action='store_true', 44 | help='output SES Element descriptors metrics in a ' 45 | 'format suitable for Carbon/Graphite') 46 | group.add_argument('-s', '--status', action='store_true', 47 | help='output status found in SES Element descriptors') 48 | 49 | group = parser.add_argument_group('output options') 50 | group.add_argument('--prefix', action='store', 51 | default='sasutils.ses_report', 52 | help='carbon prefix (example: "datacenter.cluster",' 53 | ' default is "sasutils.ses_report")') 54 | group.add_argument('-j', '--json', action='store_true', 55 | help='alternative JSON output mode') 56 | return parser.parse_args() 57 | 58 | 59 | def ses_report(): 60 | """ses_report command-line""" 61 | pargs = _init_argparser() 62 | if pargs.debug: 63 | # debugging on the same stream is recommended (stdout) 64 | logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) 65 | 66 | pfx = pargs.prefix.strip('.') 67 | if pfx: 68 | pfx += '.' 69 | 70 | json_encl_dict = {} 71 | 72 | # Iterate over sysfs SCSI enclosures 73 | for node in sysfs.node('class').node('enclosure'): 74 | # Get enclosure device 75 | enclosure = EnclosureDevice(node.node('device')) 76 | # Get enclosure SG device 77 | sg_dev = enclosure.scsi_generic 78 | # Resolve SES enclosure nickname 79 | snic = ses_get_snic_nickname(sg_dev.name) 80 | if snic: 81 | snic = snic.replace(' ', '_') 82 | else: 83 | # Use Vendor + SAS address if SES encl. nickname not defined 84 | snic = enclosure.attrs.vendor.replace(' ', '-') 85 | snic += '_' + enclosure.attrs.sas_address 86 | 87 | if pargs.carbon: 88 | if pargs.json: 89 | encl_json_list = [] 90 | for edinfo in ses_get_ed_metrics(sg_dev.name): 91 | encl_json_list.append(edinfo) 92 | json_encl_dict[snic] = encl_json_list 93 | else: 94 | time_now = time.time() 95 | for edinfo in ses_get_ed_metrics(sg_dev.name): 96 | # Print output using Carbon format 97 | fmt = '{element_type}.{descriptor}.{key}_{unit} {value}' 98 | path = fmt.format(**edinfo) 99 | print('%s%s.%s %d' % (pfx, snic, path, time_now)) 100 | else: 101 | if pargs.json: 102 | encl_json_list = [] 103 | for edstatus in ses_get_ed_status(sg_dev.name): 104 | encl_json_list.append(edstatus) 105 | json_encl_dict[snic] = encl_json_list 106 | else: 107 | for edstatus in ses_get_ed_status(sg_dev.name): 108 | fmt = '{element_type}.{descriptor} {status}' 109 | output = fmt.format(**edstatus) 110 | print('%s%s.%s' % (pfx, snic, output)) 111 | 112 | if pargs.json: 113 | print(json.dumps(json_encl_dict, sort_keys=True, indent=4)) 114 | 115 | 116 | def main(): 117 | """console_scripts entry point for ses_report""" 118 | try: 119 | ses_report() 120 | except KeyError as err: 121 | print("Not found: {0}".format(err), file=sys.stderr) 122 | sys.exit(1) 123 | 124 | 125 | if __name__ == '__main__': 126 | main() 127 | -------------------------------------------------------------------------------- /sasutils/sas.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2016 3 | # The Board of Trustees of the Leland Stanford Junior University 4 | # Written by Stephane Thiell 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | import logging 19 | 20 | from sasutils.scsi import SCSIDevice, SCSIHost 21 | from sasutils.scsi import BlockDevice, TapeDevice 22 | from sasutils.sysfs import SysfsDevice 23 | 24 | 25 | LOGGER = logging.getLogger(__name__) 26 | 27 | # 28 | # SAS topology components 29 | # 30 | 31 | class SASPhy(SysfsDevice): 32 | def __init__(self, device, subsys='sas_phy'): 33 | SysfsDevice.__init__(self, device, subsys) 34 | self._port = None 35 | 36 | @property 37 | def port(self): 38 | if self._port: 39 | return self._port 40 | try: 41 | self._port = SASPort(self.sysfsnode.node('device/port')) 42 | except KeyError: 43 | pass # phy has no port 44 | return self._port 45 | 46 | 47 | class SASPort(SysfsDevice): 48 | def __init__(self, device, subsys='sas_port'): 49 | SysfsDevice.__init__(self, device, subsys) 50 | self.expanders = [] 51 | self.end_devices = [] 52 | self.phys = [] 53 | 54 | phys = self.device.glob('phy-*') 55 | for phy in phys: 56 | self.phys.append(SASPhy(phy)) 57 | 58 | end_devices = self.device.glob('end_device-*') 59 | for end_device in end_devices: 60 | self.end_devices.append(SASEndDevice(end_device)) 61 | 62 | expanders = self.device.glob('expander-*') 63 | for expander in expanders: 64 | self.expanders.append(SASExpander(expander)) 65 | 66 | 67 | class SASNode(SysfsDevice): 68 | def __init__(self, device, subsys=None): 69 | SysfsDevice.__init__(self, device, subsys) 70 | self.phys = [] 71 | self.ports = [] 72 | 73 | ports = self.device.glob('port-*') 74 | for port in ports: 75 | # print('node has port %s' % port.path) 76 | self.ports.append( 77 | SASPort(port)) # port.node(port))) #'sas_port/port-*'))) 78 | 79 | phys = self.device.glob('phy-*') 80 | for phy in phys: 81 | # print('node has phy %s' % phy.path) 82 | self.phys.append( 83 | SASPhy(phy)) # .node(phy))) #phy.node('sas_phy/phy-*'))) 84 | 85 | def __repr__(self): 86 | return '<%s.%s %s phys=%d ports=%d>' % (self.__module__, 87 | self.__class__.__name__, 88 | self.sysfsnode.path, 89 | len(self.phys), len(self.ports)) 90 | 91 | __str__ = __repr__ 92 | 93 | def end_devices_by_scsi_type(self, device_type): 94 | """ 95 | Iterate over end_devices (direct children) by scsi type. 96 | SCSI types are defined in the scsi module. 97 | """ 98 | for port in self.ports: 99 | for end_device in port.end_devices: 100 | if int(end_device.scsi_device.attrs.type) == int(device_type): 101 | yield end_device 102 | 103 | 104 | class SASHost(SASNode): 105 | def __init__(self, device, subsys='sas_host'): 106 | SASNode.__init__(self, device, subsys) 107 | self.scsi_host = SCSIHost(device) 108 | 109 | def __str__(self): 110 | return '<%s.%s %s>' % (self.__module__, self.__class__.__name__, 111 | self.__dict__) 112 | 113 | 114 | class SASExpander(SASNode): 115 | def __init__(self, device, subsys='sas_expander'): 116 | SASNode.__init__(self, device, subsys) 117 | self.sas_device = SASDevice(device) 118 | 119 | 120 | class SASDevice(SysfsDevice): 121 | def __init__(self, device, subsys='sas_device'): 122 | SysfsDevice.__init__(self, device, subsys) 123 | 124 | 125 | class SASEndDevice(SysfsDevice): 126 | def __init__(self, device, subsys='sas_end_device'): 127 | SysfsDevice.__init__(self, device, subsys) 128 | self.sas_device = SASDevice(device) 129 | # a single SAS end device may handle several SCSI targets 130 | self.targets = [] 131 | for dev in device.glob('target*/*[0-9]'): 132 | try: 133 | self.targets.append(SCSIDevice(dev)) 134 | except KeyError as err: 135 | # eg. scsi_generic doesn't exist 136 | LOGGER.warning("WARNING: sysfs %s weirdness: %s" % \ 137 | (subsys, repr(err))) 138 | 139 | 140 | # 141 | # Other useful SAS classes 142 | # 143 | 144 | class SASBlockDevice(BlockDevice): 145 | """ 146 | SAS-aware block device class that allows direct access to SASEndDevice. 147 | """ 148 | 149 | def __init__(self, device): 150 | BlockDevice.__init__(self, device) 151 | self._end_device = None 152 | 153 | @property 154 | def end_device(self): 155 | if not self._end_device: 156 | self._end_device = SASEndDevice(self.sysfsnode.node('../../../..')) 157 | return self._end_device 158 | 159 | 160 | class SASTapeDevice(TapeDevice): 161 | """ 162 | SAS-aware tape device class that allows direct access to SASEndDevice. 163 | """ 164 | 165 | def __init__(self, device): 166 | TapeDevice.__init__(self, device) 167 | self._end_device = None 168 | 169 | @property 170 | def end_device(self): 171 | if not self._end_device: 172 | self._end_device = SASEndDevice(self.sysfsnode.node('../../../..')) 173 | return self._end_device 174 | -------------------------------------------------------------------------------- /sasutils/scsi.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2016 3 | # The Board of Trustees of the Leland Stanford Junior University 4 | # Written by Stephane Thiell 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | import re 19 | import warnings 20 | 21 | from sasutils.sysfs import SysfsDevice, SysfsObject 22 | 23 | 24 | # DEVICE TYPES 25 | # https://en.wikipedia.org/wiki/SCSI_Peripheral_Device_Type 26 | 27 | TYPE_DISK = 0x00 28 | TYPE_TAPE = 0x01 29 | TYPE_PRINTER = 0x02 30 | TYPE_PROCESSOR = 0x03 # HP scanners use this 31 | TYPE_WORM = 0x04 # Treated as ROM by our system 32 | TYPE_ROM = 0x05 33 | TYPE_SCANNER = 0x06 34 | TYPE_MOD = 0x07 # Magneto-optical disk treated as TYPE_DISK 35 | TYPE_MEDIUM_CHANGER = 0x08 36 | TYPE_COMM = 0x09 # Communications device 37 | TYPE_RAID = 0x0c 38 | TYPE_ENCLOSURE = 0x0d # Enclosure Services Device 39 | TYPE_RBC = 0x0e 40 | TYPE_OSD = 0x11 41 | TYPE_NO_LUN = 0x7f 42 | 43 | # Numeric SCSI type to string mapping 44 | 45 | MAP_TYPES = {TYPE_DISK: 'disk', 46 | TYPE_TAPE: 'tape', 47 | TYPE_PRINTER: 'printer', 48 | TYPE_PROCESSOR: 'processor', 49 | TYPE_WORM: 'worm', 50 | TYPE_ROM: 'rom', 51 | TYPE_SCANNER: 'scanner', 52 | TYPE_MOD: 'mod', 53 | TYPE_MEDIUM_CHANGER: 'medium_changer', 54 | TYPE_COMM: 'comm', 55 | TYPE_RAID: 'raid', 56 | TYPE_ENCLOSURE: 'enclosure', 57 | TYPE_RBC: 'rbc', 58 | TYPE_OSD: 'osd', 59 | TYPE_NO_LUN: 'no_lun'} 60 | 61 | 62 | def strtype(scsi_type): 63 | try: 64 | return MAP_TYPES[int(scsi_type)] 65 | except ValueError: 66 | return "unknown(%s)" % scsi_type 67 | 68 | # 69 | # SCSI classes 70 | # 71 | 72 | 73 | class SCSIHost(SysfsDevice): 74 | 75 | def __init__(self, device, subsys='scsi_host'): 76 | SysfsDevice.__init__(self, device, subsys) 77 | 78 | 79 | class SCSIDisk(SysfsDevice): 80 | 81 | def __init__(self, device, subsys='scsi_disk'): 82 | SysfsDevice.__init__(self, device, subsys) 83 | 84 | 85 | class SCSIGeneric(SysfsDevice): 86 | 87 | def __init__(self, device, subsys='scsi_generic'): 88 | SysfsDevice.__init__(self, device, subsys) 89 | # the basename of self.sysfsnode is the name of the sg device 90 | self.sg_name = str(self.sysfsnode) 91 | 92 | 93 | class SCSIDevice(SysfsObject): 94 | """ 95 | scsi_device 96 | 97 | SCSIDevice -> array_device (ArrayDevice) -> enclosure (EnclosureDevice) 98 | """ 99 | 100 | def __init__(self, device): 101 | # scsi_device attrs attached to device 102 | SysfsObject.__init__(self, device) 103 | self.scsi_generic = SCSIGeneric(self.sysfsnode) 104 | try: 105 | self.scsi_disk = SCSIDisk(self.sysfsnode) 106 | except KeyError: 107 | self.scsi_disk = None 108 | try: 109 | self.block = BlockDevice(self.sysfsnode, scsi_device=self) 110 | except KeyError: 111 | self.block = None 112 | try: 113 | self.tape = TapeDevice(self.sysfsnode, scsi_device=self) 114 | except KeyError: 115 | self.tape = None 116 | try: 117 | # define scsi type string as strtype for convenience 118 | self.strtype = strtype(self.attrs.type) 119 | except AttributeError: 120 | self.strtype = None 121 | self._array_device = None 122 | 123 | @property 124 | def array_device(self): 125 | if not self._array_device: 126 | try: 127 | array_node = self.sysfsnode.node('enclosure_device:*') 128 | self._array_device = ArrayDevice(array_node) 129 | except KeyError: 130 | # no enclosure_device, this may happen due to sysfs issues 131 | pass 132 | return self._array_device 133 | 134 | 135 | # 136 | # SCSI bus classes 137 | # 138 | 139 | class EnclosureDevice(SCSIDevice): 140 | """Managed enclosure device""" 141 | 142 | def __init__(self, device): 143 | SCSIDevice.__init__(self, device) 144 | 145 | 146 | class ArrayDevice(SysfsObject): 147 | 148 | def __init__(self, sysfsnode): 149 | SysfsObject.__init__(self, sysfsnode) 150 | self.enclosure = EnclosureDevice(sysfsnode.node('../device')) 151 | 152 | # 153 | # Block devices 154 | # 155 | 156 | class BlockDevice(SysfsDevice): 157 | """ 158 | scsi_disk 159 | """ 160 | def __init__(self, device, subsys='block', scsi_device=None): 161 | SysfsDevice.__init__(self, device, subsys, sysfsdev_pattern='sd*') 162 | self._scsi_device = scsi_device 163 | self.queue = SysfsObject(self.sysfsnode.node('queue')) 164 | 165 | def json_serialize(self): 166 | data = dict(self.__dict__) 167 | if self._scsi_device is not None: 168 | data['_scsi_device'] = repr(self._scsi_device) 169 | return data 170 | 171 | @property 172 | def array_device(self): 173 | # moved to SCSIDevice but kept here for compat 174 | warnings.warn("use .scsi_device.array_device instead", 175 | DeprecationWarning) 176 | return self.scsi_device.array_device 177 | 178 | @property 179 | def scsi_device(self): 180 | if not self._scsi_device: 181 | self._scsi_device = SCSIDevice(self.device) 182 | return self._scsi_device 183 | 184 | def sizebytes(self): 185 | """Return block device size in bytes""" 186 | blk_size = float(self.attrs.size) 187 | # Block size is expressed in 512b sectors regardless of 188 | # underlaying disk structure. 189 | # See https://goo.gl/L8GZCG for details 190 | return blk_size * 512 191 | 192 | def dm(self): 193 | """Return /dev/mapper device name if present""" 194 | try: 195 | dm_dev = SysfsDevice(self.sysfsnode, subsys="holders", 196 | sysfsdev_pattern="*[0-9]/dm") 197 | except KeyError: 198 | return "[Not mapped]" 199 | return dm_dev.attrs.name 200 | 201 | # 202 | # Tape devices 203 | # 204 | 205 | class TapeDevice(SysfsDevice): 206 | """ 207 | scsi_tape 208 | """ 209 | def __init__(self, device, subsys='scsi_tape', scsi_device=None): 210 | SysfsDevice.__init__(self, device, subsys, 211 | sysfsdev_pattern=re.compile(r'st[0-9]+')) 212 | self._scsi_device = scsi_device 213 | 214 | @property 215 | def scsi_device(self): 216 | if not self._scsi_device: 217 | self._scsi_device = SCSIDevice(self.device) 218 | return self._scsi_device 219 | -------------------------------------------------------------------------------- /sasutils/ses.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2016, 2021 3 | # The Board of Trustees of the Leland Stanford Junior University 4 | # Written by Stephane Thiell 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """SES utilities 19 | 20 | Requires sg_ses from sg3_utils (recent version, like 1.77). 21 | """ 22 | 23 | import logging 24 | import re 25 | import subprocess 26 | 27 | __author__ = 'sthiell@stanford.edu (Stephane Thiell)' 28 | 29 | LOGGER = logging.getLogger(__name__) 30 | 31 | 32 | def ses_get_snic_nickname(sg_name): 33 | """Get subenclosure nickname (SES-2) [snic]""" 34 | support_snic = False 35 | 36 | # SES nickname is not available through sysfs, use sg_ses tool instead 37 | cmdargs = ['sg_ses', '--status', '/dev/' + sg_name] 38 | LOGGER.debug('ses_get_snic_nickname: executing: %s', cmdargs) 39 | try: 40 | stdout, stderr = subprocess.Popen(cmdargs, 41 | stdout=subprocess.PIPE, 42 | stderr=subprocess.PIPE).communicate() 43 | except OSError as err: 44 | LOGGER.warning('ses_get_snic_nickname: %s', err) 45 | return None 46 | 47 | for line in stderr.decode("utf-8").splitlines(): 48 | LOGGER.debug('ses_get_snic_nickname: sg_ses(stderr): %s', line) 49 | 50 | for line in stdout.decode("utf-8").splitlines(): 51 | LOGGER.debug('ses_get_snic_nickname: sg_ses: %s', line) 52 | if '[snic]' in line: 53 | support_snic = True 54 | break 55 | 56 | if not support_snic: 57 | return None 58 | 59 | cmdargs = ['sg_ses', '--page=snic', '-I0', '/dev/' + sg_name] 60 | LOGGER.debug('ses_get_snic_nickname: executing: %s', cmdargs) 61 | try: 62 | stdout, stderr = subprocess.Popen(cmdargs, 63 | stdout=subprocess.PIPE, 64 | stderr=subprocess.PIPE).communicate() 65 | except OSError as err: 66 | LOGGER.warning('ses_get_snic_nickname: %s', err) 67 | return None 68 | 69 | for line in stderr.decode("utf-8", errors='backslashreplace').splitlines(): 70 | LOGGER.debug('ses_get_snic_nickname: sg_ses(stderr): %s', line) 71 | 72 | for line in stdout.decode("utf-8", errors='backslashreplace').splitlines(): 73 | LOGGER.debug('ses_get_snic_nickname: sg_ses: %s', line) 74 | mobj = re.match(r'\s+nickname:\s*([^ ]+)', line) 75 | if mobj: 76 | return mobj.group(1) 77 | 78 | def ses_set_snic_nickname(sg_name, nickname): 79 | """Set subenclosure nickname (SES-2) [snic]""" 80 | support_snic = False 81 | 82 | # SES nickname is not available through sysfs, use sg_ses tool instead 83 | cmdargs = ['sg_ses', '--status', '/dev/' + sg_name] 84 | LOGGER.debug('ses_set_snic_nickname: executing: %s', cmdargs) 85 | try: 86 | stdout, stderr = subprocess.Popen(cmdargs, 87 | stdout=subprocess.PIPE, 88 | stderr=subprocess.PIPE).communicate() 89 | except OSError as err: 90 | LOGGER.error('ses_set_snic_nickname: %s', err) 91 | return 92 | 93 | for line in stderr.decode("utf-8").splitlines(): 94 | LOGGER.debug('ses_set_snic_nickname: sg_ses(stderr): %s', line) 95 | 96 | for line in stdout.decode("utf-8").splitlines(): 97 | LOGGER.debug('ses_set_snic_nickname: sg_ses: %s', line) 98 | if '[snic]' in line: 99 | support_snic = True 100 | break 101 | 102 | if not support_snic: 103 | raise OSError(errno.ENOSYS, 'snic not supported by hardware') 104 | 105 | cmdargs = ['sg_ses', '--control', "--nickname=%s" % nickname, 106 | '/dev/' + sg_name] 107 | try: 108 | stdout, stderr = subprocess.Popen(cmdargs, 109 | stdout=subprocess.PIPE, 110 | stderr=subprocess.PIPE).communicate() 111 | except OSError as err: 112 | LOGGER.error('ses_set_snic_nickname: %s', err) 113 | 114 | for line in stderr.decode("utf-8").splitlines(): 115 | LOGGER.debug('ses_set_snic_nickname: sg_ses(stderr): %s', line) 116 | 117 | for line in stdout.decode("utf-8").splitlines(): 118 | LOGGER.debug('ses_set_snic_nickname: sg_ses: %s', line) 119 | 120 | def _ses_get_ed_line(sg_name): 121 | """Helper function to get element descriptor associated lines.""" 122 | cmdargs = ['sg_ses', '--page=ed', '--join', '/dev/' + sg_name] 123 | LOGGER.debug('ses_get_ed_metrics: executing: %s', cmdargs) 124 | stdout, stderr = subprocess.Popen(cmdargs, 125 | stdout=subprocess.PIPE, 126 | stderr=subprocess.PIPE).communicate() 127 | 128 | for line in stderr.decode("utf-8", errors='backslashreplace').splitlines(): 129 | LOGGER.debug('ses_get_ed_metrics: sg_ses(stderr): %s', line) 130 | 131 | element_type = None 132 | descriptor = None 133 | 134 | for line in stdout.decode("utf-8", errors='backslashreplace').splitlines(): 135 | LOGGER.debug('ses_get_ed_metrics: sg_ses: %s', line) 136 | if line and line[0] != ' ' and 'Element type:' in line: 137 | # Voltage 3.30V [6,0] Element type: Voltage sensor 138 | mobj = re.search(r'([^\[]+)\[.*\][\s,]*Element type:\s*(.+)', line) 139 | if mobj: 140 | element_type = mobj.group(2).strip().replace(' ', '_') 141 | descriptor = mobj.group(1).strip() 142 | descriptor = descriptor.replace(' ', '_').replace('.', '_') 143 | else: 144 | yield element_type, descriptor, line.strip() 145 | 146 | 147 | def ses_get_ed_metrics(sg_name): 148 | """ 149 | Return environment metrics as a dictionary from the SES Element 150 | Descriptor page. 151 | """ 152 | for element_type, descriptor, line in _ses_get_ed_line(sg_name): 153 | # Look for environment metrics 154 | mobj = re.search(r'(\w+)[:=]\s*([-+]*[0-9]+(\.[0-9]+)?)\s+(\w+)', line) 155 | if mobj: 156 | key, value, unit = mobj.group(1, 2, 4) 157 | yield dict((('element_type', element_type), 158 | ('descriptor', descriptor), ('key', key), 159 | ('value', value), ('unit', unit))) 160 | 161 | 162 | def ses_get_ed_status(sg_name): 163 | """ 164 | Return different status code as a dictionary from the SES Element 165 | Descriptor page. 166 | """ 167 | for element_type, descriptor, line in _ses_get_ed_line(sg_name): 168 | # Look for status info 169 | mobj = re.search(r'status:\s*(.+)', line) 170 | if mobj: 171 | status = mobj.group(1).replace(' ', '_') 172 | yield dict((('element_type', element_type), 173 | ('descriptor', descriptor), ('status', status))) 174 | -------------------------------------------------------------------------------- /sasutils/smp.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2016 3 | # The Board of Trustees of the Leland Stanford Junior University 4 | # Written by Stephane Thiell 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """SMP Link Layer utils 19 | 20 | Use SMPDiscover to perform a SAS topology discovery for a specified expander 21 | using SMP and retrieve results for each phy. 22 | 23 | Requires smp_utils. 24 | 25 | 26 | >>> from sasutils.smp import SMPDiscover 27 | >>> print SMPDiscover('/dev/bsg/expander-16:0') 28 | phy:0 negot:U addr:0x500605b00aaf8c30 rphy:3 devtype:i iproto:SSP+STP+SMP tproto:None speed:12 29 | phy:1 negot:U addr:0x500605b00aaf8c30 rphy:2 devtype:i iproto:SSP+STP+SMP tproto:None speed:12 30 | phy:2 negot:U addr:0x500605b00aaf8c30 rphy:1 devtype:i iproto:SSP+STP+SMP tproto:None speed:12 31 | phy:3 negot:U addr:0x500605b00aaf8c30 rphy:0 devtype:i iproto:SSP+STP+SMP tproto:None speed:12 32 | phy:12 negot:U addr:0x5001636001a42e3f rphy:13 devtype:exp iproto:None tproto:SMP speed:12 33 | phy:13 negot:U addr:0x5001636001a42e3f rphy:12 devtype:exp iproto:None tproto:SMP speed:12 34 | phy:14 negot:U addr:0x5001636001a42e3f rphy:14 devtype:exp iproto:None tproto:SMP speed:12 35 | phy:15 negot:U addr:0x5001636001a42e3f rphy:15 devtype:exp iproto:None tproto:SMP speed:12 36 | phy:28 negot:U addr:0x500605b00aaf8c30 rphy:7 devtype:i iproto:SSP+STP+SMP tproto:None speed:12 37 | phy:29 negot:U addr:0x500605b00aaf8c30 rphy:6 devtype:i iproto:SSP+STP+SMP tproto:None speed:12 38 | phy:30 negot:U addr:0x500605b00aaf8c30 rphy:5 devtype:i iproto:SSP+STP+SMP tproto:None speed:12 39 | phy:31 negot:U addr:0x500605b00aaf8c30 rphy:4 devtype:i iproto:SSP+STP+SMP tproto:None speed:12 40 | phy:48 negot:D addr:0x50012be000083c7d rphy:0 devtype:V iproto:SMP tproto:SSP speed:12 41 | """ 42 | 43 | import re 44 | from subprocess import check_output 45 | from .sysfs import SysfsObject 46 | 47 | __author__ = 'sthiell@stanford.edu (Stephane Thiell)' 48 | 49 | 50 | class PhyBaseDesc(object): 51 | """SAS Phy description (disabled).""" 52 | 53 | def __init__(self, phy, routing, negot): 54 | """Constructor for PhyBaseDesc.""" 55 | self.phy = int(phy) 56 | self.routing = routing 57 | self.negot = negot 58 | 59 | def __lt__(self, other): 60 | return self.phy < other.phy 61 | 62 | def __repr__(self): 63 | return '<%s.%s "%d">' % (self.__module__, self.__class__.__name__, 64 | self.phy) 65 | 66 | def __str__(self): 67 | return 'phy:{phy} routing:{routing} negot:{negot}'.format( 68 | **self.__dict__) 69 | 70 | 71 | class PhyDesc(PhyBaseDesc): 72 | """SAS Phy description.""" 73 | 74 | def __init__(self, phy, routing, addr, rphy, devtype, iproto, tproto, 75 | speed): 76 | """Constructor for PhyDesc. 77 | 78 | Args: 79 | phy: phy index 80 | routing: routing attribute 81 | addr: SAS addr of phy# 82 | rphy: relative/remote phy# 83 | devtype: exp (expander) or V (virtual) or 'phy' (physical) 84 | iproto: initiator link protos 85 | tproto: target link protos 86 | speed: link speed 87 | """ 88 | PhyBaseDesc.__init__(self, phy, routing, 'attached') 89 | self.rphy = int(rphy) 90 | self.addr = addr if addr.startswith('0x') else '0x' + addr 91 | self.devtype = devtype or 'phy' 92 | self.iproto = iproto 93 | self.tproto = tproto 94 | self.speed = speed 95 | 96 | def __str__(self): 97 | return 'phy:{phy} routing:{routing} addr:{addr} rphy:{rphy} ' \ 98 | 'devtype:{devtype} iproto:{iproto} tproto:{tproto} ' \ 99 | 'speed:{speed}'.format(**self.__dict__) 100 | 101 | 102 | class SMPDiscover(object): 103 | """Performs SMP DISCOVER and gathers results.""" 104 | 105 | def __init__(self, bsg): 106 | """Constructor for SMPDiscover.""" 107 | if isinstance(bsg, SysfsObject): 108 | bsg = bsg.name 109 | self.bsg = bsg if bsg.startswith('/') else '/dev/bsg/' + bsg 110 | self._attached_phys = {} 111 | self._detached_phys = {} 112 | 113 | output = check_output(('smp_discover', self.bsg)) 114 | 115 | # phy 12:U:attached:[5001636001a42e3f:13 exp t(SMP)] 12 Gbps 116 | # phy 28:U:attached:[500605b00ab06f40:07 i(SSP+STP+SMP)] 12 Gbps 117 | # phy 48:D:attached:[50012be000083c7d:00 V i(SMP) t(SSP)] 12 Gbps 118 | pattern = r'^\s*phy\s+(\d+):([A-Z]):attached:\[(\w+):(\d+)\s+' \ 119 | r'(?:(\w+)\s+)*' \ 120 | r'[i]*(?:\(([a-zA-Z+]+)\))*\s*[t]*(?:\(([a-zA-Z+]+)\))*\]' \ 121 | r'\s+(\d+)\s+Gbps' 122 | 123 | for mobj in re.finditer(pattern, output, flags=re.MULTILINE): 124 | self._attached_phys[int(mobj.group(1))] = PhyDesc(*mobj.groups()) 125 | 126 | # other detached phys 127 | pattern = r'^\s*phy\s+(\d+):([A-Z]):([\w\s]+)$' 128 | 129 | for mobj in re.finditer(pattern, output, flags=re.MULTILINE): 130 | self._detached_phys[int(mobj.group(1))] = PhyBaseDesc( 131 | *mobj.groups()) 132 | 133 | def __repr__(self): 134 | return '<%s.%s "%s">' % (self.__module__, self.__class__.__name__, 135 | self.bsg) 136 | 137 | def __str__(self): 138 | return '\n'.join(str(phydesc) for phydesc in self) 139 | 140 | def __iter__(self): 141 | """Iterates through each phy description.""" 142 | return iter(sorted(self._attached_phys.values())) 143 | 144 | def iterdetached(self): 145 | """Iterates through each detached phy description.""" 146 | return iter(sorted(self._detached_phys.values())) 147 | -------------------------------------------------------------------------------- /sasutils/sysfs.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2016, 2017 3 | # The Board of Trustees of the Leland Stanford Junior University 4 | # Written by Stephane Thiell 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | 19 | try: 20 | from collections.abc import MutableMapping 21 | except ImportError: 22 | from collections import MutableMapping 23 | import json 24 | import glob 25 | from os import access, listdir, readlink, R_OK 26 | from os.path import basename, isdir, isfile, join, realpath 27 | import re 28 | 29 | SYSFS_ROOT = '/sys' 30 | 31 | # Some VPDs contain weird characters... 32 | def sanitize_sysfs_value(value): 33 | try: 34 | value2 = value.strip('\x00') 35 | except TypeError: 36 | try: 37 | value2 = value.strip(b'\x00').decode('ascii', errors='replace') 38 | except Exception: 39 | value2 = str(value).strip('\x00') 40 | return value2.encode('ascii', errors='replace').decode() 41 | 42 | 43 | class SysfsNode(object): 44 | def __init__(self, path=None): 45 | if path is None: 46 | self.path = SYSFS_ROOT 47 | else: 48 | self.path = path 49 | 50 | def __repr__(self): 51 | return '' % self.path 52 | 53 | def __str__(self): 54 | return basename(self.path) 55 | 56 | def __eq__(self, other): 57 | return realpath(self.path) == realpath(other.path) 58 | 59 | def __hash__(self): 60 | return hash(realpath(self.path)) 61 | 62 | def __len__(self): 63 | return len([self.__class__(join(self.path, name)) 64 | for name in listdir(self.path)]) 65 | 66 | def __iter__(self): 67 | return iter(self.__class__(join(self.path, name)) 68 | for name in listdir(self.path)) 69 | 70 | def iterglob(self, pathname, is_dir=True): 71 | for path in glob.glob(join(self.path, pathname)): 72 | if isfile(path): 73 | yield basename(path) 74 | elif is_dir and isdir(path): 75 | yield self.__class__(path) 76 | 77 | def glob(self, pathname, is_dir=True): 78 | return list(self.iterglob(pathname, is_dir)) 79 | 80 | def node(self, pathm, default=None): 81 | 82 | # regex pre-processing for advanced matching 83 | funcname = 'fullmatch' 84 | if hasattr(pathm, funcname) and callable(getattr(pathm, funcname)): 85 | for name in listdir(self.path): 86 | if pathm.fullmatch(name): 87 | pathm = name 88 | break 89 | if not isinstance(pathm, str): 90 | raise KeyError(join(self.path, getattr(pathm, 'pattern', '?'))) 91 | 92 | glob_res = list(self.iterglob(pathm)) 93 | try: 94 | return glob_res[0] 95 | except IndexError: 96 | if default is not None: 97 | return default 98 | # print meaningfull error 99 | raise KeyError(join(self.path, pathm)) 100 | 101 | def iterget(self, pathname, ignore_errors, absolute=False): 102 | if absolute: 103 | path = pathname 104 | else: 105 | path = join(self.path, pathname) 106 | for path in glob.glob(path): 107 | if isfile(path) and access(path, R_OK): 108 | try: 109 | with open(path, 'rb') as fp: 110 | data = fp.read() 111 | try: 112 | data = data.decode("utf-8").strip() 113 | except UnicodeDecodeError: 114 | pass 115 | yield data 116 | except IOError as exc: 117 | if not ignore_errors: 118 | yield str(exc) 119 | 120 | def get(self, pathname, default=None, ignore_errors=False, printable=True, 121 | absolute=False): 122 | """get content of a sysfs file""" 123 | if absolute: 124 | path = pathname 125 | else: 126 | path = join(self.path, pathname) 127 | glob_res = list(self.iterget(path, ignore_errors, absolute=True)) 128 | try: 129 | result = glob_res[0] 130 | except IndexError: 131 | if not ignore_errors: 132 | raise KeyError('Not found: %s' % path) 133 | result = default 134 | 135 | return sanitize_sysfs_value(result) 136 | 137 | def readlink(self, pathname, default=None, absolute=False): 138 | if absolute: 139 | path = pathname 140 | else: 141 | path = join(self.path, pathname) 142 | try: 143 | return readlink(path) 144 | except OSError: 145 | if default is not None: 146 | return default 147 | raise 148 | 149 | def put(self, pathname, value, ignore_errors=False, absolute=False): 150 | """write content of a sysfs file entry""" 151 | found = False 152 | if absolute: 153 | path = pathname 154 | else: 155 | path = join(self.path, pathname) 156 | for path in glob.glob(path): 157 | try: 158 | found = True 159 | with open(path, 'w') as fp: 160 | fp.write(str(value)) 161 | except IOError: 162 | if not ignore_errors: 163 | raise 164 | if not found and not ignore_errors: 165 | raise KeyError('Not found: %s' % path) 166 | 167 | 168 | # For testing 169 | SYSFSNODE_CLASS = SysfsNode 170 | 171 | sysfs = SYSFSNODE_CLASS() 172 | 173 | 174 | class SysfsAttributes(MutableMapping): 175 | """SysfsObject attributes with dot.notation access""" 176 | 177 | def __init__(self): 178 | self.values = {} 179 | self.paths = {} 180 | 181 | def add_path(self, attr, path): 182 | self.paths[attr] = path 183 | 184 | def load(self): 185 | for path in self.paths: 186 | loaded = self[path] 187 | 188 | # The next five methods are requirements of the ABC. 189 | 190 | def __setitem__(self, key, value): 191 | self.values[key] = sanitize_sysfs_value(value) 192 | 193 | def get(self, key, default=None): 194 | if not self.values.__contains__(key): 195 | try: 196 | self.values[key] = sysfs.get(self.paths[key], absolute=True) 197 | except KeyError: 198 | if default is not None: 199 | return default 200 | else: 201 | raise AttributeError("%r object has no attribute %r" % 202 | (self.__class__.__name__, key)) 203 | 204 | return self.values[key] 205 | 206 | def __getitem__(self, key): 207 | return self.get(key) 208 | 209 | def __delitem__(self, key): 210 | if key in self.values: 211 | del self.values[key] 212 | del self.paths[key] 213 | 214 | def __iter__(self): 215 | return iter(self.paths) 216 | 217 | def __len__(self): 218 | return len(self.paths) 219 | 220 | __getattr__ = __getitem__ 221 | 222 | 223 | class SysfsObject(object): 224 | def __init__(self, sysfsnode): 225 | self.sysfsnode = sysfsnode 226 | self.name = str(sysfsnode) 227 | self.attrs = SysfsAttributes() 228 | self.classname = self.__class__.__name__ 229 | if type(sysfsnode) is str: 230 | assert len(sysfsnode) > 0 231 | attrs = self.sysfsnode.glob('*', is_dir=False) 232 | for attr in attrs: 233 | self.attrs.add_path(attr, join(self.sysfsnode.path, attr)) 234 | 235 | def json_serialize(self): 236 | """May be overridden to change json serialization, eg. to avoid 237 | circular reference issues.""" 238 | return self.__dict__ 239 | 240 | def to_json(self): 241 | 242 | def json_default(o): 243 | if hasattr(o, 'json_serialize'): 244 | return o.json_serialize() 245 | return o.__dict__ 246 | 247 | return json.dumps(self, default=json_default, sort_keys=True, indent=4) 248 | 249 | def __eq__(self, other): 250 | return self.sysfsnode == other.sysfsnode 251 | 252 | def __hash__(self): 253 | return hash(self.sysfsnode) 254 | 255 | def __repr__(self): 256 | return '<%s.%s %r>' % (self.__module__, self.__class__.__name__, 257 | self.sysfsnode.path) 258 | 259 | __str__ = __repr__ 260 | 261 | 262 | class SysfsDevice(SysfsObject): 263 | def __init__(self, device, subsys, sysfsdev_pattern='*[0-9]'): 264 | # only consider end_device-20:2:57, 20:0:119:0, host19 265 | SysfsObject.__init__(self, device.node(subsys).node(sysfsdev_pattern)) 266 | self.device = device 267 | -------------------------------------------------------------------------------- /sasutils/vpd.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2016, 2023 3 | # The Board of Trustees of the Leland Stanford Junior University 4 | # Written by Stephane Thiell 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | # Inspired from decode_dev_ids() in sg3_utils/src/sg_vpd.c 19 | 20 | import os 21 | from struct import unpack_from 22 | import subprocess 23 | 24 | __author__ = 'sthiell@stanford.edu (Stephane Thiell)' 25 | 26 | 27 | def vpd_decode_pg83_lu(pagebuf): 28 | """ 29 | Get the addressed logical unit address from the device identification 30 | VPD page buffer provided (eg. content of vpd_pg83 in sysfs). 31 | """ 32 | VPD_ASSOC_LU = 0 33 | sz = len(pagebuf) 34 | offset = 4 35 | d, = unpack_from('B', pagebuf, offset + 2) 36 | assert d == 0, 'skip_1st_iter not implemented' 37 | while True: 38 | code_set, = unpack_from('B', pagebuf, offset) 39 | d, = unpack_from('B', pagebuf, offset + 1) 40 | design_type = (d & 0xf) 41 | assoc = (d >> 4) & 0x3 42 | d, = unpack_from('B', pagebuf, offset + 3) 43 | next_offset = offset + (d & 0xf) + 4 44 | if next_offset > sz: 45 | break 46 | 47 | # We only support designator_type=3 (NAA) for now. 48 | if design_type == 3 and assoc == VPD_ASSOC_LU: 49 | d, = unpack_from('B', pagebuf, offset + 3) 50 | if d in (8, 16): 51 | return '0x' + ''.join("%02x" % i for i in \ 52 | unpack_from('B' * d, pagebuf, offset + 4)) 53 | offset = next_offset 54 | 55 | 56 | # 57 | # Support for RHEL/CentOS 6 (missing sysfs vpd_pg80 and vpd_pg83) 58 | # 59 | def vpd_get_page80_sn(blkdev): 60 | """ 61 | Get page 0x80 Serial Number using external command. 62 | """ 63 | env = os.environ.copy() 64 | env["PATH"] = "/lib/udev:" + env["PATH"] 65 | cmdargs = ['scsi_id', '--page=0x80', '--whitelisted', 66 | '--device=/dev/' + blkdev] 67 | output = subprocess.Popen(cmdargs, stdout=subprocess.PIPE, env=env).communicate()[0] 68 | return output.decode("utf-8", errors='backslashreplace').rstrip().split()[-1] 69 | 70 | 71 | def vpd_get_page83_lu(blkdev): 72 | """ 73 | Get page 0x83 Logical Unit using external command. 74 | """ 75 | env = os.environ.copy() 76 | env["PATH"] = "/lib/udev:" + env["PATH"] 77 | cmdargs = ['scsi_id', '--page=0x83', '--whitelisted', 78 | '--device=/dev/' + blkdev] 79 | output = subprocess.Popen(cmdargs, stdout=subprocess.PIPE, env=env).communicate()[0] 80 | return output.decode("utf-8", errors='backslashreplace').rstrip() 81 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2016, 2017, 2018, 2019, 2021, 2022, 2023, 2024 3 | # The Board of Trustees of the Leland Stanford Junior University 4 | # Written by Stephane Thiell 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | from setuptools import setup, find_packages 19 | 20 | VERSION = '0.6.1' 21 | 22 | setup(name='sasutils', 23 | version=VERSION, 24 | packages=find_packages(), 25 | author='Stephane Thiell', 26 | author_email='sthiell@stanford.edu', 27 | license='Apache Software License', 28 | url='https://github.com/stanford-rc/sasutils', 29 | platforms=['GNU/Linux'], 30 | keywords=['SAS', 'SCSI', 'storage'], 31 | description='Serial Attached SCSI (SAS) Linux utilities', 32 | long_description=open('README.rst').read(), 33 | classifiers=[ 34 | 'Development Status :: 5 - Production/Stable', 35 | 'Environment :: Console', 36 | 'Intended Audience :: System Administrators', 37 | 'License :: OSI Approved :: Apache Software License', 38 | 'Operating System :: POSIX :: Linux', 39 | 'Programming Language :: Python', 40 | 'Programming Language :: Python :: 3', 41 | 'Topic :: System :: Systems Administration' 42 | ], 43 | entry_points={ 44 | 'console_scripts': [ 45 | 'sas_counters=sasutils.cli.sas_counters:main', 46 | 'sas_devices=sasutils.cli.sas_devices:main', 47 | 'sas_discover=sasutils.cli.sas_discover:main', 48 | 'sas_mpath_snic_alias=sasutils.cli.sas_mpath_snic_alias:main', 49 | 'sas_sd_snic_alias=sasutils.cli.sas_sd_snic_alias:main', 50 | 'sas_st_snic_alias=sasutils.cli.sas_st_snic_alias:main', 51 | 'ses_report=sasutils.cli.ses_report:main' 52 | ], 53 | }, 54 | ) 55 | -------------------------------------------------------------------------------- /tests/gen_sysfs_testenv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Copyright (C) 2016 4 | # The Board of Trustees of the Leland Stanford Junior University 5 | # Written by Stephane Thiell 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | 20 | from __future__ import print_function 21 | 22 | import glob 23 | import nose 24 | import os 25 | from os.path import isdir, isfile, join 26 | import sys 27 | 28 | # use zipfile as it is safer than tarfile with sysfs 29 | # see http://bugs.python.org/issue10760 30 | from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED 31 | 32 | import sasutils.sysfs 33 | 34 | 35 | def zipfile_add(path): 36 | try: 37 | # print(path, file=sys.stderr) 38 | zipfile.write(path) 39 | except OSError as err: 40 | print('zipfile OSError %s for %s' % (err, path), file=sys.stderr) 41 | 42 | 43 | class SysfsNodeDump(sasutils.sysfs.SysfsNode): 44 | def __iter__(self): 45 | for name in os.listdir(self.path): 46 | path = join(self.path, name) 47 | zipfile_add(path) 48 | yield self.__class__(path) 49 | 50 | def iterglob(self, pathname, is_dir=True): 51 | for path in glob.glob(join(self.path, pathname)): 52 | if isfile(path): 53 | zipfile_add(path) 54 | elif is_dir and isdir(path): 55 | zipfile_add(path) 56 | return super(SysfsNodeDump, self).iterglob(pathname, is_dir) 57 | 58 | def get(self, pathname, default=None, ignore_errors=False, printable=True, 59 | absolute=False): 60 | if absolute: 61 | path = pathname 62 | else: 63 | path = join(self.path, pathname) 64 | zipfile.write(path) 65 | return super(SysfsNodeDump, self).get(pathname, default, ignore_errors, 66 | printable) 67 | 68 | def readlink(self, pathname, default=None, absolute=False): 69 | if absolute: 70 | path = pathname 71 | else: 72 | path = join(self.path, pathname) 73 | print(path) 74 | zipfile_add(path) 75 | return super(SysfsNodeDump, self).readlink(pathname, default) 76 | 77 | 78 | sasutils.sysfs.SYSFSNODE_CLASS = SysfsNodeDump 79 | sysfs = sasutils.sysfs.sysfs = SysfsNodeDump() 80 | 81 | if __name__ == '__main__': 82 | zipfile = ZipFile('sysfs_testenv.zip', 'w') 83 | os.environ['gen_sysfs_testenv'] = 'TRUE' 84 | nose.run(argv=[sys.argv[0], 'sysfs', '-v']) 85 | zipfile.close() 86 | -------------------------------------------------------------------------------- /tests/sysfs.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import dirname, join 3 | from unittest import TestCase 4 | 5 | import sasutils.sysfs 6 | from sasutils.sysfs import SysfsNode, SysfsObject, SysfsDevice 7 | 8 | if 'gen_sysfs_testenv' not in os.environ: 9 | sasutils.sysfs.SYSFS_ROOT = 'sys' 10 | 11 | sysfsroot = sasutils.sysfs.SYSFS_ROOT 12 | 13 | sysfs = sasutils.sysfs.SYSFSNODE_CLASS() 14 | 15 | 16 | class SysfsNodeTest(TestCase): 17 | """Test cases for SysfsNode""" 18 | 19 | def setUp(self): 20 | self.sd = str(sysfs.node('block').glob('sd*')[0]) 21 | 22 | def test_default(self): 23 | self.assertEqual(sysfs.path, sysfsroot) 24 | self.assertEqual(str(sysfs), 'sys') 25 | 26 | def test_iter(self): 27 | block = sysfs.node('block') 28 | for i in block: 29 | self.assertTrue( 30 | isinstance(i, SysfsNode) or isinstance(i, (str, bytes))) 31 | 32 | def test_glob(self): 33 | block = sysfs.node('block') 34 | self.assertEqual(block.path, join(sysfsroot, 'block')) 35 | sdlist = block.glob('sd*') 36 | self.assertTrue(isinstance(sdlist[0], SysfsNode)) 37 | self.assertEqual(dirname(sdlist[0].path), join(sysfsroot, 'block')) 38 | 39 | """ 40 | def test_readlink(self): 41 | blkdevnode = sysfs.node('block').node(self.sd) 42 | self.assertTrue(blkdevnode.readlink('device').startswith('../')) 43 | self.assertRaises(OSError, sysfs.node('block').readlink, 'dummyentry') 44 | """ 45 | 46 | def test_globfile(self): 47 | blkdevnode = sysfs.node('block').node(self.sd) 48 | self.assertTrue(blkdevnode.glob('remov*')[0], 'removable') 49 | 50 | def test_node(self): 51 | blkdevnode = sysfs.node('block').node(self.sd) 52 | self.assertRaises(KeyError, blkdevnode.node, 'dummyentry') 53 | self.assertEqual(blkdevnode.node('dummyentry', default='foo'), 'foo') 54 | 55 | def test_get(self): 56 | blkdevnode = sysfs.node('block').node(self.sd) 57 | self.assertIn(blkdevnode.get('removable'), ('0', '1')) 58 | self.assertRaises(KeyError, blkdevnode.get, 'dummyentry') 59 | self.assertEqual(blkdevnode.get('dummyentry', ignore_errors=True), None) 60 | 61 | 62 | class SysfsObjectTest(TestCase): 63 | """Test cases for SysfsObject""" 64 | 65 | def setUp(self): 66 | self.sd = str(sysfs.node('block').glob('sd*')[0]) 67 | 68 | def test_create(self): 69 | sysfsobj = SysfsObject(sysfs.node('block').node(self.sd)) 70 | self.assertEqual(sysfsobj.sysfsnode.path, 71 | join(sysfsroot, 'block/%s' % self.sd)) 72 | self.assertTrue(sysfsobj.attrs.size > 0) 73 | 74 | 75 | class SysfsDeviceTest(TestCase): 76 | """Test cases for SysfsDevice""" 77 | 78 | def setUp(self): 79 | self.sd = str(sysfs.node('block').glob('sd*')[0]) 80 | 81 | def test_create(self): 82 | dev = sysfs.node('block').node(self.sd).node('device') 83 | sysfsdev = SysfsDevice(dev, subsys='block', sysfsdev_pattern='sd*') 84 | self.assertEqual(sysfsdev.device.path, 85 | join(sysfsroot, 'block/%s/device' % self.sd)) 86 | self.assertEqual(sysfsdev.sysfsnode.path, 87 | join(sysfsroot, 88 | 'block/%s/device/block/%s' % (self.sd, self.sd))) 89 | --------------------------------------------------------------------------------