├── .github └── workflows │ ├── build.yml │ └── test.yml ├── .gitignore ├── .travis.yml ├── AUTHORS ├── LICENSE ├── MANIFEST.in ├── NEWS ├── README.rst ├── docs ├── Makefile ├── conf.py └── index.rst ├── pylint.cfg ├── pymdstat ├── __init__.py └── pymdstat.py ├── setup.py ├── tests ├── mdstat.01 ├── mdstat.02 ├── mdstat.03 ├── mdstat.04 ├── mdstat.05 ├── mdstat.06 ├── mdstat.07 ├── mdstat.08 ├── mdstat.09 ├── mdstat.10 ├── mdstat.11 ├── mdstat.12 ├── mdstat.13 ├── mdstat.14 ├── mdstat.15 └── mdstat.16 ├── tox.ini ├── uninstall.sh └── unitest.py /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This pipeline aims at building PyMmStat for the following targets: 2 | # - Pypi 3 | 4 | name: CI 5 | 6 | env: 7 | DEFAULT_DOCKER_IMAGE: nicolargo/glances 8 | NODE_ENV: ${{ (contains('refs/heads/master', github.ref) || startsWith(github.ref, 'refs/tags/v')) && 'prod' || 'dev' }} 9 | PUSH_BRANCH: ${{ 'refs/heads/develop' == github.ref || 'refs/heads/master' == github.ref || startsWith(github.ref, 'refs/tags/v') }} 10 | # linux/arm/v6 support following issue #2120 11 | DOCKER_PLATFORMS: linux/amd64,linux/arm/v7,linux/arm64,linux/386 12 | 13 | on: 14 | push: 15 | branches: [ master ] 16 | tags: 17 | - v* 18 | 19 | jobs: 20 | 21 | pypi: 22 | runs-on: ubuntu-latest 23 | steps: 24 | 25 | - uses: actions/checkout@v3 26 | 27 | - name: Install pip install build tools 28 | run: >- 29 | python -m 30 | pip install 31 | build 32 | --user 33 | 34 | - name: Build a binary wheel and a source tarball 35 | run: >- 36 | python -m 37 | build 38 | --sdist 39 | --wheel 40 | --outdir dist/ 41 | 42 | - name: Publish distribution package to PyPI 43 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 44 | uses: pypa/gh-action-pypi-publish@master 45 | with: 46 | user: __token__ 47 | password: ${{ secrets.PYPI_API_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Run unitary test 2 | 3 | name: test 4 | 5 | on: [push] 6 | 7 | jobs: 8 | test: 9 | 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: ["2.7", "3.7", "3.8", "3.9", "3.10", "3.11"] 14 | 15 | steps: 16 | 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install flake8 28 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 29 | 30 | - name: Lint with flake8 31 | run: | 32 | # Stop the build if there are Python syntax errors or undefined names 33 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=.git,./docs 34 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 35 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --exclude=.git,./docs 36 | 37 | - name: Static type check 38 | run: | 39 | if [ "${{ matrix.python-version }}" == "2.7" ]; 40 | then 41 | echo "Skipping static type check for Python 2.7"; 42 | else 43 | echo "Skipping static type check for the moment, too much error..."; 44 | # pip install pyright 45 | # pyright glances 46 | fi 47 | 48 | - name: Unitary tests 49 | run: | 50 | python ./unitest.py 51 | 52 | - name: Security issues with Bandit 53 | uses: jpetrucciani/bandit-check@master 54 | with: 55 | path: '-r --exit-zero --skip B104 ./pymdstat/' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "pypy" 6 | install: 7 | - pip install coveralls 8 | script: 9 | - python setup.py install 10 | - coverage run --source=pymdstat unitest.py 11 | after_success: 12 | - coveralls 13 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | ========== 2 | Developers 3 | ========== 4 | 5 | Nicolas Hennion (aka) Nicolargo 6 | http://blog.nicolargo.com 7 | https://twitter.com/nicolargo 8 | nicolashennion@gmail.com 9 | PGP Fingerprint: 835F C447 3BCD 60E9 9200 2778 ABA4 D1AB 9731 6A3C 10 | PGP Public key: gpg --keyserver pgp.mit.edu --recv-keys 0xaba4d1ab97316a3c 11 | 12 | ========= 13 | Packagers 14 | ========= 15 | 16 | Philip Lacroix (philnx@posteo.de) for the Slackware (SlackBuild) package 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Nicolas Hennion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include README.rst 4 | include NEWS 5 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | v0.4.2 - 31/12/2014 2 | =================== 3 | 4 | * Do not send a sys.exit if /proc/mdstat doesn't exist. Just return the error. 5 | 6 | v0.4.1 - 24/12/2014 7 | ================= 8 | * Handle I/O error if /proc/mdstat doesn't exist (issue #2) 9 | 10 | v0.4 - 24/12/2014 11 | ================= 12 | 13 | * Misc tweaks - Pep (issue #3) 14 | 15 | v0.3 - 23/12/2014 16 | ================= 17 | 18 | * Python 3 compatibility issue (issue #1) 19 | 20 | v0.2 - 22/12/2014 21 | ================= 22 | 23 | * Manage inactive RAID arrays 24 | 25 | v0.1 - 20/12/2014 26 | ================= 27 | 28 | * First public version 29 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | PyMDstat 3 | ======== 4 | 5 | .. image:: https://scrutinizer-ci.com/g/nicolargo/pymdstat/badges/build.png?b=master 6 | :target: https://scrutinizer-ci.com/g/nicolargo/pymdstat/?branch=master 7 | .. image:: https://scrutinizer-ci.com/g/nicolargo/pymdstat/badges/quality-score.png?b=master 8 | :target: https://scrutinizer-ci.com/g/nicolargo/pymdstat/?branch=master 9 | .. image:: https://img.shields.io/pypi/v/pymdstat.svg 10 | :target: https://pypi.python.org/pypi/pymdstat/ 11 | :alt: Latest Version 12 | 13 | 14 | A pythonic library to parse Linux /proc/mdstat file. 15 | 16 | .. code-block:: python 17 | 18 | >>> from pymdstat import MdStat 19 | 20 | >>> mds = MdStat() # Read the /proc/mdstat file 21 | 22 | >>> mds 23 | Personalities : [raid1] [raid6] [raid5] [raid4] 24 | md1 : active raid1 sdb2[1] sda2[0] 25 | 136448 blocks [2/2] [UU] 26 | 27 | md2 : active raid1 sdb3[1] sda3[0] 28 | 129596288 blocks [2/2] [UU] 29 | 30 | md3 : active raid5 sdl1[9] sdk1[8] sdj1[7] sdi1[6] sdh1[5] sdg1[4] sdf1[3] sde1[2] sdd1[1] sdc1[0] 31 | 1318680576 blocks level 5, 1024k chunk, algorithm 2 [10/10] [UUUUUUUUUU] 32 | 33 | md0 : active raid1 sdb1[1] sda1[0] 34 | 16787776 blocks [2/2] [UU] 35 | 36 | unused devices: 37 | 38 | >>> mds.get_stats() 39 | {'arrays': {'md0': {'available': '2', 40 | 'components': {'sda1': '0', 'sdb1': '1'}, 41 | 'config': 'UU', 42 | 'status': 'active', 43 | 'type': 'raid1', 44 | 'used': '2'}, 45 | 'md1': {'available': '2', 46 | 'components': {'sda2': '0', 'sdb2': '1'}, 47 | 'config': 'UU', 48 | 'status': 'active', 49 | 'type': 'raid1', 50 | 'used': '2'}, 51 | 'md2': {'available': '2', 52 | 'components': {'sda3': '0', 'sdb3': '1'}, 53 | 'config': 'UU', 54 | 'status': 'active', 55 | 'type': 'raid1', 56 | 'used': '2'}, 57 | 'md3': {'available': '10', 58 | 'components': {'sdc1': '0', 59 | 'sdd1': '1', 60 | 'sde1': '2', 61 | 'sdf1': '3', 62 | 'sdg1': '4', 63 | 'sdh1': '5', 64 | 'sdi1': '6', 65 | 'sdj1': '7', 66 | 'sdk1': '8', 67 | 'sdl1': '9'}, 68 | 'config': 'UUUUUUUUUU', 69 | 'status': 'active', 70 | 'type': 'raid5', 71 | 'used': '10'}}, 72 | 'personalities': ['raid1', 'raid6', 'raid5', 'raid4']} 73 | 74 | >>> mds.personalities() 75 | ['raid1', 'raid6', 'raid5', 'raid4'] 76 | 77 | >>> mds.arrays() 78 | ['md2', 'md3', 'md0', 'md1'] 79 | 80 | >>> mds.type('md3') 81 | 'raid6' 82 | 83 | >>> mds.status('md3') 84 | 'active' 85 | 86 | >>> mds.components('md3') 87 | ['sdk1', 'sdj1', 'sde1', 'sdl1', 'sdg1', 'sdf1', 'sdh1', 'sdc1', 'sdd1', 'sdi1'] 88 | 89 | >>> mds.available('md3') 90 | 10 91 | 92 | >>> mds.used('md3') 93 | 10 94 | 95 | >>> mds.config('md3') 96 | 'UUUUUUUUUU' 97 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pymdstat.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pymdstat.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pymdstat" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pymdstat" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pymdstat documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Dec 20 16:43:07 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | ] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates'] 37 | 38 | # The suffix of source filenames. 39 | source_suffix = '.rst' 40 | 41 | # The encoding of source files. 42 | #source_encoding = 'utf-8-sig' 43 | 44 | # The master toctree document. 45 | master_doc = 'index' 46 | 47 | # General information about the project. 48 | project = u'pymdstat' 49 | copyright = u'2014, Nicolargo' 50 | 51 | # The version info for the project you're documenting, acts as replacement for 52 | # |version| and |release|, also used in various other places throughout the 53 | # built documents. 54 | # 55 | # The short X.Y version. 56 | version = '0.1' 57 | # The full version, including alpha/beta/rc tags. 58 | release = '0.1' 59 | 60 | # The language for content autogenerated by Sphinx. Refer to documentation 61 | # for a list of supported languages. 62 | #language = None 63 | 64 | # There are two options for replacing |today|: either, you set today to some 65 | # non-false value, then it is used: 66 | #today = '' 67 | # Else, today_fmt is used as the format for a strftime call. 68 | #today_fmt = '%B %d, %Y' 69 | 70 | # List of patterns, relative to source directory, that match files and 71 | # directories to ignore when looking for source files. 72 | exclude_patterns = ['_build'] 73 | 74 | # The reST default role (used for this markup: `text`) to use for all 75 | # documents. 76 | #default_role = None 77 | 78 | # If true, '()' will be appended to :func: etc. cross-reference text. 79 | #add_function_parentheses = True 80 | 81 | # If true, the current module name will be prepended to all description 82 | # unit titles (such as .. function::). 83 | #add_module_names = True 84 | 85 | # If true, sectionauthor and moduleauthor directives will be shown in the 86 | # output. They are ignored by default. 87 | #show_authors = False 88 | 89 | # The name of the Pygments (syntax highlighting) style to use. 90 | pygments_style = 'sphinx' 91 | 92 | # A list of ignored prefixes for module index sorting. 93 | #modindex_common_prefix = [] 94 | 95 | # If true, keep warnings as "system message" paragraphs in the built documents. 96 | #keep_warnings = False 97 | 98 | 99 | # -- Options for HTML output ---------------------------------------------- 100 | 101 | # The theme to use for HTML and HTML Help pages. See the documentation for 102 | # a list of builtin themes. 103 | html_theme = 'default' 104 | 105 | # Theme options are theme-specific and customize the look and feel of a theme 106 | # further. For a list of options available for each theme, see the 107 | # documentation. 108 | #html_theme_options = {} 109 | 110 | # Add any paths that contain custom themes here, relative to this directory. 111 | #html_theme_path = [] 112 | 113 | # The name for this set of Sphinx documents. If None, it defaults to 114 | # " v documentation". 115 | #html_title = None 116 | 117 | # A shorter title for the navigation bar. Default is the same as html_title. 118 | #html_short_title = None 119 | 120 | # The name of an image file (relative to this directory) to place at the top 121 | # of the sidebar. 122 | #html_logo = None 123 | 124 | # The name of an image file (within the static path) to use as favicon of the 125 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 126 | # pixels large. 127 | #html_favicon = None 128 | 129 | # Add any paths that contain custom static files (such as style sheets) here, 130 | # relative to this directory. They are copied after the builtin static files, 131 | # so a file named "default.css" will overwrite the builtin "default.css". 132 | html_static_path = ['_static'] 133 | 134 | # Add any extra paths that contain custom files (such as robots.txt or 135 | # .htaccess) here, relative to this directory. These files are copied 136 | # directly to the root of the documentation. 137 | #html_extra_path = [] 138 | 139 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 140 | # using the given strftime format. 141 | #html_last_updated_fmt = '%b %d, %Y' 142 | 143 | # If true, SmartyPants will be used to convert quotes and dashes to 144 | # typographically correct entities. 145 | #html_use_smartypants = True 146 | 147 | # Custom sidebar templates, maps document names to template names. 148 | #html_sidebars = {} 149 | 150 | # Additional templates that should be rendered to pages, maps page names to 151 | # template names. 152 | #html_additional_pages = {} 153 | 154 | # If false, no module index is generated. 155 | #html_domain_indices = True 156 | 157 | # If false, no index is generated. 158 | #html_use_index = True 159 | 160 | # If true, the index is split into individual pages for each letter. 161 | #html_split_index = False 162 | 163 | # If true, links to the reST sources are added to the pages. 164 | #html_show_sourcelink = True 165 | 166 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 167 | #html_show_sphinx = True 168 | 169 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 170 | #html_show_copyright = True 171 | 172 | # If true, an OpenSearch description file will be output, and all pages will 173 | # contain a tag referring to it. The value of this option must be the 174 | # base URL from which the finished HTML is served. 175 | #html_use_opensearch = '' 176 | 177 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 178 | #html_file_suffix = None 179 | 180 | # Output file base name for HTML help builder. 181 | htmlhelp_basename = 'pymdstatdoc' 182 | 183 | 184 | # -- Options for LaTeX output --------------------------------------------- 185 | 186 | latex_elements = { 187 | # The paper size ('letterpaper' or 'a4paper'). 188 | #'papersize': 'letterpaper', 189 | 190 | # The font size ('10pt', '11pt' or '12pt'). 191 | #'pointsize': '10pt', 192 | 193 | # Additional stuff for the LaTeX preamble. 194 | #'preamble': '', 195 | } 196 | 197 | # Grouping the document tree into LaTeX files. List of tuples 198 | # (source start file, target name, title, 199 | # author, documentclass [howto, manual, or own class]). 200 | latex_documents = [ 201 | ('index', 'pymdstat.tex', u'pymdstat Documentation', 202 | u'Nicolargo', 'manual'), 203 | ] 204 | 205 | # The name of an image file (relative to this directory) to place at the top of 206 | # the title page. 207 | #latex_logo = None 208 | 209 | # For "manual" documents, if this is true, then toplevel headings are parts, 210 | # not chapters. 211 | #latex_use_parts = False 212 | 213 | # If true, show page references after internal links. 214 | #latex_show_pagerefs = False 215 | 216 | # If true, show URL addresses after external links. 217 | #latex_show_urls = False 218 | 219 | # Documents to append as an appendix to all manuals. 220 | #latex_appendices = [] 221 | 222 | # If false, no module index is generated. 223 | #latex_domain_indices = True 224 | 225 | 226 | # -- Options for manual page output --------------------------------------- 227 | 228 | # One entry per manual page. List of tuples 229 | # (source start file, name, description, authors, manual section). 230 | man_pages = [ 231 | ('index', 'pymdstat', u'pymdstat Documentation', 232 | [u'Nicolargo'], 1) 233 | ] 234 | 235 | # If true, show URL addresses after external links. 236 | #man_show_urls = False 237 | 238 | 239 | # -- Options for Texinfo output ------------------------------------------- 240 | 241 | # Grouping the document tree into Texinfo files. List of tuples 242 | # (source start file, target name, title, author, 243 | # dir menu entry, description, category) 244 | texinfo_documents = [ 245 | ('index', 'pymdstat', u'pymdstat Documentation', 246 | u'Nicolargo', 'pymdstat', 'One line description of project.', 247 | 'Miscellaneous'), 248 | ] 249 | 250 | # Documents to append as an appendix to all manuals. 251 | #texinfo_appendices = [] 252 | 253 | # If false, no module index is generated. 254 | #texinfo_domain_indices = True 255 | 256 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 257 | #texinfo_show_urls = 'footnote' 258 | 259 | # If true, do not generate a @detailmenu in the "Top" node's menu. 260 | #texinfo_no_detailmenu = False 261 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. pymdstat documentation master file, created by 2 | sphinx-quickstart on Sat Dec 20 16:43:07 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to pymdstat's documentation! 7 | ==================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | Programmer's manual 15 | =================== 16 | 17 | :mod:`pymdstat` -- Main package 18 | ******************************* 19 | 20 | .. automodule:: pymdstat 21 | 22 | Indices and tables 23 | ================== 24 | 25 | * :ref:`genindex` 26 | * :ref:`modindex` 27 | * :ref:`search` 28 | 29 | -------------------------------------------------------------------------------- /pylint.cfg: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Profiled execution. 11 | profile=no 12 | 13 | # Add files or directories to the blacklist. They should be base names, not 14 | # paths. 15 | ignore=CVS 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=yes 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins= 23 | 24 | 25 | [MESSAGES CONTROL] 26 | 27 | # Enable the message, report, category or checker with the given id(s). You can 28 | # either give multiple identifier separated by comma (,) or put this option 29 | # multiple time. 30 | #enable= 31 | 32 | # Disable the message, report, category or checker with the given id(s). You 33 | # can either give multiple identifier separated by comma (,) or put this option 34 | # multiple time (only on the command line, not in the configuration file where 35 | # it should appear only once). 36 | disable=C,no-name-in-module 37 | 38 | 39 | [REPORTS] 40 | 41 | # Set the output format. Available formats are text, parseable, colorized, msvs 42 | # (visual studio) and html. You can also give a reporter class, eg 43 | # mypackage.mymodule.MyReporterClass. 44 | output-format=text 45 | 46 | # Include message's id in output 47 | include-ids=no 48 | 49 | # Include symbolic ids of messages in output 50 | symbols=no 51 | 52 | # Put messages in a separate file for each module / package specified on the 53 | # command line instead of printing them on stdout. Reports (if any) will be 54 | # written in a file name "pylint_global.[txt|html]". 55 | files-output=no 56 | 57 | # Tells whether to display a full report or only the messages 58 | reports=yes 59 | 60 | # Python expression which should return a note less than 10 (10 is the highest 61 | # note). You have access to the variables errors warning, statement which 62 | # respectively contain the number of errors / warnings messages and the total 63 | # number of statements analyzed. This is used by the global evaluation report 64 | # (RP0004). 65 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 66 | 67 | # Add a comment according to your evaluation note. This is used by the global 68 | # evaluation report (RP0004). 69 | comment=no 70 | 71 | 72 | [VARIABLES] 73 | 74 | # Tells whether we should check for unused import in __init__ files. 75 | init-import=no 76 | 77 | # A regular expression matching the beginning of the name of dummy variables 78 | # (i.e. not used). 79 | dummy-variables-rgx=_|dummy 80 | 81 | # List of additional names supposed to be defined in builtins. Remember that 82 | # you should avoid to define new builtins when possible. 83 | additional-builtins=_ 84 | 85 | 86 | [TYPECHECK] 87 | 88 | # Tells whether missing members accessed in mixin class should be ignored. A 89 | # mixin class is detected if its name ends with "mixin" (case insensitive). 90 | ignore-mixin-members=yes 91 | 92 | # List of classes names for which member attributes should not be checked 93 | # (useful for classes with attributes dynamically set). 94 | ignored-classes=SQLObject 95 | 96 | # When zope mode is activated, add a predefined set of Zope acquired attributes 97 | # to generated-members. 98 | zope=no 99 | 100 | # List of members which are set dynamically and missed by pylint inference 101 | # system, and so shouldn't trigger E0201 when accessed. Python regular 102 | # expressions are accepted. 103 | generated-members=REQUEST,acl_users,aq_parent 104 | 105 | 106 | [FORMAT] 107 | 108 | # Maximum number of characters on a single line. 109 | max-line-length=80 110 | 111 | # Maximum number of lines in a module 112 | max-module-lines=1000 113 | 114 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 115 | # tab). 116 | indent-string=' ' 117 | 118 | 119 | [MISCELLANEOUS] 120 | 121 | # List of note tags to take in consideration, separated by a comma. 122 | notes=FIXME,XXX,TODO 123 | 124 | 125 | [BASIC] 126 | 127 | # Required attributes for module, separated by a comma 128 | required-attributes= 129 | 130 | # List of builtins function names that should not be used, separated by a comma 131 | bad-functions=map,filter,apply,input 132 | 133 | # Regular expression which should only match correct module names 134 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 135 | 136 | # Regular expression which should only match correct module level names 137 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 138 | 139 | # Regular expression which should only match correct class names 140 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 141 | 142 | # Regular expression which should only match correct function names 143 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 144 | 145 | # Regular expression which should only match correct method names 146 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 147 | 148 | # Regular expression which should only match correct instance attribute names 149 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 150 | 151 | # Regular expression which should only match correct argument names 152 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 153 | 154 | # Regular expression which should only match correct variable names 155 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 156 | 157 | # Regular expression which should only match correct list comprehension / 158 | # generator expression variable names 159 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 160 | 161 | # Good variable names which should always be accepted, separated by a comma 162 | good-names=i,j,k,ex,Run,_ 163 | 164 | # Bad variable names which should always be refused, separated by a comma 165 | bad-names=foo,bar,baz,toto,tutu,tata 166 | 167 | # Regular expression which should only match functions or classes name which do 168 | # not require a docstring 169 | no-docstring-rgx=__.*__ 170 | 171 | 172 | [SIMILARITIES] 173 | 174 | # Minimum lines number of a similarity. 175 | min-similarity-lines=4 176 | 177 | # Ignore comments when computing similarities. 178 | ignore-comments=yes 179 | 180 | # Ignore docstrings when computing similarities. 181 | ignore-docstrings=yes 182 | 183 | # Ignore imports when computing similarities. 184 | ignore-imports=no 185 | 186 | 187 | [IMPORTS] 188 | 189 | # Deprecated modules which should not be used, separated by a comma 190 | deprecated-modules=regsub,string,TERMIOS,Bastion,rexec 191 | 192 | # Create a graph of every (i.e. internal and external) dependencies in the 193 | # given file (report RP0402 must not be disabled) 194 | import-graph= 195 | 196 | # Create a graph of external dependencies in the given file (report RP0402 must 197 | # not be disabled) 198 | ext-import-graph= 199 | 200 | # Create a graph of internal dependencies in the given file (report RP0402 must 201 | # not be disabled) 202 | int-import-graph= 203 | 204 | 205 | [CLASSES] 206 | 207 | # List of interface methods to ignore, separated by a comma. This is used for 208 | # instance to not check methods defines in Zope's Interface base class. 209 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 210 | 211 | # List of method names used to declare (i.e. assign) instance attributes. 212 | defining-attr-methods=__init__,__new__,setUp 213 | 214 | # List of valid names for the first argument in a class method. 215 | valid-classmethod-first-arg=cls 216 | 217 | # List of valid names for the first argument in a metaclass class method. 218 | valid-metaclass-classmethod-first-arg=mcs 219 | 220 | 221 | [DESIGN] 222 | 223 | # Maximum number of arguments for function / method 224 | max-args=5 225 | 226 | # Argument names that match this expression will be ignored. Default to name 227 | # with leading underscore 228 | ignored-argument-names=_.* 229 | 230 | # Maximum number of locals for function / method body 231 | max-locals=15 232 | 233 | # Maximum number of return / yield for function / method body 234 | max-returns=6 235 | 236 | # Maximum number of branch for function / method body 237 | max-branchs=12 238 | 239 | # Maximum number of statements in function / method body 240 | max-statements=50 241 | 242 | # Maximum number of parents for a class (see R0901). 243 | max-parents=7 244 | 245 | # Maximum number of attributes for a class (see R0902). 246 | max-attributes=7 247 | 248 | # Minimum number of public methods for a class (see R0903). 249 | min-public-methods=2 250 | 251 | # Maximum number of public methods for a class (see R0904). 252 | max-public-methods=20 253 | 254 | 255 | [EXCEPTIONS] 256 | 257 | # Exceptions that will emit a warning when being caught. Defaults to 258 | # "Exception" 259 | overgeneral-exceptions=Exception 260 | -------------------------------------------------------------------------------- /pymdstat/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # PyMDstat 5 | # ... 6 | # 7 | # Copyright (C) 2023 Nicolargo 8 | 9 | __appname__ = "PyMDstat" 10 | __version__ = "0.4.3" 11 | __author__ = "Nicolas Hennion " 12 | __licence__ = "MIT" 13 | 14 | __all__ = ['MdStat'] 15 | 16 | from .pymdstat import MdStat 17 | -------------------------------------------------------------------------------- /pymdstat/pymdstat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) 2014 Nicolargo 5 | # License: MIT, see LICENSE for more details. 6 | 7 | import sys 8 | from functools import reduce 9 | from re import split 10 | 11 | 12 | class MdStat(object): 13 | 14 | """Main mdstat class.""" 15 | 16 | def __init__(self, path='/proc/mdstat'): 17 | self.path = path 18 | self.content = '' 19 | 20 | # Stats will be stored in a dict 21 | self.stats = self.load() 22 | 23 | def __str__(self): 24 | """Return the content of the file.""" 25 | return self.content 26 | 27 | def __repr__(self): 28 | """Return the content of the file.""" 29 | return self.content 30 | 31 | def get_path(self): 32 | """Return the mdstat file path.""" 33 | return self.path 34 | 35 | def get_stats(self): 36 | """Return the stats.""" 37 | return self.stats 38 | 39 | def personalities(self): 40 | """Return the personalities (list). 41 | List of all the software RAID levels supported by your md driver 42 | """ 43 | return self.get_stats()['personalities'] 44 | 45 | def arrays(self): 46 | """Return the arrays (list). 47 | List of actual RAID arrays configured on your system 48 | """ 49 | return self.get_stats()['arrays'].keys() 50 | 51 | def type(self, array): 52 | """Return the array's type.""" 53 | return self.get_stats()['arrays'][array]['type'] 54 | 55 | def status(self, array): 56 | """Return the array's status. 57 | Status of the array (active, inactive, ...).""" 58 | return self.get_stats()['arrays'][array]['status'] 59 | 60 | def components(self, array): 61 | """Return the components of the arrays (list). 62 | List of devices that belong to that array. 63 | """ 64 | return self.get_stats()['arrays'][array]['components'].keys() 65 | 66 | def available(self, array): 67 | """Return the array's available components number.""" 68 | return int(self.get_stats()['arrays'][array]['available']) 69 | 70 | def used(self, array): 71 | """Return the array's used components number.""" 72 | return int(self.get_stats()['arrays'][array]['used']) 73 | 74 | def config(self, array): 75 | """Return the array's config/status. 76 | 77 | U mean OK 78 | _ mean Failed 79 | """ 80 | return self.get_stats()['arrays'][array]['config'] 81 | 82 | def load(self): 83 | """Return a dict of stats.""" 84 | ret = {} 85 | 86 | # Read the mdstat file 87 | with open(self.get_path(), 'r') as f: 88 | # lines is a list of line (with \n) 89 | lines = f.readlines() 90 | 91 | # First line: get the personalities 92 | # The "Personalities" line tells you what RAID level the kernel currently supports. 93 | # This can be changed by either changing the raid modules or recompiling the kernel. 94 | # Possible personalities include: [raid0] [raid1] [raid4] [raid5] [raid6] [linear] [multipath] [faulty] 95 | ret['personalities'] = self.get_personalities(lines[0]) 96 | 97 | # Second to last before line: Array definition 98 | ret['arrays'] = self.get_arrays(lines[1:-1], ret['personalities']) 99 | 100 | # Save the file content as it for the __str__ method 101 | self.content = reduce(lambda x, y: x + y, lines) 102 | 103 | return ret 104 | 105 | def get_personalities(self, line): 106 | """Return a list of personalities readed from the input line.""" 107 | return [split('\W+', i)[1] for i in line.split(':')[1].split(' ') if i.startswith('[')] 108 | 109 | def get_arrays(self, lines, personalities=[]): 110 | """Return a dict of arrays.""" 111 | ret = {} 112 | 113 | i = 0 114 | while i < len(lines): 115 | try: 116 | # First array line: get the md device 117 | md_device = self.get_md_device_name(lines[i]) 118 | except IndexError: 119 | # No array detected 120 | pass 121 | else: 122 | # Array detected 123 | if md_device is not None: 124 | # md device line 125 | ret[md_device] = self.get_md_device(lines[i], personalities) 126 | # md config/status line 127 | i += 1 128 | ret[md_device].update(self.get_md_status(lines[i])) 129 | # action line 130 | if ret[md_device].get('config') and '_' in ret[md_device].get('config'): 131 | i += 1 132 | print(lines[i]) 133 | ret[md_device].update(self.get_md_action(lines[i])) 134 | i += 1 135 | 136 | return ret 137 | 138 | def get_md_device(self, line, personalities=[]): 139 | """Return a dict of md device define in the line.""" 140 | ret = {} 141 | 142 | splitted = split('\W+', line) 143 | # Raid status 144 | # Active or 'started'. An inactive array is usually faulty. 145 | # Stopped arrays aren't visible here. 146 | ret['status'] = splitted[1] 147 | if splitted[2] in personalities: 148 | # Raid type (ex: RAID5) 149 | ret['type'] = splitted[2] 150 | # Array's components 151 | ret['components'] = self.get_components(line, with_type=True) 152 | else: 153 | # Raid type (ex: RAID5) 154 | ret['type'] = None 155 | # Array's components 156 | ret['components'] = self.get_components(line, with_type=False) 157 | 158 | return ret 159 | 160 | def get_md_status(self, line): 161 | """Return a dict of md status define in the line.""" 162 | ret = {} 163 | 164 | splitted = split('\W+', line) 165 | if line.rstrip().endswith(']'): 166 | # The final 2 entries on this line: [n/m] [UUUU_] 167 | # [n/m] means that ideally the array would have n devices however, currently, m devices are in use. 168 | # Obviously when m >= n then things are good. 169 | ret['available'] = splitted[-4] 170 | ret['used'] = splitted[-3] 171 | # [UUUU_] represents the status of each device, either U for up or _ for down. 172 | ret['config'] = splitted[-2] 173 | elif line.lstrip().startswith('['): 174 | print(line) 175 | pass 176 | else: 177 | ret['available'] = None 178 | ret['used'] = None 179 | ret['config'] = None 180 | 181 | return ret 182 | 183 | def get_md_action(self, line): 184 | """Return a dict of md action line. 185 | 186 | @TODO: 187 | Nothing is done for the moment, because i don't know if it is the only pattern. 188 | 189 | But the following line should be analysed: 190 | [>....................] reshape = 2.1% (115168/5237760) finish=3.7min speed=23033K/sec 191 | and the output should be: 192 | {'reshape': '2.1%', 'finish': '3.7min', 'speed': '23033K/sec'} 193 | """ 194 | ret = {} 195 | 196 | splitted = split('\W+', line) 197 | if line.lstrip().startswith('['): 198 | pass 199 | 200 | return ret 201 | 202 | def get_components(self, line, with_type=True): 203 | """Return a dict of components in the line. 204 | 205 | key: device name (ex: 'sdc1') 206 | value: device role number 207 | """ 208 | ret = {} 209 | 210 | # Ignore (F) (see test 08) 211 | line2 = reduce(lambda x, y: x + y, split('\(.+\)', line)) 212 | if with_type: 213 | splitted = split('\W+', line2)[3:] 214 | else: 215 | splitted = split('\W+', line2)[2:] 216 | ret = dict(zip(splitted[0::2], splitted[1::2])) 217 | 218 | return ret 219 | 220 | def get_md_device_name(self, line): 221 | """Return the md device name from the input line.""" 222 | ret = split('\W+', line)[0] 223 | if ret.startswith('md'): 224 | return ret 225 | else: 226 | return None 227 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | import os 5 | import re 6 | 7 | with open(os.path.join('pymdstat', '__init__.py'), encoding='utf-8') as f: 8 | version = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", f.read(), re.M).group(1) 9 | 10 | if not version: 11 | raise RuntimeError('Cannot find version information in __init__.py file.') 12 | 13 | with open('README.rst', encoding='utf-8') as f: 14 | long_description = f.read() 15 | 16 | data_files = [('share/doc/pymdstat', ['AUTHORS', 'LICENSE', 'NEWS', 'README.rst'])] 17 | 18 | setup( 19 | name='pymdstat', 20 | version=version, 21 | description="Python library to parse Linux /proc/mdstat", 22 | long_description=long_description, 23 | author='Nicolas Hennion', 24 | author_email='nicolas@nicolargo.com', 25 | url='https://github.com/nicolargo/pymdstat', 26 | # download_url='https://s3.amazonaws.com/pymdstat/pymdstat-0.4.2.tar.gz', 27 | license="MIT", 28 | keywords="raid linux", 29 | packages=['pymdstat'], 30 | include_package_data=True, 31 | data_files=data_files, 32 | test_suite="unitest.py", 33 | classifiers=[ 34 | 'Development Status :: 4 - Beta', 35 | 'Intended Audience :: Developers', 36 | 'License :: OSI Approved :: MIT License', 37 | 'Operating System :: POSIX :: Linux', 38 | 'Programming Language :: Python :: 2', 39 | 'Programming Language :: Python :: 3', 40 | 'Topic :: Software Development :: Libraries :: Python Modules' 41 | ] 42 | ) 43 | -------------------------------------------------------------------------------- /tests/mdstat.01: -------------------------------------------------------------------------------- 1 | Personalities : 2 | unused devices: -------------------------------------------------------------------------------- /tests/mdstat.02: -------------------------------------------------------------------------------- 1 | Personalities : [raid1] [raid6] [raid5] [raid4] 2 | md_d0 : active raid5 sde1[0] sdf1[4] sdb1[5] sdd1[2] sdc1[1] 3 | 1250241792 blocks super 1.2 level 5, 64k chunk, algorithm 2 [5/5] [UUUUU] 4 | bitmap: 0/10 pages [0KB], 16384KB chunk 5 | 6 | unused devices: -------------------------------------------------------------------------------- /tests/mdstat.03: -------------------------------------------------------------------------------- 1 | Personalities : [raid6] [raid5] [raid4] 2 | md0 : active raid5 sda1[0] sdd1[2] sdb1[1] 3 | 1465151808 blocks level 5, 64k chunk, algorithm 2 [4/3] [UUU_] 4 | 5 | unused devices: -------------------------------------------------------------------------------- /tests/mdstat.04: -------------------------------------------------------------------------------- 1 | Personalities : [raid1] [raid6] [raid5] [raid4] 2 | md1 : active raid1 sdb2[1] sda2[0] 3 | 136448 blocks [2/2] [UU] 4 | 5 | md2 : active raid1 sdb3[1] sda3[0] 6 | 129596288 blocks [2/2] [UU] 7 | 8 | md3 : active raid5 sdl1[9] sdk1[8] sdj1[7] sdi1[6] sdh1[5] sdg1[4] sdf1[3] sde1[2] sdd1[1] sdc1[0] 9 | 1318680576 blocks level 5, 1024k chunk, algorithm 2 [10/10] [UUUUUUUUUU] 10 | 11 | md0 : active raid1 sdb1[1] sda1[0] 12 | 16787776 blocks [2/2] [UU] 13 | 14 | unused devices: -------------------------------------------------------------------------------- /tests/mdstat.05: -------------------------------------------------------------------------------- 1 | Personalities : [raid1] [raid6] [raid5] [raid4] 2 | md127 : active raid5 sdh1[6] sdg1[4] sdf1[3] sde1[2] sdd1[1] sdc1[0] 3 | 1464725760 blocks level 5, 64k chunk, algorithm 2 [6/5] [UUUUU_] 4 | [==>..................] recovery = 12.6% (37043392/292945152) finish=127.5min speed=33440K/sec 5 | 6 | unused devices: -------------------------------------------------------------------------------- /tests/mdstat.06: -------------------------------------------------------------------------------- 1 | Personalities : [linear] [raid0] [raid1] [raid5] [raid4] [raid6] 2 | md0 : active raid6 sdf1[0] sde1[1] sdd1[2] sdc1[3] sdb1[4] sda1[5] hdb1[6] 3 | 1225557760 blocks level 6, 256k chunk, algorithm 2 [7/7] [UUUUUUU] 4 | bitmap: 0/234 pages [0KB], 512KB chunk 5 | 6 | unused devices: -------------------------------------------------------------------------------- /tests/mdstat.07: -------------------------------------------------------------------------------- 1 | Personalities : [raid1] 2 | md1 : active raid1 sde1[6](F) sdg1[1] sdb1[4] sdd1[3] sdc1[2] 3 | 488383936 blocks [6/4] [_UUUU_] 4 | 5 | unused devices: -------------------------------------------------------------------------------- /tests/mdstat.08: -------------------------------------------------------------------------------- 1 | Personalities : [raid5] 2 | md0 : inactive raid5 sdd1[3] sdc1[2] sda1[0] 3 | 95425536 blocks level 5, 128k chunk, algorithm 2 [4/4] [UUUU] 4 | 5 | unused devices: -------------------------------------------------------------------------------- /tests/mdstat.09: -------------------------------------------------------------------------------- 1 | Personalities : [linear] [multipath] [raid0] [raid1] [raid6] [raid5] [raid4] [raid10] 2 | md0 : active raid1 sdf1[1] sde1[0] 3 | 3709312 blocks [2/2] [UU] 4 | 5 | md1 : active raid1 sdf2[1] sde2[0] 6 | 200704 blocks [2/2] [UU] 7 | 8 | md2 : inactive sdb[0](S) sdc[2](S) sdd[3](S) sda[1](S) 9 | 5860553984 blocks 10 | 11 | unused devices: -------------------------------------------------------------------------------- /tests/mdstat.10: -------------------------------------------------------------------------------- 1 | Personalities : [raid0] [raid1] [linear] [multipath] [raid6] [raid5] [raid4] [raid10] 2 | md129 : active raid1 sda3[0] sdd3[1] 3 | 15825920 blocks super 1.2 [2/2] [UU] 4 | 5 | md128 : active raid1 sda2[0] sdd2[1] 6 | 126887936 blocks super 1.2 [2/2] [UU] 7 | bitmap: 1/1 pages [4KB], 65536KB chunk 8 | 9 | md125 : active raid0 sdg[4] sdc[1] sdf[3] sdh[5] sde[2] sdb[0] 10 | 2306837760 blocks super 1.2 128k chunks 11 | 12 | md126 : active raid0 nvme0n1[0] nvme1n1[1] 13 | 999948288 blocks super 1.2 3072k chunks 14 | 15 | unused devices: 16 | -------------------------------------------------------------------------------- /tests/mdstat.11: -------------------------------------------------------------------------------- 1 | Personalities : [raid0] 2 | md127 : inactive sda[0] sdb[1] sdc[2](S) 3 | 6282240 blocks super 1.2 4 | 5 | unused devices: -------------------------------------------------------------------------------- /tests/mdstat.12: -------------------------------------------------------------------------------- 1 | Personalities : [raid0] [raid6] [raid5] [raid4] 2 | md127 : active raid0 loop1[1] loop0[0] 3 | 10475520 blocks super 1.2 32k chunks 4 | 5 | unused devices: -------------------------------------------------------------------------------- /tests/mdstat.13: -------------------------------------------------------------------------------- 1 | Personalities : [raid6] [raid5] [raid4] [raid0] 2 | md127 : active raid4 loop2[3] loop1[1] loop0[0] 3 | 10475520 blocks super 1.2 level 4, 32k chunk, algorithm 5 [4/3] [UU__] 4 | [>....................] reshape = 2.1% (115168/5237760) finish=3.7min speed=23033K/sec 5 | 6 | unused devices: -------------------------------------------------------------------------------- /tests/mdstat.14: -------------------------------------------------------------------------------- 1 | Personalities : [raid0]  2 | md127 : active raid0 sda[0] sdb[1] 3 | 4188160 blocks super 1.2 256k chunks 4 | 5 | unused devices: -------------------------------------------------------------------------------- /tests/mdstat.15: -------------------------------------------------------------------------------- 1 | Personalities : [linear] [raid0] [raid1] [raid10] [raid6] [raid5] [raid4] 2 | md3 : active raid6 sdh6[8] sda6[0] sdg6[6] sdf6[5] sde6[4] sdd6[3] sdc6[2] sdb6[1] 3 | 17581481472 blocks super 1.2 level 6, 64k chunk, algorithm 2 [8/8] [UUUUUUUU] 4 | 5 | md2 : active raid6 sdh5[15] sda5[9] sdg5[14] sdf5[8] sde5[13] sdd5[12] sdc5[11] sdb5[10] 6 | 17552612736 blocks super 1.2 level 6, 64k chunk, algorithm 2 [8/8] [UUUUUUUU] 7 | 8 | md1 : active raid1 sdh2[7] sda2[0] sdb2[1] sdc2[2] sdd2[3] sde2[4] sdf2[5] sdg2[6] 9 | 2097088 blocks [8/8] [UUUUUUUU] 10 | 11 | md0 : active raid1 sdh1[7] sda1[0] sdb1[1] sdc1[2] sdd1[3] sde1[4] sdf1[5] sdg1[6] 12 | 2490176 blocks [8/8] [UUUUUUUU] 13 | 14 | unused devices: -------------------------------------------------------------------------------- /tests/mdstat.16: -------------------------------------------------------------------------------- 1 | Personalities : [raid6] [raid5] [raid4] [linear] [multipath] [raid0] [raid1] [raid10] 2 | md1 : active raid5 sdd[2] sdf[8] sdb[6] sda[0] sdh[5] sdc[1] sde[7] sdg[3] 3 | 95705752576 blocks super 1.2 level 5, 512k chunk, algorithm 2 [8/8] [UUUUUUUU] 4 | bitmap: 2/102 pages [8KB], 65536KB chunk 5 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # Install: 3 | # pip install tox 4 | # Run: 5 | # tox 6 | 7 | [tox] 8 | envlist = py27, py34 9 | 10 | [testenv] 11 | commands = 12 | python unitest.py 13 | #flake8 --exclude=build,.tox,.git 14 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ $(id -u) -ne 0 ]; then 4 | echo -e "* ERROR: User $(whoami) is not root, and does not have sudo privileges" 5 | exit 1 6 | fi 7 | 8 | if [ ! -f "setup.py" ]; then 9 | echo -e "* ERROR: Setup file doesn't exist" 10 | exit 1 11 | fi 12 | 13 | 14 | 15 | python setup.py install --record install.record 16 | 17 | for i in $(cat install.record); do 18 | rm $i 19 | done 20 | 21 | echo -e "\n\n* SUCCESS: Uninstall complete." 22 | rm install.record 23 | -------------------------------------------------------------------------------- /unitest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # PyMDstat 5 | # Unitary test 6 | # 7 | # Copyright (C) 2014 Nicolargo 8 | 9 | import unittest 10 | from pprint import pprint 11 | 12 | from pymdstat import MdStat 13 | 14 | # In Python 3, assertItemsEqual method is named assertCountEqual 15 | try: 16 | unittest.TestCase.assertCountEqual = unittest.TestCase.assertItemsEqual 17 | except AttributeError: 18 | pass 19 | 20 | 21 | class TestPyMdStat(unittest.TestCase): 22 | 23 | """Test PyMDstat module.""" 24 | 25 | def test_000_loadall(self): 26 | for i in range(1, 10): 27 | mdstat_test = MdStat('./tests/mdstat.0%s' % i) 28 | # print('%s' % mdstat_test.get_stats()) 29 | self.assertNotEqual(mdstat_test.get_stats(), {}) 30 | 31 | def test_099_didnotexist(self): 32 | try: 33 | mdstat_test = MdStat('/proc/NOmdstat') 34 | except IOError: 35 | self.assertTrue(True) 36 | else: 37 | self.assertFalse(True) 38 | 39 | def test_099_model(self): 40 | i = 4 41 | mdstat_test = MdStat('./tests/mdstat.0%s' % i) 42 | self.assertCountEqual(mdstat_test.personalities(), ['raid1', 'raid6', 'raid5', 'raid4']) 43 | self.assertCountEqual(mdstat_test.arrays(), ['md2', 'md3', 'md0', 'md1']) 44 | self.assertEqual(mdstat_test.type('md3'), 'raid5') 45 | self.assertEqual(mdstat_test.status('md3'), 'active') 46 | self.assertEqual(mdstat_test.available('md3'), 10) 47 | self.assertEqual(mdstat_test.used('md3'), 10) 48 | self.assertCountEqual(mdstat_test.components('md3'), ['sdk1', 'sdj1', 'sde1', 'sdl1', 'sdg1', 'sdf1', 'sdh1', 'sdc1', 'sdd1', 'sdi1']) 49 | self.assertEqual(mdstat_test.config('md3'), 'UUUUUUUUUU') 50 | 51 | def test_001(self): 52 | i = 1 53 | mdstat_test = MdStat('./tests/mdstat.0%s' % i) 54 | self.assertEqual(mdstat_test.get_stats()['personalities'], []) 55 | self.assertEqual(mdstat_test.get_stats()['arrays'], {}) 56 | 57 | def test_002(self): 58 | i = 2 59 | mdstat_test = MdStat('./tests/mdstat.0%s' % i) 60 | self.assertCountEqual(mdstat_test.get_stats()['personalities'], ['raid1', 'raid6', 'raid5', 'raid4']) 61 | self.assertEqual(mdstat_test.get_stats()['arrays'], {'md_d0': {'status': 'active', 'available': '5', 'used': '5', 'components': {'sdc1': '1', 'sdb1': '5', 'sde1': '0', 'sdd1': '2', 'sdf1': '4'}, 'config': 'UUUUU', 'type': 'raid5'}}) 62 | 63 | def test_003(self): 64 | i = 3 65 | mdstat_test = MdStat('./tests/mdstat.0%s' % i) 66 | self.assertCountEqual(mdstat_test.get_stats()['personalities'], ['raid6', 'raid5', 'raid4']) 67 | self.assertEqual(mdstat_test.get_stats()['arrays'], {'md0': {'status': 'active', 'available': '4', 'used': '3', 'components': {'sdb1': '1', 'sdd1': '2', 'sda1': '0'}, 'config': 'UUU_', 'type': 'raid5'}}) 68 | 69 | def test_004(self): 70 | i = 4 71 | mdstat_test = MdStat('./tests/mdstat.0%s' % i) 72 | self.assertCountEqual(mdstat_test.get_stats()['personalities'], ['raid1', 'raid6', 'raid5', 'raid4']) 73 | self.assertEqual(mdstat_test.get_stats()['arrays'], {'md2': {'status': 'active', 'available': '2', 'used': '2', 'components': {'sdb3': '1', 'sda3': '0'}, 'config': 'UU', 'type': 'raid1'}, 'md3': {'status': 'active', 'available': '10', 'used': '10', 'components': {'sdk1': '8', 'sdj1': '7', 'sde1': '2', 'sdl1': '9', 'sdg1': '4', 'sdf1': '3', 'sdh1': '5', 'sdc1': '0', 'sdd1': '1', 'sdi1': '6'}, 'config': 'UUUUUUUUUU', 'type': 'raid5'}, 'md0': {'status': 'active', 'available': '2', 'used': '2', 'components': {'sdb1': '1', 'sda1': '0'}, 'config': 'UU', 'type': 'raid1'}, 'md1': {'status': 'active', 'available': '2', 'used': '2', 'components': {'sdb2': '1', 'sda2': '0'}, 'config': 'UU', 'type': 'raid1'}}) 74 | 75 | def test_005(self): 76 | i = 5 77 | mdstat_test = MdStat('./tests/mdstat.0%s' % i) 78 | self.assertCountEqual(mdstat_test.get_stats()['personalities'], ['raid1', 'raid6', 'raid5', 'raid4']) 79 | self.assertEqual(mdstat_test.get_stats()['arrays'], {'md127': {'status': 'active', 'available': '6', 'used': '5', 'components': {'sde1': '2', 'sdg1': '4', 'sdf1': '3', 'sdh1': '6', 'sdc1': '0', 'sdd1': '1'}, 'config': 'UUUUU_', 'type': 'raid5'}}) 80 | 81 | def test_006(self): 82 | i = 6 83 | mdstat_test = MdStat('./tests/mdstat.0%s' % i) 84 | self.assertCountEqual(mdstat_test.get_stats()['personalities'], ['linear', 'raid0', 'raid1', 'raid5', 'raid4', 'raid6']) 85 | self.assertEqual(mdstat_test.get_stats()['arrays'], {'md0': {'status': 'active', 'available': '7', 'used': '7', 'components': {'sde1': '1', 'sdf1': '0', 'sdc1': '3', 'sdb1': '4', 'hdb1': '6', 'sdd1': '2', 'sda1': '5'}, 'config': 'UUUUUUU', 'type': 'raid6'}}) 86 | 87 | def test_007(self): 88 | i = 7 89 | mdstat_test = MdStat('./tests/mdstat.0%s' % i) 90 | self.assertCountEqual(mdstat_test.get_stats()['personalities'], ['raid1']) 91 | self.assertEqual(mdstat_test.get_stats()['arrays'], {'md1': {'status': 'active', 'available': '6', 'used': '4', 'components': {'sdc1': '2', 'sdb1': '4', 'sde1': '6', 'sdd1': '3', 'sdg1': '1'}, 'config': '_UUUU_', 'type': 'raid1'}}) 92 | 93 | def test_008(self): 94 | i = 8 95 | mdstat_test = MdStat('./tests/mdstat.0%s' % i) 96 | self.assertCountEqual(mdstat_test.get_stats()['personalities'], ['raid5']) 97 | self.assertEqual(mdstat_test.get_stats()['arrays'], {'md0': {'status': 'inactive', 'available': '4', 'used': '4', 'components': {'sdd1': '3', 'sdc1': '2', 'sda1': '0'}, 'config': 'UUUU', 'type': 'raid5'}}) 98 | 99 | def test_009(self): 100 | i = 9 101 | mdstat_test = MdStat('./tests/mdstat.0%s' % i) 102 | self.assertCountEqual(mdstat_test.get_stats()['personalities'], ['linear', 'multipath', 'raid0', 'raid1', 'raid6', 'raid5', 'raid4', 'raid10']) 103 | self.assertEqual(mdstat_test.get_stats()['arrays'], {'md2': {'status': 'inactive', 'available': None, 'used': None, 'components': {'sdb': '0'}, 'config': None, 'type': None}, 'md0': {'status': 'active', 'available': '2', 'used': '2', 'components': {'sde1': '0', 'sdf1': '1'}, 'config': 'UU', 'type': 'raid1'}, 'md1': {'status': 'active', 'available': '2', 'used': '2', 'components': {'sde2': '0', 'sdf2': '1'}, 'config': 'UU', 'type': 'raid1'}}) 104 | 105 | def test_010(self): 106 | i = 10 107 | mdstat_test = MdStat('./tests/mdstat.%s' % i) 108 | self.assertEqual(mdstat_test.get_stats()['arrays']['md126'], {'status': 'active', 'available': None, 'used': None, 'components': {'nvme0n1': '0', 'nvme1n1': '1'}, 'config': None, 'type': 'raid0'}) 109 | self.assertEqual(mdstat_test.get_stats()['arrays']['md129'], {'status': 'active', 'available': '2', 'used': '2', 'components': {'sda3': '0', 'sdd3': '1'}, 'config': 'UU', 'type': 'raid1'}) 110 | 111 | def test_011(self): 112 | i = 11 113 | mdstat_test = MdStat('./tests/mdstat.%s' % i) 114 | self.assertEqual(type(mdstat_test.get_stats()), type({})) 115 | 116 | def test_012(self): 117 | i = 12 118 | mdstat_test = MdStat('./tests/mdstat.%s' % i) 119 | self.assertEqual(type(mdstat_test.get_stats()), type({})) 120 | 121 | def test_013(self): 122 | i = 13 123 | mdstat_test = MdStat('./tests/mdstat.%s' % i) 124 | self.assertEqual(type(mdstat_test.get_stats()), type({})) 125 | 126 | def test_014(self): 127 | i = 14 128 | mdstat_test = MdStat('./tests/mdstat.%s' % i) 129 | self.assertEqual(type(mdstat_test.get_stats()), type({})) 130 | 131 | def test_015(self): 132 | i = 15 133 | mdstat_test = MdStat('./tests/mdstat.%s' % i) 134 | 135 | 136 | if __name__ == '__main__': 137 | unittest.main() 138 | --------------------------------------------------------------------------------