├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ └── test-build.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── ouroboros_logo_icon_256.png ├── ouroboros_logo_icon_72.png ├── ouroboros_logo_primary.png ├── ouroboros_logo_primary_cropped.jpg ├── ouroboros_logo_primary_cropped.png ├── ouroboros_logo_primary_discord.jpg ├── ouroboros_logo_primary_long_cropped.jpg └── ouroboros_logo_primary_smaller_square_crop.png ├── docker-compose.yml ├── locales ├── es_ES │ └── LC_MESSAGES │ │ ├── notifiers.mo │ │ ├── notifiers.po │ │ ├── ouroboros.mo │ │ └── ouroboros.po ├── notifiers.pot └── ouroboros.pot ├── ouroboros ├── ouroboros.pyproj ├── ouroboros.sln ├── pyouroboros ├── __init__.py ├── config.py ├── dataexporters.py ├── dockerclient.py ├── helpers.py ├── logger.py ├── notifiers.py └── ouroboros.py ├── requirements.txt └── setup.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: docker 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | open-pull-requests-limit: 10 14 | 15 | - package-ecosystem: pip 16 | directory: "/" 17 | schedule: 18 | interval: daily 19 | open-pull-requests-limit: 10 20 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | test: 9 | name: Docker Test on ubuntu-latest 10 | runs-on: ubuntu-latest 11 | services: 12 | registry: 13 | image: registry:2 14 | ports: 15 | - 5000:5000 16 | steps: 17 | - name: Checkout Repository 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 1 21 | - name: Set up QEMU 22 | uses: docker/setup-qemu-action@v3 23 | with: 24 | platforms: amd64 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v3 27 | with: 28 | version: latest 29 | driver-opts: network=host 30 | - name: Update Version String 31 | run: | 32 | sed -i -r 's/VERSION = "custom"/VERSION = "0.0+test"/' pyouroboros/__init__.py 33 | echo $?\ 34 | - name: Build Docker 35 | uses: docker/build-push-action@v6 36 | with: 37 | context: . 38 | file: ./Dockerfile 39 | platforms: linux/amd64 40 | push: true 41 | cache-from: type=gha,scope=main 42 | cache-to: type=gha,mode=max,scope=main 43 | tags: localhost:5000/tester/ouroboros:test 44 | - name: Test with Docker 45 | run: | 46 | sudo mkdir -p /app/pyouroboros/hooks 47 | docker run --rm --name ouroboros -v /var/run/docker.sock:/var/run/docker.sock localhost:5000/tester/ouroboros:test --run-once --dry-run --log-level debug 48 | build: 49 | name: Docker Build on ubuntu-latest 50 | runs-on: ubuntu-latest 51 | needs: test 52 | steps: 53 | - name: Check Credentials 54 | id: check_credentials 55 | env: 56 | DOCKER_USER: ${{ secrets.DOCKER_USER }} 57 | DOCKER_CLITOKEN: ${{ secrets.DOCKER_CLITOKEN }} 58 | run: | 59 | if [ "${DOCKER_USER}" == "" ]; then 60 | echo "Missing User" 61 | echo "missingsecrets=yes" >> $GITHUB_OUTPUT 62 | elif [ "${DOCKER_CLITOKEN}" == "" ]; then 63 | echo "Missing Cli Token" 64 | echo "missingsecrets=yes" >> $GITHUB_OUTPUT 65 | else 66 | echo "All secrets present" 67 | echo "missingsecrets=no" >> $GITHUB_OUTPUT 68 | fi 69 | - name: Checkout Repository 70 | if: contains(steps.check_credentials.outputs.missingsecrets, 'no') 71 | uses: actions/checkout@v4 72 | with: 73 | fetch-depth: 1 74 | - name: Get Revision Variables 75 | id: build_env 76 | run: | 77 | echo ${GITHUB_REF:10} 78 | echo "branch=${GITHUB_REF:10}" >> $GITHUB_OUTPUT 79 | - name: Set up QEMU 80 | if: contains(steps.check_credentials.outputs.missingsecrets, 'no') 81 | uses: docker/setup-qemu-action@v3 82 | with: 83 | platforms: amd64,arm64,arm 84 | - name: Set up Docker Buildx 85 | if: contains(steps.check_credentials.outputs.missingsecrets, 'no') 86 | uses: docker/setup-buildx-action@v3 87 | with: 88 | version: latest 89 | - name: Login to DockerHub Registry 90 | if: contains(steps.check_credentials.outputs.missingsecrets, 'no') 91 | uses: docker/login-action@v3 92 | with: 93 | username: ${{ secrets.DOCKER_USER }} 94 | password: ${{ secrets.DOCKER_CLITOKEN }} 95 | logout: true 96 | - name: Login to GitHub Container Registry 97 | if: contains(steps.check_credentials.outputs.missingsecrets, 'no') 98 | uses: docker/login-action@v3 99 | with: 100 | registry: ghcr.io 101 | username: ${{ github.repository_owner }} 102 | password: ${{ secrets.GITHUB_TOKEN }} 103 | logout: true 104 | - name: Update Version String 105 | if: contains(steps.check_credentials.outputs.missingsecrets, 'no') 106 | env: 107 | OUROBOROS_VERSION: ${{ steps.build_env.outputs.branch }} 108 | run: | 109 | sed -i -r 's/VERSION = "custom"/VERSION = "'$OUROBOROS_VERSION'"/' pyouroboros/__init__.py 110 | echo $?\ 111 | - name: Build and Push Docker 112 | if: contains(steps.check_credentials.outputs.missingsecrets, 'no') 113 | uses: docker/build-push-action@v6 114 | with: 115 | context: . 116 | file: ./Dockerfile 117 | platforms: linux/amd64,linux/arm64,linux/arm/v7 118 | push: true 119 | cache-from: type=gha,scope=main 120 | cache-to: type=gha,mode=max,scope=main 121 | tags: | 122 | ${{ secrets.DOCKER_USER }}/ouroboros:${{ steps.build_env.outputs.branch }} 123 | ${{ secrets.DOCKER_USER }}/ouroboros:latest 124 | ghcr.io/${{ github.repository_owner }}/ouroboros:${{ steps.build_env.outputs.branch }} 125 | ghcr.io/${{ github.repository_owner }}/ouroboros:latest 126 | cleanup: 127 | name: Cleanup Cache 128 | runs-on: ubuntu-latest 129 | needs: [test, build] 130 | steps: 131 | - name: Cleanup Cache 132 | run: | 133 | gh extension install actions/gh-actions-cache 134 | REPO="${{ github.repository }}" 135 | BRANCH="${{ github.ref }}" 136 | echo "Fetching list of cache keys" 137 | cacheKeys=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 ) 138 | ## Setting this to not fail the workflow while deleting cache keys. 139 | set +e 140 | echo "Deleting caches..." 141 | for cacheKey in $cacheKeys 142 | do 143 | gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm 144 | done 145 | echo "Done" 146 | env: 147 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 148 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '0 3 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | if: github.actor != 'dependabot' || github.event_name != 'push' 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v3 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v3 73 | -------------------------------------------------------------------------------- /.github/workflows/test-build.yml: -------------------------------------------------------------------------------- 1 | name: Test Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | test: 10 | name: Docker Test on ubuntu-latest 11 | runs-on: ubuntu-latest 12 | services: 13 | registry: 14 | image: registry:2 15 | ports: 16 | - 5000:5000 17 | steps: 18 | - name: Checkout Repository 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 1 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v3 24 | with: 25 | platforms: amd64 26 | - name: Set up Docker Buildx 27 | uses: docker/setup-buildx-action@v3 28 | with: 29 | version: latest 30 | driver-opts: network=host 31 | - name: Update Version String 32 | run: | 33 | sed -i -r 's/VERSION = "custom"/VERSION = "0.0+test"/' pyouroboros/__init__.py 34 | echo $?\ 35 | - name: Build Docker 36 | uses: docker/build-push-action@v6 37 | with: 38 | context: . 39 | file: ./Dockerfile 40 | platforms: linux/amd64 41 | push: true 42 | cache-from: type=gha,scope=main 43 | tags: localhost:5000/tester/ouroboros:test 44 | - name: Build Docker (main) 45 | if: github.ref_name == 'main' 46 | uses: docker/build-push-action@v6 47 | with: 48 | context: . 49 | file: ./Dockerfile 50 | platforms: linux/amd64 51 | push: true 52 | cache-from: type=gha 53 | cache-to: type=gha,mode=max 54 | tags: localhost:5000/tester/ouroboros:test 55 | - name: Test with Docker 56 | run: | 57 | sudo mkdir -p /app/pyouroboros/hooks 58 | docker run --rm --name ouroboros -v /var/run/docker.sock:/var/run/docker.sock localhost:5000/tester/ouroboros:test --run-once --dry-run --log-level debug 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific files 2 | *.rsuser 3 | *.suo 4 | *.user 5 | *.userosscache 6 | *.sln.docstates 7 | 8 | # Build results 9 | [Dd]ebug/ 10 | [Dd]ebugPublic/ 11 | [Rr]elease/ 12 | [Rr]eleases/ 13 | x64/ 14 | x86/ 15 | [Aa][Rr][Mm]/ 16 | [Aa][Rr][Mm]64/ 17 | bld/ 18 | [Bb]in/ 19 | [Oo]bj/ 20 | [Ll]og/ 21 | [Ll]ogs/ 22 | 23 | # Visual Studio 2015/2017 cache/options directory 24 | .vs/ 25 | # Uncomment if you have tasks that create the project's static files in wwwroot 26 | #wwwroot/ 27 | 28 | # Visual Studio 2017 auto generated files 29 | Generated\ Files/ 30 | 31 | # Files built by Visual Studio 32 | *_i.c 33 | *_p.c 34 | *_h.h 35 | *.ilk 36 | *.meta 37 | *.obj 38 | *.iobj 39 | *.pch 40 | *.pdb 41 | *.ipdb 42 | *.pgc 43 | *.pgd 44 | *.rsp 45 | *.sbr 46 | *.tlb 47 | *.tli 48 | *.tlh 49 | *.tmp 50 | *.tmp_proj 51 | *_wpftmp.csproj 52 | *.log 53 | *.vspscc 54 | *.vssscc 55 | .builds 56 | *.pidb 57 | *.svclog 58 | *.scc 59 | 60 | # Visual Studio profiler 61 | *.psess 62 | *.vsp 63 | *.vspx 64 | *.sap 65 | 66 | # Visual Studio Trace Files 67 | *.e2e 68 | 69 | # TFS 2012 Local Workspace 70 | $tf/ 71 | 72 | # Visual Studio cache files 73 | # files ending in .cache can be ignored 74 | *.[Cc]ache 75 | # but keep track of directories ending in .cache 76 | !?*.[Cc]ache/ 77 | 78 | # Byte-compiled / optimized / DLL files 79 | __pycache__/ 80 | *.py[cod] 81 | *$py.class 82 | 83 | # C extensions 84 | *.so 85 | 86 | # Distribution / packaging 87 | .Python 88 | build/ 89 | develop-eggs/ 90 | dist/ 91 | downloads/ 92 | eggs/ 93 | .eggs/ 94 | lib/ 95 | lib64/ 96 | parts/ 97 | sdist/ 98 | var/ 99 | wheels/ 100 | *.egg-info/ 101 | .installed.cfg 102 | *.egg 103 | MANIFEST 104 | 105 | # PyInstaller 106 | # Usually these files are written by a python script from a template 107 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 108 | *.manifest 109 | *.spec 110 | 111 | # Installer logs 112 | pip-log.txt 113 | pip-delete-this-directory.txt 114 | 115 | # Unit test / coverage reports 116 | htmlcov/ 117 | .tox/ 118 | .nox/ 119 | .coverage 120 | .coverage.* 121 | .cache 122 | nosetests.xml 123 | coverage.xml 124 | *.cover 125 | .hypothesis/ 126 | .pytest_cache/ 127 | 128 | # Translations 129 | #*.mo 130 | #*.pot 131 | 132 | # Django stuff: 133 | *.log 134 | local_settings.py 135 | db.sqlite3 136 | 137 | # Flask stuff: 138 | instance/ 139 | .webassets-cache 140 | 141 | # Scrapy stuff: 142 | .scrapy 143 | 144 | # Sphinx documentation 145 | docs/_build/ 146 | 147 | # PyBuilder 148 | target/ 149 | 150 | # Jupyter Notebook 151 | .ipynb_checkpoints 152 | 153 | # IPython 154 | profile_default/ 155 | ipython_config.py 156 | 157 | # pyenv 158 | .python-version 159 | 160 | # celery beat schedule file 161 | celerybeat-schedule 162 | 163 | # SageMath parsed files 164 | *.sage.py 165 | 166 | # Environments 167 | .env 168 | .venv 169 | env/ 170 | venv/ 171 | ENV/ 172 | env.bak/ 173 | venv.bak/ 174 | 175 | # Spyder project settings 176 | .spyderproject 177 | .spyproject 178 | 179 | # Rope project settings 180 | .ropeproject 181 | 182 | # mkdocs documentation 183 | /site 184 | 185 | # mypy 186 | .mypy_cache/ 187 | .dmypy.json 188 | dmypy.json 189 | 190 | #vscode 191 | .vscode/ 192 | 193 | # JetBrains 194 | .idea/ 195 | *.iml 196 | 197 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v1.9.0](https://github.com/gmt2001/ouroboros/tree/v1.9.0) (2021-09-20) 4 | [Full Changelog](https://github.com/gmt2001/ouroboros/compare/1.9.0...v1.8.0) 5 | 6 | **Implemented enhancements:** 7 | 8 | - Added the ability to change the Docker client timeout via DOCKER_TIMEOUT environment variable [\#33](https://github.com/gmt2001/ouroboros/pull/33) ([gkovelman](https://github.com/gkovelman)) 9 | 10 | ## [v1.8.0](https://github.com/gmt2001/ouroboros/tree/v1.8.0) (2021-06-14) 11 | [Full Changelog](https://github.com/gmt2001/ouroboros/compare/1.8.0...v1.7.0) 12 | 13 | **Implemented enhancements:** 14 | 15 | - Added the ability to translate the notifications sent out via apprise [\#28](https://github.com/gmt2001/ouroboros/pull/28) ([TaixMiguel](https://github.com/TaixMiguel)) 16 | 17 | ## [v1.7.0](https://github.com/gmt2001/ouroboros/tree/v1.7.0) (2020-11-15) 18 | [Full Changelog](https://github.com/gmt2001/ouroboros/compare/1.7.0...v1.6.0) 19 | 20 | **Fixed bugs:** 21 | 22 | - Bump Apprise to v0.8.5, which fixes the slack Token B limit issue [pyouroboros#385](https://github.com/pyouroboros/ouroboros/pull/385) ([Mdleal](https://github.com/Mdleal)) 23 | - Fixed issue where multiple containers which are on different tags that point to the same image id would be updated, when only one tag actually had an update available [pyouroboros#393](https://github.com/pyouroboros/ouroboros/pull/393) ([koreno](https://github.com/koreno)) 24 | - Fixed issue where multiple containers which are on different tags could end up all switching to the updated tag unintentionally [\pyouroboros#395](https://github.com/pyouroboros/ouroboros/pull/395) ([koreno](https://github.com/koreno)) 25 | 26 | ## [v1.6.0](https://github.com/gmt2001/ouroboros/tree/v1.6.0) (2020-06-11) 27 | [Full Changelog](https://github.com/gmt2001/ouroboros/compare/1.6.0...v1.5.1) 28 | 29 | **Implemented enhancements:** 30 | 31 | - Added hooks system 32 | - Added option to cleanup unused volumes [\#6](https://github.com/gmt2001/ouroboros/pull/6) ([MENTAL](https://github.com/thisis-mental)) 33 | 34 | ## [v1.5.1](https://github.com/gmt2001/ouroboros/tree/v1.5.1) (2020-06-11) 35 | [Full Changelog](https://github.com/gmt2001/ouroboros/compare/1.5.1...v1.4.3) 36 | 37 | **Implemented enhancements:** 38 | 39 | - Added Monitor only capacity [\#4](https://github.com/gmt2001/ouroboros/pull/4) ([RUSSANDOL](https://github.com/russandol-sarl)) 40 | - Switched GitHub Actions build chain 41 | 42 | **Fixed bugs:** 43 | 44 | - Let Apprise know that emails are sent as text, not HTML [\#3](https://github.com/gmt2001/ouroboros/pull/3) ([Felix Engelmann](https://github.com/felix-engelmann)) 45 | - Added patch as images in gitlab registrys dont have @sha256: part of the image therefore it needs to be pulled from the image name 'RepoDigests" [\#2](https://github.com/gmt2001/ouroboros/pull/2) ([samson4649](https://github.com/samson4649)) 46 | - Apprise - Fix Slack issue - Token B limited to 8 characters [\#1](https://github.com/gmt2001/ouroboros/pull/1) ([Mdleal](https://github.com/Mdleal)) 47 | 48 | ## [1.4.3](https://github.com/pyouroboros/ouroboros/tree/1.4.3) (2019-12-11) 49 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.4.3...1.4.2) 50 | 51 | **Fixed bugs:** 52 | 53 | - Join Notifications truncated and icon returning 404 [\#325](https://github.com/pyouroboros/ouroboros/issues/325) 54 | 55 | **Other Pull Requests** 56 | 57 | - v1.4.3 Merge [\#354](https://github.com/pyouroboros/ouroboros/pull/354) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 58 | - v1.4.3 to develop [\#353](https://github.com/pyouroboros/ouroboros/pull/353) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 59 | - Update requirements.txt [\#349](https://github.com/pyouroboros/ouroboros/pull/349) ([nemchik](https://github.com/nemchik)) 60 | 61 | ## [1.4.2](https://github.com/pyouroboros/ouroboros/tree/1.4.2) (2019-08-01) 62 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.4.1...1.4.2) 63 | 64 | **Other Pull Requests** 65 | 66 | - 1.4.2 Merge [\#327](https://github.com/pyouroboros/ouroboros/pull/327) ([circa10a](https://github.com/circa10a)) 67 | - 1.4.2 to develop [\#326](https://github.com/pyouroboros/ouroboros/pull/326) ([circa10a](https://github.com/circa10a)) 68 | 69 | ## [1.4.1](https://github.com/pyouroboros/ouroboros/tree/1.4.1) (2019-06-04) 70 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/v1.4.0...1.4.1) 71 | 72 | **Other Pull Requests** 73 | 74 | - v1.4.1 Merge [\#315](https://github.com/pyouroboros/ouroboros/pull/315) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 75 | - v1.4.1 to develop [\#314](https://github.com/pyouroboros/ouroboros/pull/314) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 76 | 77 | ## [v1.4.0](https://github.com/pyouroboros/ouroboros/tree/v1.4.0) (2019-04-25) 78 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.3.1...v1.4.0) 79 | 80 | **Implemented enhancements:** 81 | 82 | - Make startup notification optional [\#253](https://github.com/pyouroboros/ouroboros/issues/253) 83 | 84 | **Fixed bugs:** 85 | 86 | - Missing MANIFEST.in file causes pypi install to fail [\#284](https://github.com/pyouroboros/ouroboros/issues/284) 87 | - Healthcheck section not re-applied after container update [\#275](https://github.com/pyouroboros/ouroboros/issues/275) 88 | - docker-compose local and remote tls logger location [\#273](https://github.com/pyouroboros/ouroboros/issues/273) 89 | - Self update errors. Not deleting old self [\#262](https://github.com/pyouroboros/ouroboros/issues/262) 90 | - ouroboros sets fixed IP addresses [\#254](https://github.com/pyouroboros/ouroboros/issues/254) 91 | 92 | **Closed issues:** 93 | 94 | - Update apprise to v0.7.4 [\#266](https://github.com/pyouroboros/ouroboros/issues/266) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] 95 | - Docker TLS verify: Does it support server and client-side auth, or only server-side auth? [\#256](https://github.com/pyouroboros/ouroboros/issues/256) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] 96 | 97 | **Other Pull Requests** 98 | 99 | - v1.4.0 Merge [\#299](https://github.com/pyouroboros/ouroboros/pull/299) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 100 | - Remove Watchtower Reference [\#298](https://github.com/pyouroboros/ouroboros/pull/298) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 101 | - v1.4.0 to develop [\#297](https://github.com/pyouroboros/ouroboros/pull/297) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 102 | - fixes \#284 [\#296](https://github.com/pyouroboros/ouroboros/pull/296) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 103 | - Patches [\#295](https://github.com/pyouroboros/ouroboros/pull/295) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 104 | - Fix healthcheck attr [\#294](https://github.com/pyouroboros/ouroboros/pull/294) ([circa10a](https://github.com/circa10a)) 105 | - Patch/catch up [\#271](https://github.com/pyouroboros/ouroboros/pull/271) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 106 | - Arg skip startup notifications [\#261](https://github.com/pyouroboros/ouroboros/pull/261) ([circa10a](https://github.com/circa10a)) 107 | - Revert "add option to skip startup notifications" [\#260](https://github.com/pyouroboros/ouroboros/pull/260) ([circa10a](https://github.com/circa10a)) 108 | - add option to skip startup notifications [\#259](https://github.com/pyouroboros/ouroboros/pull/259) ([circa10a](https://github.com/circa10a)) 109 | 110 | ## [1.3.1](https://github.com/pyouroboros/ouroboros/tree/1.3.1) (2019-02-28) 111 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.3.0...1.3.1) 112 | 113 | **Fixed bugs:** 114 | 115 | - Since 1.3.0, docker login fails [\#243](https://github.com/pyouroboros/ouroboros/issues/243) 116 | - Catch Failed self-updates [\#230](https://github.com/pyouroboros/ouroboros/issues/230) 117 | 118 | **Other Pull Requests** 119 | 120 | - v1.3.1 Merge [\#249](https://github.com/pyouroboros/ouroboros/pull/249) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 121 | - v1.3.1 to develop [\#248](https://github.com/pyouroboros/ouroboros/pull/248) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 122 | - fix name subscript for \#243 [\#247](https://github.com/pyouroboros/ouroboros/pull/247) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 123 | - fixes \#230 and \#243 [\#242](https://github.com/pyouroboros/ouroboros/pull/242) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 124 | 125 | ## [1.3.0](https://github.com/pyouroboros/ouroboros/tree/1.3.0) (2019-02-25) 126 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.2.1...1.3.0) 127 | 128 | **Implemented enhancements:** 129 | 130 | - Start new container in detached mode [\#222](https://github.com/pyouroboros/ouroboros/pull/222) ([nightvisi0n](https://github.com/nightvisi0n)) 131 | - Optimise dockerfile layers [\#218](https://github.com/pyouroboros/ouroboros/pull/218) ([nightvisi0n](https://github.com/nightvisi0n)) 132 | 133 | **Fixed bugs:** 134 | 135 | - Cron scheduled missed following successful runs [\#229](https://github.com/pyouroboros/ouroboros/issues/229) 136 | - Catch attribute.id error [\#226](https://github.com/pyouroboros/ouroboros/issues/226) 137 | - AttachStdout and AttachStderr are not carried over properly [\#221](https://github.com/pyouroboros/ouroboros/issues/221) 138 | - Exception when updating container started with --rm \(autoremove\) [\#219](https://github.com/pyouroboros/ouroboros/issues/219) 139 | - Issue with Swarm Mode V2 [\#216](https://github.com/pyouroboros/ouroboros/issues/216) 140 | - Fix docker swarm mode [\#227](https://github.com/pyouroboros/ouroboros/pull/227) ([mathcantin](https://github.com/mathcantin)) 141 | 142 | **Other Pull Requests** 143 | 144 | - v1.3.0 Merge [\#241](https://github.com/pyouroboros/ouroboros/pull/241) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 145 | - v1.3.0 to develop [\#240](https://github.com/pyouroboros/ouroboros/pull/240) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 146 | - Catch self update apierror [\#238](https://github.com/pyouroboros/ouroboros/pull/238) ([circa10a](https://github.com/circa10a)) 147 | - Catch attribute error [\#237](https://github.com/pyouroboros/ouroboros/pull/237) ([circa10a](https://github.com/circa10a)) 148 | - Check for autoremove [\#236](https://github.com/pyouroboros/ouroboros/pull/236) ([circa10a](https://github.com/circa10a)) 149 | - Add misfire\_grace\_time for cron scheduler [\#234](https://github.com/pyouroboros/ouroboros/pull/234) ([circa10a](https://github.com/circa10a)) 150 | - Check all services by default on swarm mode [\#228](https://github.com/pyouroboros/ouroboros/pull/228) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([mathcantin](https://github.com/mathcantin)) 151 | - remove git in pypi + branch develop + version bump + maintainer\_email [\#214](https://github.com/pyouroboros/ouroboros/pull/214) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 152 | 153 | ## [1.2.1](https://github.com/pyouroboros/ouroboros/tree/1.2.1) (2019-02-14) 154 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.2.0...1.2.1) 155 | 156 | **Fixed bugs:** 157 | 158 | - Broken when no :tag specified [\#210](https://github.com/pyouroboros/ouroboros/issues/210) 159 | 160 | **Other Pull Requests** 161 | 162 | - v1.2.1 Merge [\#213](https://github.com/pyouroboros/ouroboros/pull/213) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 163 | - v1.2.1 to develop [\#212](https://github.com/pyouroboros/ouroboros/pull/212) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 164 | - fixes \#210 [\#211](https://github.com/pyouroboros/ouroboros/pull/211) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 165 | - version bump to 1.2.1 + develop branch + twine fix + … [\#209](https://github.com/pyouroboros/ouroboros/pull/209) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 166 | 167 | ## [1.2.0](https://github.com/pyouroboros/ouroboros/tree/1.2.0) (2019-02-14) 168 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.1.2...1.2.0) 169 | 170 | **Implemented enhancements:** 171 | 172 | - Move "Interval container update" messages to debug log level [\#194](https://github.com/pyouroboros/ouroboros/issues/194) 173 | - \[Feature Request\] Support for Swarm Services [\#178](https://github.com/pyouroboros/ouroboros/issues/178) 174 | - Add Warning for label\_enable not set while using labels\_only [\#202](https://github.com/pyouroboros/ouroboros/pull/202) ([larsderidder](https://github.com/larsderidder)) 175 | 176 | **Fixed bugs:** 177 | 178 | - Change depends\_on logic [\#198](https://github.com/pyouroboros/ouroboros/issues/198) 179 | - Containers relying upon network namespace of a container that gets updated breaks when the parent container is recreated [\#197](https://github.com/pyouroboros/ouroboros/issues/197) 180 | - Exception when trying to update container with complex compose networks [\#196](https://github.com/pyouroboros/ouroboros/issues/196) 181 | - Problem with network IPv4 address carry-over [\#193](https://github.com/pyouroboros/ouroboros/issues/193) 182 | - Monitor Ignored Re-Address + jenkins cleanup [\#191](https://github.com/pyouroboros/ouroboros/pull/191) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 183 | 184 | **Closed issues:** 185 | 186 | - Remove legacy --latest [\#206](https://github.com/pyouroboros/ouroboros/issues/206) [[breaking change](https://github.com/pyouroboros/ouroboros/labels/breaking%20change)] [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] 187 | - Add environment variables in Wiki [\#203](https://github.com/pyouroboros/ouroboros/issues/203) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] 188 | - Slack notifications via webhook not working [\#187](https://github.com/pyouroboros/ouroboros/issues/187) 189 | 190 | **Other Pull Requests** 191 | 192 | - v1.2.0 Merge [\#208](https://github.com/pyouroboros/ouroboros/pull/208) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 193 | - v1.2.0 to develop [\#207](https://github.com/pyouroboros/ouroboros/pull/207) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 194 | - Patch/tag bug [\#205](https://github.com/pyouroboros/ouroboros/pull/205) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 195 | - Patch/group 5 [\#201](https://github.com/pyouroboros/ouroboros/pull/201) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 196 | - Fix bug in user defined network detection [\#200](https://github.com/pyouroboros/ouroboros/pull/200) ([nightvisi0n](https://github.com/nightvisi0n)) 197 | - Adjust apscheduler logger [\#199](https://github.com/pyouroboros/ouroboros/pull/199) ([circa10a](https://github.com/circa10a)) 198 | - Carry over network config [\#195](https://github.com/pyouroboros/ouroboros/pull/195) ([nightvisi0n](https://github.com/nightvisi0n)) 199 | - Jenkins tweaks [\#192](https://github.com/pyouroboros/ouroboros/pull/192) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 200 | - Swarm + Jenkins [\#188](https://github.com/pyouroboros/ouroboros/pull/188) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 201 | 202 | ## [1.1.2](https://github.com/pyouroboros/ouroboros/tree/1.1.2) (2019-02-02) 203 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.1.1...1.1.2) 204 | 205 | **Fixed bugs:** 206 | 207 | - No default timezone [\#176](https://github.com/pyouroboros/ouroboros/issues/176) 208 | 209 | **Closed issues:** 210 | 211 | - cron documentation example update [\#182](https://github.com/pyouroboros/ouroboros/issues/182) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] 212 | 213 | **Other Pull Requests** 214 | 215 | - v1.1.2 Merge [\#186](https://github.com/pyouroboros/ouroboros/pull/186) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 216 | - v1.1.2 to develop [\#183](https://github.com/pyouroboros/ouroboros/pull/183) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 217 | - Fix default timezone [\#177](https://github.com/pyouroboros/ouroboros/pull/177) ([circa10a](https://github.com/circa10a)) 218 | 219 | ## [1.1.1](https://github.com/pyouroboros/ouroboros/tree/1.1.1) (2019-02-01) 220 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.1.0...1.1.1) 221 | 222 | **Implemented enhancements:** 223 | 224 | - Support for adding an identifier \(hostname?\) to notifications [\#158](https://github.com/pyouroboros/ouroboros/issues/158) 225 | - Influx config data + ocd cleanup [\#162](https://github.com/pyouroboros/ouroboros/pull/162) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 226 | - add cli arg for cron [\#157](https://github.com/pyouroboros/ouroboros/pull/157) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 227 | 228 | **Fixed bugs:** 229 | 230 | - Ouroboros does not respect MONITOR= [\#166](https://github.com/pyouroboros/ouroboros/issues/166) 231 | - Docker TLS over TCP connections [\#154](https://github.com/pyouroboros/ouroboros/issues/154) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] 232 | - Patch/group 4 [\#169](https://github.com/pyouroboros/ouroboros/pull/169) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 233 | - Recheck properly for only non lists [\#164](https://github.com/pyouroboros/ouroboros/pull/164) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 234 | - add some missing passthrough info for restart [\#163](https://github.com/pyouroboros/ouroboros/pull/163) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 235 | 236 | **Other Pull Requests** 237 | 238 | - v1.1.1 Merge [\#173](https://github.com/pyouroboros/ouroboros/pull/173) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 239 | - v1.1.1 to develop [\#172](https://github.com/pyouroboros/ouroboros/pull/172) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 240 | - Patch/group 3 [\#167](https://github.com/pyouroboros/ouroboros/pull/167) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 241 | - Add hostname to the notifications [\#161](https://github.com/pyouroboros/ouroboros/pull/161) ([tlkamp](https://github.com/tlkamp)) 242 | - Patch/group 2 [\#155](https://github.com/pyouroboros/ouroboros/pull/155) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 243 | 244 | ## [1.1.0](https://github.com/pyouroboros/ouroboros/tree/1.1.0) (2019-01-26) 245 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.0.0...1.1.0) 246 | 247 | **Implemented enhancements:** 248 | 249 | - Notification via Telegram [\#146](https://github.com/pyouroboros/ouroboros/issues/146) 250 | - Add flag to allow a labels\_only condition [\#142](https://github.com/pyouroboros/ouroboros/issues/142) 251 | - DRY\_RUN flag [\#140](https://github.com/pyouroboros/ouroboros/issues/140) 252 | - Notification on startup [\#138](https://github.com/pyouroboros/ouroboros/issues/138) 253 | - Start/Stop containers in sequence [\#106](https://github.com/pyouroboros/ouroboros/issues/106) 254 | - Refactor/notifications with apprise [\#151](https://github.com/pyouroboros/ouroboros/pull/151) [[breaking change](https://github.com/pyouroboros/ouroboros/labels/breaking%20change)] [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 255 | 256 | **Fixed bugs:** 257 | 258 | - Catch invalid docker socket config [\#148](https://github.com/pyouroboros/ouroboros/issues/148) 259 | - Explicitly Define true/false [\#141](https://github.com/pyouroboros/ouroboros/issues/141) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] 260 | 261 | **Other Pull Requests** 262 | 263 | - v1.1.0 Merge [\#153](https://github.com/pyouroboros/ouroboros/pull/153) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 264 | - v1.1.0 to develop [\#152](https://github.com/pyouroboros/ouroboros/pull/152) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 265 | - Patch/group 1 [\#150](https://github.com/pyouroboros/ouroboros/pull/150) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 266 | - Add volume for docker socket path [\#144](https://github.com/pyouroboros/ouroboros/pull/144) ([mauvehed](https://github.com/mauvehed)) 267 | 268 | ## [1.0.0](https://github.com/pyouroboros/ouroboros/tree/1.0.0) (2019-01-23) 269 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.6.0...1.0.0) 270 | 271 | **Implemented enhancements:** 272 | 273 | - Stop containers with alternate signal [\#107](https://github.com/pyouroboros/ouroboros/issues/107) 274 | - Docker Socket secure connections [\#105](https://github.com/pyouroboros/ouroboros/issues/105) 275 | - Selectively monitor containers with label [\#104](https://github.com/pyouroboros/ouroboros/issues/104) 276 | - Allow stop-signal label [\#133](https://github.com/pyouroboros/ouroboros/pull/133) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 277 | - Docker TLS Verify option [\#132](https://github.com/pyouroboros/ouroboros/pull/132) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 278 | - add label priority feature for watch/ignore. Addresses \#104 [\#121](https://github.com/pyouroboros/ouroboros/pull/121) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 279 | 280 | **Fixed bugs:** 281 | 282 | - Unexpected docker API causes program to quit ‘500 Server Error: Internal Server Error’ [\#130](https://github.com/pyouroboros/ouroboros/issues/130) 283 | - Error tag handling under the registry with port [\#129](https://github.com/pyouroboros/ouroboros/issues/129) 284 | - a fatal error when none tag image [\#122](https://github.com/pyouroboros/ouroboros/issues/122) 285 | - Bug/ignore logic [\#135](https://github.com/pyouroboros/ouroboros/pull/135) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 286 | - Bug/registry logic [\#131](https://github.com/pyouroboros/ouroboros/pull/131) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 287 | - catch no tags in get\_running [\#124](https://github.com/pyouroboros/ouroboros/pull/124) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 288 | - fixed logic for latest vs develop, and added -f to specify file [\#119](https://github.com/pyouroboros/ouroboros/pull/119) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 289 | 290 | **Closed issues:** 291 | 292 | - Missing docker-compose.yml from documentation [\#120](https://github.com/pyouroboros/ouroboros/issues/120) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] 293 | - Wiki usage docs reference old argument names [\#115](https://github.com/pyouroboros/ouroboros/issues/115) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] 294 | 295 | **Other Pull Requests** 296 | 297 | - v1.0.0 Merge [\#137](https://github.com/pyouroboros/ouroboros/pull/137) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 298 | - v1.0.0 to develop [\#136](https://github.com/pyouroboros/ouroboros/pull/136) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 299 | - Clean old legacy files [\#134](https://github.com/pyouroboros/ouroboros/pull/134) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 300 | - Cleanup/qemu logic [\#128](https://github.com/pyouroboros/ouroboros/pull/128) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 301 | - fix readme wording for monitoring remote hosts [\#126](https://github.com/pyouroboros/ouroboros/pull/126) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] ([circa10a](https://github.com/circa10a)) 302 | 303 | ## [0.6.0](https://github.com/pyouroboros/ouroboros/tree/0.6.0) (2019-01-17) 304 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.5.0...0.6.0) 305 | 306 | **Implemented enhancements:** 307 | 308 | - Support multi-architecture Docker images [\#78](https://github.com/pyouroboros/ouroboros/issues/78) 309 | - Mail notification [\#59](https://github.com/pyouroboros/ouroboros/issues/59) 310 | - Multi architecture docker [\#110](https://github.com/pyouroboros/ouroboros/pull/110) ([circa10a](https://github.com/circa10a)) 311 | - added logo to readme [\#109](https://github.com/pyouroboros/ouroboros/pull/109) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 312 | - Feature/ouroboros self\_update [\#103](https://github.com/pyouroboros/ouroboros/pull/103) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 313 | - add version cli arg [\#100](https://github.com/pyouroboros/ouroboros/pull/100) ([circa10a](https://github.com/circa10a)) 314 | - added email notifications. Addresses \#59 [\#97](https://github.com/pyouroboros/ouroboros/pull/97) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 315 | - Documentation [\#96](https://github.com/pyouroboros/ouroboros/pull/96) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 316 | 317 | **Fixed bugs:** 318 | 319 | - Ignore not working as expected [\#98](https://github.com/pyouroboros/ouroboros/issues/98) 320 | - specify for specificity! [\#114](https://github.com/pyouroboros/ouroboros/pull/114) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 321 | - manifesting failures [\#113](https://github.com/pyouroboros/ouroboros/pull/113) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 322 | - sigh. [\#112](https://github.com/pyouroboros/ouroboros/pull/112) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 323 | - Multiarch/fine tuning [\#111](https://github.com/pyouroboros/ouroboros/pull/111) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 324 | - catch index error and account for shared images, x [\#102](https://github.com/pyouroboros/ouroboros/pull/102) ([circa10a](https://github.com/circa10a)) 325 | - add monitor/ignore to list sanity check. Fixes \#98 [\#99](https://github.com/pyouroboros/ouroboros/pull/99) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 326 | 327 | **Other Pull Requests** 328 | 329 | - v0.6.0 to develop [\#118](https://github.com/pyouroboros/ouroboros/pull/118) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 330 | - v0.6.0 Merge [\#117](https://github.com/pyouroboros/ouroboros/pull/117) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 331 | - add changelog formatting and fix all labels going back to 1 [\#116](https://github.com/pyouroboros/ouroboros/pull/116) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 332 | 333 | ## [0.5.0](https://github.com/pyouroboros/ouroboros/tree/0.5.0) (2019-01-13) 334 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.4.3...0.5.0) 335 | 336 | **Implemented enhancements:** 337 | 338 | - Auto discover slack/discord notifications in WEBHOOK\_URLS [\#83](https://github.com/pyouroboros/ouroboros/issues/83) 339 | - Add to schedule logic run now [\#75](https://github.com/pyouroboros/ouroboros/issues/75) 340 | - add pushover functionality. Finishes other half of \#80 [\#93](https://github.com/pyouroboros/ouroboros/pull/93) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 341 | - add keep\_alive url for healthchecks. Addresses half of \#80 [\#89](https://github.com/pyouroboros/ouroboros/pull/89) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 342 | - changed webhook json to auto-deciding + fixed RUN\_ONCE no underscore [\#86](https://github.com/pyouroboros/ouroboros/pull/86) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 343 | - Refactor [\#79](https://github.com/pyouroboros/ouroboros/pull/79) ([DirtyCajunRice](https://github.com/DirtyCajunRice)) 344 | 345 | **Fixed bugs:** 346 | 347 | - Fix log level case sensitivity [\#82](https://github.com/pyouroboros/ouroboros/issues/82) 348 | - Invalid URL 'h': No schema supplied. Perhaps you meant http://h? [\#76](https://github.com/pyouroboros/ouroboros/issues/76) 349 | - Installation via pip fails [\#73](https://github.com/pyouroboros/ouroboros/issues/73) 350 | - Added try except [\#95](https://github.com/pyouroboros/ouroboros/pull/95) ([circa10a](https://github.com/circa10a)) 351 | - Fix dockerfile [\#92](https://github.com/pyouroboros/ouroboros/pull/92) ([circa10a](https://github.com/circa10a)) 352 | - use ouroboros script in dockerfile [\#91](https://github.com/pyouroboros/ouroboros/pull/91) ([circa10a](https://github.com/circa10a)) 353 | - fix deploy script to push git tags [\#90](https://github.com/pyouroboros/ouroboros/pull/90) ([circa10a](https://github.com/circa10a)) 354 | - change pypi travis username [\#88](https://github.com/pyouroboros/ouroboros/pull/88) ([circa10a](https://github.com/circa10a)) 355 | - install flake8 for travis, run on appropriate directories [\#87](https://github.com/pyouroboros/ouroboros/pull/87) ([circa10a](https://github.com/circa10a)) 356 | - Removed old test related items, removed the need for duplicate bin sc… [\#85](https://github.com/pyouroboros/ouroboros/pull/85) ([circa10a](https://github.com/circa10a)) 357 | - change loglevel to use upper\(\) [\#84](https://github.com/pyouroboros/ouroboros/pull/84) ([circa10a](https://github.com/circa10a)) 358 | - Prometheus bind fix, org rename [\#81](https://github.com/pyouroboros/ouroboros/pull/81) ([circa10a](https://github.com/circa10a)) 359 | 360 | ## [0.4.3](https://github.com/pyouroboros/ouroboros/tree/0.4.3) (2019-01-09) 361 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.4.2...0.4.3) 362 | 363 | **Implemented enhancements:** 364 | 365 | - grafana to metrics/prometheus endpoint [\#74](https://github.com/pyouroboros/ouroboros/issues/74) 366 | - add aarch64 docker image [\#77](https://github.com/pyouroboros/ouroboros/pull/77) ([circa10a](https://github.com/circa10a)) 367 | 368 | ## [0.4.2](https://github.com/pyouroboros/ouroboros/tree/0.4.2) (2019-01-08) 369 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.4.1...0.4.2) 370 | 371 | **Implemented enhancements:** 372 | 373 | - Add autopep8 to the pre-merge checks [\#30](https://github.com/pyouroboros/ouroboros/issues/30) 374 | 375 | ## [0.4.1](https://github.com/pyouroboros/ouroboros/tree/0.4.1) (2018-12-30) 376 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.4.0...0.4.1) 377 | 378 | **Implemented enhancements:** 379 | 380 | - Pre merge code quality checks [\#72](https://github.com/pyouroboros/ouroboros/pull/72) ([circa10a](https://github.com/circa10a)) 381 | 382 | ## [0.4.0](https://github.com/pyouroboros/ouroboros/tree/0.4.0) (2018-12-30) 383 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.7...0.4.0) 384 | 385 | **Implemented enhancements:** 386 | 387 | - Slack notification [\#61](https://github.com/pyouroboros/ouroboros/issues/61) 388 | - Webhook notifications [\#71](https://github.com/pyouroboros/ouroboros/pull/71) ([circa10a](https://github.com/circa10a)) 389 | 390 | ## [0.3.7](https://github.com/pyouroboros/ouroboros/tree/0.3.7) (2018-12-26) 391 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.6...0.3.7) 392 | 393 | **Implemented enhancements:** 394 | 395 | - Timezone Support [\#68](https://github.com/pyouroboros/ouroboros/issues/68) 396 | - Add output to log at container start [\#66](https://github.com/pyouroboros/ouroboros/issues/66) 397 | - Enable Timezone Configuration [\#69](https://github.com/pyouroboros/ouroboros/pull/69) ([circa10a](https://github.com/circa10a)) 398 | 399 | ## [0.3.6](https://github.com/pyouroboros/ouroboros/tree/0.3.6) (2018-12-21) 400 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.5...0.3.6) 401 | 402 | **Implemented enhancements:** 403 | 404 | - print ouroboros configuration on startup [\#67](https://github.com/pyouroboros/ouroboros/pull/67) ([circa10a](https://github.com/circa10a)) 405 | 406 | ## [0.3.5](https://github.com/pyouroboros/ouroboros/tree/0.3.5) (2018-12-20) 407 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.4...0.3.5) 408 | 409 | **Implemented enhancements:** 410 | 411 | - Raspberry Pi compatible docker image [\#62](https://github.com/pyouroboros/ouroboros/issues/62) 412 | - Scheduling docs [\#65](https://github.com/pyouroboros/ouroboros/pull/65) ([circa10a](https://github.com/circa10a)) 413 | 414 | ## [0.3.4](https://github.com/pyouroboros/ouroboros/tree/0.3.4) (2018-12-19) 415 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.3...0.3.4) 416 | 417 | **Implemented enhancements:** 418 | 419 | - Rpi docker image [\#64](https://github.com/pyouroboros/ouroboros/pull/64) ([circa10a](https://github.com/circa10a)) 420 | 421 | ## [0.3.3](https://github.com/pyouroboros/ouroboros/tree/0.3.3) (2018-11-29) 422 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.2...0.3.3) 423 | 424 | **Implemented enhancements:** 425 | 426 | - add docs, bump version [\#58](https://github.com/pyouroboros/ouroboros/pull/58) ([circa10a](https://github.com/circa10a)) 427 | 428 | **Fixed bugs:** 429 | 430 | - Problem accessing private registry [\#55](https://github.com/pyouroboros/ouroboros/issues/55) 431 | 432 | **Closed issues:** 433 | 434 | - Q: Add config file? [\#46](https://github.com/pyouroboros/ouroboros/issues/46) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] 435 | 436 | ## [0.3.2](https://github.com/pyouroboros/ouroboros/tree/0.3.2) (2018-11-28) 437 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.1...0.3.2) 438 | 439 | **Fixed bugs:** 440 | 441 | - unrecognized arguments [\#52](https://github.com/pyouroboros/ouroboros/issues/52) 442 | - Fix config json [\#56](https://github.com/pyouroboros/ouroboros/pull/56) ([circa10a](https://github.com/circa10a)) 443 | 444 | ## [0.3.1](https://github.com/pyouroboros/ouroboros/tree/0.3.1) (2018-11-16) 445 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.0...0.3.1) 446 | 447 | **Implemented enhancements:** 448 | 449 | - Add Prometheus endpoint [\#23](https://github.com/pyouroboros/ouroboros/issues/23) [[hacktoberfest](https://github.com/pyouroboros/ouroboros/labels/hacktoberfest)] 450 | 451 | **Fixed bugs:** 452 | 453 | - fix bind address bug [\#53](https://github.com/pyouroboros/ouroboros/pull/53) ([circa10a](https://github.com/circa10a)) 454 | 455 | ## [0.3.0](https://github.com/pyouroboros/ouroboros/tree/0.3.0) (2018-11-15) 456 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.2.3...0.3.0) 457 | 458 | **Implemented enhancements:** 459 | 460 | - Q: continue to update to latest or same tag [\#43](https://github.com/pyouroboros/ouroboros/issues/43) 461 | - Metrics [\#51](https://github.com/pyouroboros/ouroboros/pull/51) ([circa10a](https://github.com/circa10a)) 462 | - Disable pip cache in Dockerfile [\#50](https://github.com/pyouroboros/ouroboros/pull/50) ([Strayer](https://github.com/Strayer)) 463 | 464 | ## [0.2.3](https://github.com/pyouroboros/ouroboros/tree/0.2.3) (2018-11-08) 465 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.2.2...0.2.3) 466 | 467 | **Implemented enhancements:** 468 | 469 | - Keep tags [\#48](https://github.com/pyouroboros/ouroboros/pull/48) ([circa10a](https://github.com/circa10a)) 470 | 471 | ## [0.2.2](https://github.com/pyouroboros/ouroboros/tree/0.2.2) (2018-11-03) 472 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.2.1...0.2.2) 473 | 474 | **Implemented enhancements:** 475 | 476 | - Add ability to ignore select containers [\#35](https://github.com/pyouroboros/ouroboros/issues/35) 477 | - Ignore containers [\#45](https://github.com/pyouroboros/ouroboros/pull/45) ([tlkamp](https://github.com/tlkamp)) 478 | - Update setup.py, travis param [\#42](https://github.com/pyouroboros/ouroboros/pull/42) ([circa10a](https://github.com/circa10a)) 479 | 480 | ## [0.2.1](https://github.com/pyouroboros/ouroboros/tree/0.2.1) (2018-10-28) 481 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.1.3...0.2.1) 482 | 483 | **Implemented enhancements:** 484 | 485 | - Option precedence [\#32](https://github.com/pyouroboros/ouroboros/issues/32) 486 | - Add ouroboros to the user's path automagically [\#28](https://github.com/pyouroboros/ouroboros/issues/28) 487 | - Deploy to Pypi [\#41](https://github.com/pyouroboros/ouroboros/pull/41) ([circa10a](https://github.com/circa10a)) 488 | - Add setup.py [\#40](https://github.com/pyouroboros/ouroboros/pull/40) ([tlkamp](https://github.com/tlkamp)) 489 | - change branch to master [\#39](https://github.com/pyouroboros/ouroboros/pull/39) ([circa10a](https://github.com/circa10a)) 490 | - Move api client out of cli.py [\#38](https://github.com/pyouroboros/ouroboros/pull/38) ([tlkamp](https://github.com/tlkamp)) 491 | - Handle the exceptions better in cli.py [\#36](https://github.com/pyouroboros/ouroboros/pull/36) ([tlkamp](https://github.com/tlkamp)) 492 | 493 | **Closed issues:** 494 | 495 | - \[question\] network\_mode: "service:XXX" ? [\#33](https://github.com/pyouroboros/ouroboros/issues/33) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] 496 | 497 | **Other Pull Requests** 498 | 499 | - Remove global hosts variable [\#37](https://github.com/pyouroboros/ouroboros/pull/37) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([tlkamp](https://github.com/tlkamp)) 500 | - update docs [\#34](https://github.com/pyouroboros/ouroboros/pull/34) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] ([circa10a](https://github.com/circa10a)) 501 | 502 | ## [0.1.3](https://github.com/pyouroboros/ouroboros/tree/0.1.3) (2018-10-25) 503 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.1.2...0.1.3) 504 | 505 | **Implemented enhancements:** 506 | 507 | - Make CLI expose fewer globals, formatting [\#31](https://github.com/pyouroboros/ouroboros/pull/31) ([tlkamp](https://github.com/tlkamp)) 508 | 509 | ## [0.1.2](https://github.com/pyouroboros/ouroboros/tree/0.1.2) (2018-10-24) 510 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.1.1...0.1.2) 511 | 512 | **Implemented enhancements:** 513 | 514 | - Rewrite script to use vendor packages if possible [\#25](https://github.com/pyouroboros/ouroboros/pull/25) ([dannysauer](https://github.com/dannysauer)) 515 | - Improve URL matching Regex [\#24](https://github.com/pyouroboros/ouroboros/pull/24) ([dannysauer](https://github.com/dannysauer)) 516 | - Add environment files to the project for those working with Conda [\#22](https://github.com/pyouroboros/ouroboros/pull/22) ([tlkamp](https://github.com/tlkamp)) 517 | 518 | **Other Pull Requests** 519 | 520 | - regex changes, cli cleanup. [\#29](https://github.com/pyouroboros/ouroboros/pull/29) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a)) 521 | - Clean up cli.py [\#27](https://github.com/pyouroboros/ouroboros/pull/27) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([tlkamp](https://github.com/tlkamp)) 522 | 523 | ## [0.1.1](https://github.com/pyouroboros/ouroboros/tree/0.1.1) (2018-10-21) 524 | [Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.1.0...0.1.1) 525 | 526 | ## [0.1.0](https://github.com/pyouroboros/ouroboros/tree/0.1.0) (2018-10-21) 527 | **Implemented enhancements:** 528 | 529 | - account for environment variables [\#19](https://github.com/pyouroboros/ouroboros/issues/19) 530 | - Support private repos [\#10](https://github.com/pyouroboros/ouroboros/issues/10) 531 | - Deploy to pypi [\#5](https://github.com/pyouroboros/ouroboros/issues/5) 532 | - Create travis build [\#4](https://github.com/pyouroboros/ouroboros/issues/4) 533 | - Rewrite new container class [\#3](https://github.com/pyouroboros/ouroboros/issues/3) 534 | - Write Unit Tests [\#2](https://github.com/pyouroboros/ouroboros/issues/2) 535 | - Add CLI Args [\#1](https://github.com/pyouroboros/ouroboros/issues/1) 536 | - added support for private registries [\#12](https://github.com/pyouroboros/ouroboros/pull/12) ([circa10a](https://github.com/circa10a)) 537 | - Torpus cli args [\#11](https://github.com/pyouroboros/ouroboros/pull/11) ([Torpus](https://github.com/Torpus)) 538 | - single client [\#9](https://github.com/pyouroboros/ouroboros/pull/9) ([circa10a](https://github.com/circa10a)) 539 | - the less code the better [\#8](https://github.com/pyouroboros/ouroboros/pull/8) ([circa10a](https://github.com/circa10a)) 540 | - Initial stuff [\#6](https://github.com/pyouroboros/ouroboros/pull/6) ([circa10a](https://github.com/circa10a)) 541 | 542 | **Closed issues:** 543 | 544 | - Create good docs [\#7](https://github.com/pyouroboros/ouroboros/issues/7) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] 545 | 546 | **Other Pull Requests** 547 | 548 | - Docs [\#21](https://github.com/pyouroboros/ouroboros/pull/21) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] ([circa10a](https://github.com/circa10a)) 549 | - Tests [\#20](https://github.com/pyouroboros/ouroboros/pull/20) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a)) 550 | - Add travis [\#18](https://github.com/pyouroboros/ouroboros/pull/18) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a)) 551 | - Tests [\#17](https://github.com/pyouroboros/ouroboros/pull/17) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a)) 552 | - Tests [\#16](https://github.com/pyouroboros/ouroboros/pull/16) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a)) 553 | - Tests [\#15](https://github.com/pyouroboros/ouroboros/pull/15) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a)) 554 | - Tests [\#14](https://github.com/pyouroboros/ouroboros/pull/14) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a)) 555 | - Tests [\#13](https://github.com/pyouroboros/ouroboros/pull/13) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a)) 556 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.13.2-alpine 2 | 3 | ENV TZ=UTC 4 | 5 | WORKDIR /app 6 | 7 | COPY /setup.py /ouroboros /README.md /app/ 8 | 9 | COPY /requirements.txt /app/ 10 | 11 | RUN apk update && apk upgrade \ 12 | && apk add --no-cache --virtual .build-deps gcc build-base linux-headers \ 13 | ca-certificates musl-dev python3-dev libffi-dev openssl-dev cargo \ 14 | && pip install --upgrade pip \ 15 | && pip install --upgrade setuptools \ 16 | && pip install --no-cache-dir -r requirements.txt \ 17 | && apk del .build-deps 18 | 19 | COPY /locales /app/locales 20 | 21 | COPY /pyouroboros /app/pyouroboros 22 | 23 | RUN pip install --no-cache-dir . 24 | 25 | RUN mkdir /app/pyouroboros/hooks 26 | 27 | VOLUME /app/pyouroboros/hooks 28 | 29 | ENTRYPOINT ["ouroboros"] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Caleb Lemoine 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Ouroboros Logo 3 | 4 | Automatically update your running Docker containers to the latest available image. 5 | 6 | The de-facto standard for docker update automation 7 | 8 | Forked from the original at https://github.com/pyouroboros/ouroboros/ 9 | 10 | ## Overview 11 | 12 | Ouroboros will monitor (all or specified) running docker containers and update them to the (latest or tagged) available image in the remote registry. The updated container uses the same tag and parameters that were used when the container was first created such as volume/bind mounts, docker network connections, environment variables, restart policies, entrypoints, commands, etc. 13 | 14 | - Push your image to your registry and simply wait your defined interval for ouroboros to find the new image and redeploy your container autonomously. 15 | - Notify you via many platforms courtesy of [Apprise](https://github.com/caronc/apprise) 16 | - Serve metrics for trend monitoring (Currently: Prometheus/Influxdb) 17 | - Limit your server ssh access 18 | - `ssh -i key server.domainname "docker pull ... && docker run ..."` is for scrubs 19 | - `docker-compose pull && docker-compose up -d` is for fancier scrubs 20 | 21 | ## Getting Started 22 | 23 | More detailed usage and configuration can be found on [the wiki](https://github.com/gmt2001/ouroboros/wiki). 24 | 25 | ### Docker 26 | 27 | Ouroboros is deployed via docker image like so: 28 | 29 | ```bash 30 | docker run -d --name ouroboros \ 31 | -v /var/run/docker.sock:/var/run/docker.sock \ 32 | ghcr.io/gmt2001/ouroboros 33 | ``` 34 | 35 | > This image is compatible with amd64, arm64, and arm/v7 CPU architectures 36 | 37 | or via `docker-compose`: 38 | 39 | [Official Example](docker-compose.yml) 40 | 41 | ## Examples 42 | Per-command and scenario examples can be found in the [wiki](https://github.com/gmt2001/ouroboros/wiki/Usage) 43 | 44 | ## Contributing 45 | 46 | All contributions are welcome! Contributing guidelines are in the works 47 | -------------------------------------------------------------------------------- /assets/ouroboros_logo_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmt2001/ouroboros/4c1719ddafd9af4490d004358016d4d1bd5ff7b6/assets/ouroboros_logo_icon_256.png -------------------------------------------------------------------------------- /assets/ouroboros_logo_icon_72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmt2001/ouroboros/4c1719ddafd9af4490d004358016d4d1bd5ff7b6/assets/ouroboros_logo_icon_72.png -------------------------------------------------------------------------------- /assets/ouroboros_logo_primary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmt2001/ouroboros/4c1719ddafd9af4490d004358016d4d1bd5ff7b6/assets/ouroboros_logo_primary.png -------------------------------------------------------------------------------- /assets/ouroboros_logo_primary_cropped.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmt2001/ouroboros/4c1719ddafd9af4490d004358016d4d1bd5ff7b6/assets/ouroboros_logo_primary_cropped.jpg -------------------------------------------------------------------------------- /assets/ouroboros_logo_primary_cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmt2001/ouroboros/4c1719ddafd9af4490d004358016d4d1bd5ff7b6/assets/ouroboros_logo_primary_cropped.png -------------------------------------------------------------------------------- /assets/ouroboros_logo_primary_discord.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmt2001/ouroboros/4c1719ddafd9af4490d004358016d4d1bd5ff7b6/assets/ouroboros_logo_primary_discord.jpg -------------------------------------------------------------------------------- /assets/ouroboros_logo_primary_long_cropped.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmt2001/ouroboros/4c1719ddafd9af4490d004358016d4d1bd5ff7b6/assets/ouroboros_logo_primary_long_cropped.jpg -------------------------------------------------------------------------------- /assets/ouroboros_logo_primary_smaller_square_crop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmt2001/ouroboros/4c1719ddafd9af4490d004358016d4d1bd5ff7b6/assets/ouroboros_logo_primary_smaller_square_crop.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | ouroboros: 4 | container_name: ouroboros 5 | hostname: ouroboros 6 | image: ghcr.io/gmt2001/ouroboros 7 | environment: 8 | - CLEANUP=true 9 | - INTERVAL=300 10 | - LOG_LEVEL=info 11 | - SELF_UPDATE=true 12 | - IGNORE=mongo influxdb postgres mariadb 13 | - TZ=America/New_York 14 | - LANGUAGE=en 15 | restart: unless-stopped 16 | volumes: 17 | - /var/run/docker.sock:/var/run/docker.sock 18 | - /app/pyouroboros/hooks:/app/pyouroboros/hooks 19 | -------------------------------------------------------------------------------- /locales/es_ES/LC_MESSAGES/notifiers.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmt2001/ouroboros/4c1719ddafd9af4490d004358016d4d1bd5ff7b6/locales/es_ES/LC_MESSAGES/notifiers.mo -------------------------------------------------------------------------------- /locales/es_ES/LC_MESSAGES/notifiers.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # FIRST AUTHOR , YEAR. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PACKAGE VERSION\n" 8 | "POT-Creation-Date: 2021-03-02 21:54+0100\n" 9 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 10 | "Last-Translator: FULL NAME \n" 11 | "Language-Team: LANGUAGE \n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=UTF-8\n" 14 | "Content-Transfer-Encoding: 8bit\n" 15 | "Generated-By: pygettext.py 1.5\n" 16 | 17 | 18 | #: pyouroboros/notifiers.py:37 19 | msgid "Could not add notifier %s" 20 | msgstr "No se pudo agregar el notificador %s" 21 | 22 | #: pyouroboros/notifiers.py:44 23 | msgid "Ouroboros has started" 24 | msgstr "Ouroboros ha comenzado" 25 | 26 | #: pyouroboros/notifiers.py:46 27 | msgid "Host: %s" 28 | msgstr "Servidor: %s" 29 | 30 | #: pyouroboros/notifiers.py:51 31 | msgid "%Y-%m-%d %H:%M:%S" 32 | msgstr "%d/%m/%Y %H:%M:%S" 33 | 34 | #: pyouroboros/notifiers.py:47 35 | msgid "Time: %s" 36 | msgstr "Fecha: %s" 37 | 38 | #: pyouroboros/notifiers.py:48 39 | msgid "Next Run: %s" 40 | msgstr "Siguiente ejecución: %s" 41 | 42 | #: pyouroboros/notifiers.py:50 43 | msgid "Ouroboros has updated containers!" 44 | msgstr "!Ouroboros ha actualizado contenedores!" 45 | 46 | #: pyouroboros/notifiers.py:52 47 | msgid "Host/Socket: %s / %s" 48 | msgstr "Servidor/Socket: %s / %s" 49 | 50 | #: pyouroboros/notifiers.py:53 51 | msgid "Containers Monitored: %d" 52 | msgstr "Contenedores monitorizados: %d" 53 | 54 | #: pyouroboros/notifiers.py:54 55 | msgid "Total Containers Updated: %d" 56 | msgstr "Total contenedores actualizados: %d" 57 | 58 | #: pyouroboros/notifiers.py:55 59 | msgid "Containers updated this pass: %d" 60 | msgstr "Contenedores actualizados en iteración: %d" 61 | 62 | #: pyouroboros/notifiers.py:59 63 | msgid "{} updated from {} to {}" 64 | msgstr "{} actualizado desde {} a {}" 65 | 66 | -------------------------------------------------------------------------------- /locales/es_ES/LC_MESSAGES/ouroboros.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmt2001/ouroboros/4c1719ddafd9af4490d004358016d4d1bd5ff7b6/locales/es_ES/LC_MESSAGES/ouroboros.mo -------------------------------------------------------------------------------- /locales/es_ES/LC_MESSAGES/ouroboros.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # FIRST AUTHOR , YEAR. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PACKAGE VERSION\n" 8 | "POT-Creation-Date: 2021-03-03 21:33+0100\n" 9 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 10 | "Last-Translator: FULL NAME \n" 11 | "Language-Team: LANGUAGE \n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=UTF-8\n" 14 | "Content-Transfer-Encoding: 8bit\n" 15 | "Generated-By: pygettext.py 1.5\n" 16 | 17 | 18 | #: pyouroboros/ouroboros.py:158 19 | msgid "Version: %s-%s" 20 | msgstr "Versión: %s-%s" 21 | 22 | #: pyouroboros/ouroboros.py:161 23 | msgid "Ouroboros configuration: %s" 24 | msgstr "Configuración de Ouroboros: %s" 25 | 26 | #: pyouroboros/ouroboros.py:177 27 | msgid "Run Once container update for %s" 28 | msgstr "Ejecutar una vez la actualización del contenedor para %s" 29 | 30 | #: pyouroboros/ouroboros.py:180 31 | msgid "Self Check for %s" 32 | msgstr "Autocomprobación de %s" 33 | 34 | #: pyouroboros/ouroboros.py:184 35 | msgid "Cron container update for %s" 36 | msgstr "Actualización de contenedor cron para %s" 37 | 38 | #: pyouroboros/ouroboros.py:196 39 | msgid "Initial run interval container update for %s" 40 | msgstr "Actualización del contenedor del intervalo de ejecución inicial para %s" 41 | 42 | #: pyouroboros/ouroboros.py:200 43 | msgid "Interval container update for %s" 44 | msgstr "Actualización de contenedor de intervalo para %s" 45 | 46 | #: pyouroboros/ouroboros.py:204 47 | msgid "Could not connect to socket %s. Check your config" 48 | msgstr "No se pudo conectar al socket %s. Comprueba tu configuración" 49 | 50 | #: pyouroboros/ouroboros.py:212 51 | msgid "%Y-%m-%d %H:%M:%S" 52 | msgstr "%d/%m/%Y %H:%M:%S" 53 | 54 | -------------------------------------------------------------------------------- /locales/notifiers.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # FIRST AUTHOR , YEAR. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PACKAGE VERSION\n" 8 | "POT-Creation-Date: 2021-03-03 21:07+0100\n" 9 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 10 | "Last-Translator: FULL NAME \n" 11 | "Language-Team: LANGUAGE \n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=UTF-8\n" 14 | "Content-Transfer-Encoding: 8bit\n" 15 | "Generated-By: pygettext.py 1.5\n" 16 | 17 | 18 | #: pyouroboros/notifiers.py:42 19 | msgid "Could not add notifier %s" 20 | msgstr "" 21 | 22 | #: pyouroboros/notifiers.py:49 23 | msgid "Ouroboros has started" 24 | msgstr "" 25 | 26 | #: pyouroboros/notifiers.py:51 27 | msgid "Host: %s" 28 | msgstr "" 29 | 30 | #: pyouroboros/notifiers.py:52 31 | msgid "Time: %s" 32 | msgstr "" 33 | 34 | #: pyouroboros/notifiers.py:53 35 | msgid "Next Run: %s" 36 | msgstr "" 37 | 38 | #: pyouroboros/notifiers.py:55 39 | msgid "Ouroboros has detected updates!" 40 | msgstr "" 41 | 42 | #: pyouroboros/notifiers.py:57 43 | msgid "Host/Socket: %s / %s" 44 | msgstr "" 45 | 46 | #: pyouroboros/notifiers.py:58 47 | msgid "Containers Monitored: %d" 48 | msgstr "" 49 | 50 | #: pyouroboros/notifiers.py:59 51 | msgid "Total Containers Updated: %d" 52 | msgstr "" 53 | 54 | #: pyouroboros/notifiers.py:63 55 | msgid "{} updated from {} to {}" 56 | msgstr "" 57 | 58 | #: pyouroboros/notifiers.py:71 59 | msgid "Ouroboros has updated containers!" 60 | msgstr "" 61 | 62 | #: pyouroboros/notifiers.py:76 63 | msgid "Containers updated this pass: %d" 64 | msgstr "" 65 | 66 | -------------------------------------------------------------------------------- /locales/ouroboros.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # FIRST AUTHOR , YEAR. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PACKAGE VERSION\n" 8 | "POT-Creation-Date: 2021-03-03 21:33+0100\n" 9 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 10 | "Last-Translator: FULL NAME \n" 11 | "Language-Team: LANGUAGE \n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=UTF-8\n" 14 | "Content-Transfer-Encoding: 8bit\n" 15 | "Generated-By: pygettext.py 1.5\n" 16 | 17 | 18 | #: pyouroboros/ouroboros.py:165 19 | msgid "Version: %s-%s" 20 | msgstr "" 21 | 22 | #: pyouroboros/ouroboros.py:168 23 | msgid "Ouroboros configuration: %s" 24 | msgstr "" 25 | 26 | #: pyouroboros/ouroboros.py:184 27 | msgid "Run Once container update for %s" 28 | msgstr "" 29 | 30 | #: pyouroboros/ouroboros.py:187 31 | msgid "Self Check for %s" 32 | msgstr "" 33 | 34 | #: pyouroboros/ouroboros.py:191 35 | msgid "Cron container update for %s" 36 | msgstr "" 37 | 38 | #: pyouroboros/ouroboros.py:203 39 | msgid "Initial run interval container update for %s" 40 | msgstr "" 41 | 42 | #: pyouroboros/ouroboros.py:207 43 | msgid "Interval container update for %s" 44 | msgstr "" 45 | 46 | #: pyouroboros/ouroboros.py:211 47 | msgid "Could not connect to socket %s. Check your config" 48 | msgstr "" 49 | 50 | -------------------------------------------------------------------------------- /ouroboros: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from pyouroboros.ouroboros import main 3 | # stub to allow cli call 4 | if __name__ == "__main__": 5 | main() 6 | -------------------------------------------------------------------------------- /ouroboros.pyproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | 2.0 6 | {d41e6627-82ce-4f97-b788-65fefa7ed519} 7 | 8 | setup.py 9 | 10 | . 11 | . 12 | {888888a0-9f3d-457c-b088-3a5042f75d52} 13 | Standard Python launcher 14 | 15 | 16 | 17 | 18 | 19 | 10.0 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /ouroboros.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.12.35521.163 d17.12 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "ouroboros", "ouroboros.pyproj", "{D41E6627-82CE-4F97-B788-65FEFA7ED519}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {D41E6627-82CE-4F97-B788-65FEFA7ED519}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {D41E6627-82CE-4F97-B788-65FEFA7ED519}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | EndGlobal 21 | -------------------------------------------------------------------------------- /pyouroboros/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = "custom" 2 | BRANCH = "main" 3 | -------------------------------------------------------------------------------- /pyouroboros/config.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | from logging import getLogger 3 | from pyouroboros.logger import BlacklistFilter 4 | 5 | 6 | class Config(object): 7 | options = ['INTERVAL', 'PROMETHEUS', 'DOCKER_SOCKETS', 'MONITOR', 'IGNORE', 'LOG_LEVEL', 'PROMETHEUS_ADDR', 8 | 'PROMETHEUS_PORT', 'NOTIFIERS', 'REPO_USER', 'REPO_PASS', 'CLEANUP', 'RUN_ONCE', 'CRON', 'GRACE', 9 | 'INFLUX_URL', 'INFLUX_PORT', 'INFLUX_USERNAME', 'INFLUX_PASSWORD', 'INFLUX_DATABASE', 'INFLUX_SSL', 10 | 'INFLUX_VERIFY_SSL', 'DATA_EXPORT', 'SELF_UPDATE', 'LABEL_ENABLE', 'DOCKER_TLS', 'LABELS_ONLY', 11 | 'DRY_RUN', 'MONITOR_ONLY', 'HOSTNAME', 'DOCKER_TLS_VERIFY', 'SWARM', 'SKIP_STARTUP_NOTIFICATIONS', 'LANGUAGE', 12 | 'TZ', 'CLEANUP_UNUSED_VOLUMES', 'DOCKER_TIMEOUT', 'LATEST_ONLY', 'SAVE_COUNTERS'] 13 | 14 | hostname = environ.get('HOSTNAME') 15 | interval = 300 16 | cron = None 17 | docker_sockets = 'unix://var/run/docker.sock' 18 | docker_tls = False 19 | docker_tls_verify = True 20 | docker_timeout = 60 21 | grace = 15 22 | swarm = False 23 | monitor = [] 24 | ignore = [] 25 | data_export = None 26 | log_level = 'info' 27 | cleanup = False 28 | cleanup_unused_volumes = False 29 | run_once = False 30 | dry_run = False 31 | monitor_only = False 32 | self_update = False 33 | label_enable = False 34 | labels_only = False 35 | latest_only = False 36 | language = 'en' 37 | tz = 'UTC' 38 | 39 | save_counters = False 40 | 41 | repo_user = None 42 | repo_pass = None 43 | auth_json = None 44 | 45 | prometheus = False 46 | prometheus_addr = '127.0.0.1' 47 | prometheus_port = 8000 48 | 49 | influx_url = '127.0.0.1' 50 | influx_port = 8086 51 | influx_ssl = False 52 | influx_verify_ssl = False 53 | influx_username = 'root' 54 | influx_password = 'root' 55 | influx_database = None 56 | 57 | notifiers = [] 58 | skip_startup_notifications = False 59 | 60 | def __init__(self, environment_vars, cli_args): 61 | self.cli_args = cli_args 62 | self.environment_vars = environment_vars 63 | self.filtered_strings = None 64 | 65 | self.logger = getLogger() 66 | self.parse() 67 | 68 | def config_blacklist(self): 69 | filtered_strings = [getattr(self, key.lower()) for key in Config.options 70 | if key.lower() in BlacklistFilter.blacklisted_keys] 71 | # Clear None values 72 | self.filtered_strings = list(filter(None, filtered_strings)) 73 | # take lists inside of list and append to list 74 | for index, value in enumerate(self.filtered_strings, 0): 75 | if isinstance(value, list): 76 | self.filtered_strings.extend(self.filtered_strings.pop(index)) 77 | self.filtered_strings.insert(index, self.filtered_strings[-1:][0]) 78 | # Added matching for ports 79 | ports = [string.split(':')[0] for string in self.filtered_strings if ':' in string] 80 | self.filtered_strings.extend(ports) 81 | # Added matching for tcp sockets. ConnectionPool ignores the tcp:// 82 | tcp_sockets = [string.split('//')[1] for string in self.filtered_strings if '//' in string] 83 | self.filtered_strings.extend(tcp_sockets) 84 | # Get JUST hostname from tcp//unix 85 | for socket in getattr(self, 'docker_sockets'): 86 | self.filtered_strings.append(socket.split('//')[1].split(':')[0]) 87 | 88 | for handler in self.logger.handlers: 89 | handler.addFilter(BlacklistFilter(set(self.filtered_strings))) 90 | 91 | def parse(self): 92 | for option in Config.options: 93 | if self.environment_vars.get(option): 94 | env_opt = self.environment_vars[option] 95 | if isinstance(env_opt, str): 96 | # Clean out quotes, both single/double and whitespace 97 | env_opt = env_opt.strip("'").strip('"').strip(' ') 98 | if option in ['INTERVAL', 'GRACE', 'PROMETHEUS_PORT', 'INFLUX_PORT', 'DOCKER_TIMEOUT']: 99 | try: 100 | opt = int(env_opt) 101 | setattr(self, option.lower(), opt) 102 | except ValueError as e: 103 | print(e) 104 | elif option in ['CLEANUP', 'RUN_ONCE', 'INFLUX_SSL', 'INFLUX_VERIFY_SSL', 'DRY_RUN', 'MONITOR_ONLY', 'SWARM', 105 | 'SELF_UPDATE', 'LABEL_ENABLE', 'DOCKER_TLS', 'LABELS_ONLY', 'DOCKER_TLS_VERIFY', 106 | 'SKIP_STARTUP_NOTIFICATIONS', 'CLEANUP_UNUSED_VOLUMES', 'LATEST_ONLY']: 107 | if env_opt.lower() in ['true', 'yes']: 108 | setattr(self, option.lower(), True) 109 | elif env_opt.lower() in ['false', 'no']: 110 | setattr(self, option.lower(), False) 111 | else: 112 | self.logger.error('%s is not true/yes, nor false/no for %s. Assuming %s', 113 | env_opt, option, getattr(self, option)) 114 | else: 115 | setattr(self, option.lower(), env_opt) 116 | elif vars(self.cli_args).get(option): 117 | setattr(self, option.lower(), vars(self.cli_args).get(option)) 118 | 119 | # Specific var changes 120 | if self.repo_user and self.repo_pass: 121 | self.auth_json = {'Username': self.repo_user, 'Password': self.repo_pass} 122 | 123 | if self.interval < 30: 124 | self.interval = 30 125 | 126 | if self.grace < 0: 127 | self.grace = None 128 | 129 | if self.labels_only and not self.label_enable: 130 | self.logger.warning('labels_only enabled but not in use without label_enable') 131 | 132 | for option in ['docker_sockets', 'notifiers', 'monitor', 'ignore']: 133 | if isinstance(getattr(self, option), str): 134 | string_list = getattr(self, option) 135 | setattr(self, option, [string for string in string_list.split(' ')]) 136 | 137 | # Config sanity checks 138 | if self.cron: 139 | cron_times = self.cron.strip().split(' ') 140 | if len(cron_times) != 5: 141 | self.logger.error("Cron must be in cron syntax. e.g. * * * * * (5 places). Ignoring and using interval") 142 | self.cron = None 143 | else: 144 | self.logger.info("Cron configuration is valid. Using Cron schedule %s", cron_times) 145 | self.cron = cron_times 146 | self.interval = None 147 | 148 | if self.data_export == 'influxdb' and not self.influx_database: 149 | self.logger.error("You need to specify an influx database if you want to export to influxdb. Disabling " 150 | "influxdb data export.") 151 | 152 | if self.data_export == 'prometheus' and self.self_update: 153 | self.logger.warning("If you bind a port to ouroboros, it will be lost when it updates itself.") 154 | 155 | if self.dry_run and not self.run_once: 156 | self.logger.warning("Dry run is designed to be ran with run once. Setting for you.") 157 | self.run_once = True 158 | 159 | # Remove default config that is not used for cleaner logs 160 | if self.data_export != 'prometheus': 161 | self.prometheus_addr, self.prometheus_port = None, None 162 | 163 | if self.data_export != 'influxdb': 164 | self.influx_url, self.influx_port, self.influx_username, self.influx_password = None, None, None, None 165 | 166 | self.config_blacklist() 167 | -------------------------------------------------------------------------------- /pyouroboros/dataexporters.py: -------------------------------------------------------------------------------- 1 | import prometheus_client 2 | import json 3 | from os import unlink 4 | from logging import getLogger 5 | from influxdb import InfluxDBClient 6 | from datetime import datetime, timezone 7 | from pathlib import Path 8 | from pyouroboros.helpers import get_exec_dir 9 | 10 | class DataManager(object): 11 | def __init__(self, config): 12 | self.config = config 13 | self.logger = getLogger() 14 | self.enabled = True 15 | 16 | self.monitored_containers = {} 17 | self.total_updated = {} 18 | 19 | self.prometheus = PrometheusExporter(self, config) if self.config.data_export == "prometheus" else None 20 | self.influx = InfluxClient(self, config) if self.config.data_export == "influxdb" else None 21 | 22 | def add(self, label, socket): 23 | if self.config.data_export == "prometheus" and self.enabled: 24 | self.prometheus.update(label, socket) 25 | 26 | elif self.config.data_export == "influxdb" and self.enabled: 27 | if label == "all": 28 | self.logger.debug("Total containers updated %s", self.total_updated[socket]) 29 | 30 | self.influx.write_points(label, socket) 31 | 32 | def set(self, socket): 33 | if self.config.data_export == "prometheus" and self.enabled: 34 | self.prometheus.set_monitored(socket) 35 | 36 | def save(self): 37 | if self.config.save_counters: 38 | fpath = Path(get_exec_dir() + '/hooks/datamanager.json') 39 | try: 40 | with open(fpath, 'w') as file: 41 | json.dump(self.total_updated, file) 42 | except: 43 | self.logger.debug('Unable to save JSON') 44 | 45 | def load(self): 46 | if self.config.save_counters: 47 | fpath = Path(get_exec_dir() + '/hooks/datamanager.json') 48 | try: 49 | with open(fpath, 'r') as file: 50 | self.total_updated = json.load(file) 51 | unlink(fpath) 52 | except: 53 | self.logger.debug('No JSON to load or unlink failed') 54 | 55 | 56 | class PrometheusExporter(object): 57 | def __init__(self, data_manager, config): 58 | self.config = config 59 | self.data_manager = data_manager 60 | self.http_server = prometheus_client.start_http_server( 61 | self.config.prometheus_port, 62 | addr=self.config.prometheus_addr 63 | ) 64 | self.updated_containers_counter = prometheus_client.Counter( 65 | 'containers_updated', 66 | 'Count of containers updated', 67 | ['socket', 'container'] 68 | ) 69 | self.monitored_containers_gauge = prometheus_client.Gauge( 70 | 'containers_being_monitored', 71 | 'Gauge of containers being monitored', 72 | ['socket'] 73 | ) 74 | self.updated_all_containers_gauge = prometheus_client.Gauge( 75 | 'all_containers_updated', 76 | 'Count of total updated', 77 | ['socket'] 78 | ) 79 | self.logger = getLogger() 80 | 81 | def set_monitored(self, socket): 82 | """Set number of containers being monitoring with a gauge""" 83 | self.monitored_containers_gauge.labels(socket=socket).set(self.data_manager.monitored_containers[socket]) 84 | self.logger.debug("Prometheus Exporter monitored containers gauge set to %s", 85 | self.data_manager.monitored_containers[socket]) 86 | 87 | def update(self, label, socket): 88 | """Set container update count based on label""" 89 | if label == "all": 90 | self.updated_all_containers_gauge.labels(socket=socket).set(self.data_manager.total_updated[socket]) 91 | else: 92 | self.updated_containers_counter.labels(socket=socket, container=label).inc() 93 | 94 | self.logger.debug("Prometheus Exporter container update counter incremented for %s", label) 95 | 96 | 97 | class InfluxClient(object): 98 | def __init__(self, data_manger, config): 99 | self.data_manager = data_manger 100 | self.config = config 101 | self.logger = getLogger() 102 | self.influx = InfluxDBClient( 103 | host=self.config.influx_url, 104 | port=self.config.influx_port, 105 | username=self.config.influx_username, 106 | password=self.config.influx_password, 107 | database=self.config.influx_database, 108 | ssl=self.config.influx_ssl, 109 | verify_ssl=self.config.influx_verify_ssl 110 | ) 111 | self.db_check() 112 | 113 | def db_check(self): 114 | database_dicts = self.influx.get_list_database() 115 | databases = [d['name'] for d in database_dicts] 116 | if self.config.influx_database in databases: 117 | self.logger.debug("Influxdb database existence check passed for %s", self.config.influx_database) 118 | else: 119 | self.logger.debug("Influxdb database existence failed for %s. Disabling exports.", 120 | self.config.influx_database) 121 | self.data_manager.enabled = False 122 | 123 | def write_points(self, label, socket): 124 | clean_socket = socket.split("//")[1] 125 | now = datetime.now(timezone.utc).astimezone().isoformat() 126 | influx_payload = [ 127 | { 128 | "measurement": "Ouroboros", 129 | "tags": {'socket': clean_socket}, 130 | "time": now, 131 | "fields": {} 132 | }, 133 | { 134 | "measurement": "Ouroboros", 135 | "tags": {'configuration': self.config.hostname}, 136 | "time": now, 137 | "fields": {key: (value if not isinstance(value, list) else ' '.join(value)) for key, value in 138 | vars(self.config).items() if key.upper() in self.config.options} 139 | } 140 | ] 141 | if label == "all": 142 | influx_payload[0]['tags']["type"] = "stats" 143 | influx_payload[0]['fields'] = { 144 | "monitored_containers": self.data_manager.monitored_containers[socket], 145 | "updated_count": self.data_manager.total_updated[socket] 146 | } 147 | else: 148 | influx_payload[0]['tags'].update( 149 | { 150 | "type": "container_update", 151 | "container": label 152 | } 153 | ) 154 | influx_payload[0]['fields'] = {"count": 1} 155 | 156 | self.logger.debug("Writing data to influxdb: %s", influx_payload) 157 | self.influx.write_points(influx_payload) 158 | -------------------------------------------------------------------------------- /pyouroboros/dockerclient.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from logging import getLogger 3 | from docker import DockerClient, tls 4 | from os.path import isdir, isfile, join 5 | from docker.errors import DockerException, APIError, NotFound 6 | 7 | from pyouroboros.helpers import set_properties, remove_sha_prefix, get_digest, run_hook 8 | 9 | 10 | class Docker(object): 11 | def __init__(self, socket, config, data_manager, notification_manager): 12 | self.config = config 13 | self.socket = socket 14 | self.client = self.connect() 15 | self.data_manager = data_manager 16 | self.logger = getLogger() 17 | 18 | self.notification_manager = notification_manager 19 | 20 | def connect(self): 21 | if self.config.docker_tls: 22 | try: 23 | cert_paths = { 24 | 'cert_top_dir': '/etc/docker/certs.d/', 25 | 'clean_socket': self.socket.split('//')[1] 26 | } 27 | cert_paths['cert_dir'] = join(cert_paths['cert_top_dir'], cert_paths['clean_socket']) 28 | cert_paths['cert_files'] = { 29 | 'client_cert': join(cert_paths['cert_dir'], 'client.cert'), 30 | 'client_key': join(cert_paths['cert_dir'], 'client.key'), 31 | 'ca_crt': join(cert_paths['cert_dir'], 'ca.crt') 32 | } 33 | 34 | if not isdir(cert_paths['cert_dir']): 35 | self.logger.error('%s is not a valid cert folder', cert_paths['cert_dir']) 36 | raise ValueError 37 | 38 | for cert_file in cert_paths['cert_files'].values(): 39 | if not isfile(cert_file): 40 | self.logger.error('%s does not exist', cert_file) 41 | raise ValueError 42 | 43 | tls_config = tls.TLSConfig( 44 | ca_cert=cert_paths['cert_files']['ca_crt'], 45 | verify=cert_paths['cert_files']['ca_crt'] if self.config.docker_tls_verify else False, 46 | client_cert=(cert_paths['cert_files']['client_cert'], cert_paths['cert_files']['client_key']) 47 | ) 48 | client = DockerClient(base_url=self.socket, tls=tls_config, timeout=self.config.docker_timeout) 49 | except ValueError: 50 | self.logger.error('Invalid Docker TLS config for %s, reverting to unsecured', self.socket) 51 | client = DockerClient(base_url=self.socket, timeout=self.config.docker_timeout) 52 | else: 53 | client = DockerClient(base_url=self.socket, timeout=self.config.docker_timeout) 54 | 55 | return client 56 | 57 | 58 | class BaseImageObject(object): 59 | def __init__(self, docker_client): 60 | self.docker = docker_client 61 | self.logger = self.docker.logger 62 | self.config = self.docker.config 63 | self.client = self.docker.client 64 | self.socket = self.docker.socket 65 | self.data_manager = self.docker.data_manager 66 | self.data_manager.total_updated[self.socket] = 0 67 | self.notification_manager = self.docker.notification_manager 68 | 69 | def _pull(self, tag): 70 | """Docker pull image tag""" 71 | self.logger.debug('Checking tag: %s', tag) 72 | try: 73 | if self.config.dry_run: 74 | # The authentication doesn't work with this call 75 | # See bugs https://github.com/docker/docker-py/issues/2225 76 | return self.client.images.get_registry_data(tag) 77 | else: 78 | if self.config.auth_json: 79 | return_image = self.client.images.pull(tag, auth_config=self.config.auth_json) 80 | else: 81 | return_image = self.client.images.pull(tag) 82 | return return_image 83 | except APIError as e: 84 | self.logger.debug(str(e)) 85 | if '' in str(e): 86 | self.logger.debug("Docker api issue. Ignoring") 87 | raise ConnectionError 88 | elif 'unauthorized' in str(e): 89 | if self.config.dry_run: 90 | self.logger.error('dry run : Upstream authentication issue while checking %s. See: ' 91 | 'https://github.com/docker/docker-py/issues/2225', tag) 92 | raise ConnectionError 93 | else: 94 | self.logger.critical("Invalid Credentials. Exiting") 95 | exit(1) 96 | elif 'Client.Timeout' in str(e): 97 | self.logger.critical( 98 | "Couldn't find an image on docker.com for %s. Local Build?", tag) 99 | raise ConnectionError 100 | elif ('pull access' or 'TLS handshake') in str(e): 101 | self.logger.critical("Couldn't pull. Skipping. Error: %s", e) 102 | raise ConnectionError 103 | 104 | 105 | class Container(BaseImageObject): 106 | mode = 'container' 107 | 108 | def __init__(self, docker_client): 109 | super().__init__(docker_client) 110 | self.monitored = self.monitor_filter() 111 | 112 | # Container sub functions 113 | def stop(self, container): 114 | self.logger.debug('Stopping container: %s', container.name) 115 | stop_signal = container.labels.get('com.ouroboros.stop_signal', False) 116 | if stop_signal: 117 | try: 118 | container.kill(signal=stop_signal) 119 | except APIError as e: 120 | self.logger.error('Cannot kill container using signal %s. stopping normally. Error: %s', 121 | stop_signal, e) 122 | container.stop() 123 | else: 124 | container.stop() 125 | 126 | def remove(self, container): 127 | self.logger.debug('Removing container: %s', container.name) 128 | try: 129 | container.remove() 130 | except NotFound as e: 131 | self.logger.error("Could not remove container. Error: %s", e) 132 | return 133 | 134 | def recreate(self, container, latest_image): 135 | new_config = set_properties(old=container, new=latest_image) 136 | 137 | self.stop(container) 138 | self.remove(container) 139 | 140 | created = self.client.api.create_container(**new_config) 141 | new_container = self.client.containers.get(created.get("Id")) 142 | 143 | # connect the new container to all networks of the old container 144 | for network_config in container.attrs['NetworkSettings']['Networks'].values(): 145 | network = self.client.networks.get(network_config['NetworkID']) 146 | try: 147 | network.disconnect(new_container.id, force=True) 148 | except APIError: 149 | pass 150 | new_network_config = { 151 | 'container': new_container, 152 | 'aliases': network_config['Aliases'], 153 | 'links': network_config['Links'] 154 | } 155 | if network_config['IPAMConfig']: 156 | new_network_config.update( 157 | { 158 | 'ipv4_address': network_config['IPAddress'], 159 | 'ipv6_address': network_config['GlobalIPv6Address'] 160 | } 161 | ) 162 | try: 163 | network.connect(**new_network_config) 164 | except APIError as e: 165 | if any(err in str(e) for err in ['user configured subnets', 'user defined networks']): 166 | if new_network_config.get('ipv4_address'): 167 | del new_network_config['ipv4_address'] 168 | if new_network_config.get('ipv6_address'): 169 | del new_network_config['ipv6_address'] 170 | network.connect(**new_network_config) 171 | else: 172 | self.logger.error('Unable to attach updated container to network "%s". Error: %s', network.name, e) 173 | 174 | new_container.start() 175 | return new_container 176 | 177 | def pull(self, current_tag): 178 | """Docker pull image tag""" 179 | tag = current_tag 180 | if not tag: 181 | self.logger.error('Missing tag. Skipping...') 182 | raise ConnectionError 183 | elif ':' not in tag: 184 | tag = f'{tag}:latest' 185 | return self._pull(tag) 186 | 187 | # Filters 188 | def running_filter(self): 189 | """Return running container objects list, except ouroboros itself""" 190 | running_containers = [] 191 | try: 192 | for container in self.client.containers.list(filters={'status': 'running'}): 193 | if self.config.self_update: 194 | running_containers.append(container) 195 | else: 196 | try: 197 | if 'ouroboros' not in container.image.tags[0]: 198 | if container.attrs['HostConfig']['AutoRemove']: 199 | self.logger.debug("Skipping %s due to --rm property.", container.name) 200 | else: 201 | running_containers.append(container) 202 | except IndexError: 203 | self.logger.error("%s has no tags.. you should clean it up! Ignoring.", container.id) 204 | continue 205 | 206 | except DockerException: 207 | self.logger.critical("Can't connect to Docker API at %s", self.config.docker_socket) 208 | exit(1) 209 | 210 | return running_containers 211 | 212 | def monitor_filter(self): 213 | """Return filtered running container objects list""" 214 | running_containers = self.running_filter() 215 | monitored_containers = [] 216 | 217 | for container in running_containers: 218 | ouro_label = container.labels.get('com.ouroboros.enable', False) 219 | # if labels enabled, use the label. 'true/yes' trigger monitoring. 220 | if self.config.label_enable and ouro_label: 221 | if ouro_label.lower() in ["true", "yes"]: 222 | monitored_containers.append(container) 223 | else: 224 | continue 225 | elif not self.config.labels_only: 226 | if self.config.monitor: 227 | if container.name in self.config.monitor and container.name not in self.config.ignore: 228 | monitored_containers.append(container) 229 | elif container.name not in self.config.ignore: 230 | monitored_containers.append(container) 231 | 232 | self.data_manager.monitored_containers[self.socket] = len(monitored_containers) 233 | self.data_manager.set(self.socket) 234 | 235 | return monitored_containers 236 | 237 | # Socket Functions 238 | def self_check(self): 239 | if self.config.self_update: 240 | me_list = [container for container in self.client.containers.list() if 'ouroboros' in container.name] 241 | if len(me_list) > 1: 242 | self.update_self(count=2, me_list=me_list) 243 | 244 | def socket_check(self): 245 | depends_on_names = [] 246 | hard_depends_on_names = [] 247 | updateable = [] 248 | self.monitored = self.monitor_filter() 249 | 250 | if not self.monitored: 251 | self.logger.info('No containers are running or monitored on %s', self.socket) 252 | return 253 | 254 | for container in self.monitored: 255 | current_image = container.image 256 | current_tag = container.attrs['Config']['Image'] 257 | latest_image = None 258 | 259 | if self.config.latest_only: 260 | image_name = current_tag.split(':')[0] 261 | try: 262 | latest_image = self.pull(f"{image_name}:latest") 263 | except ConnectionError: 264 | latest_image = None 265 | 266 | try: 267 | if latest_image is None: 268 | latest_image = self.pull(current_tag) 269 | except ConnectionError: 270 | continue 271 | 272 | try: 273 | if current_image.id != latest_image.id: 274 | updateable.append((container, current_image, latest_image)) 275 | else: 276 | continue 277 | except AttributeError: 278 | self.logger.error("Issue detecting %s's image tag. Skipping...", container.name) 279 | 280 | # Get container list to restart after update complete 281 | depends_on = container.labels.get('com.ouroboros.depends_on', False) 282 | hard_depends_on = container.labels.get('com.ouroboros.hard_depends_on', False) 283 | if depends_on: 284 | depends_on_names.extend([name.strip() for name in depends_on.split(',')]) 285 | if hard_depends_on: 286 | hard_depends_on_names.extend([name.strip() for name in hard_depends_on.split(',')]) 287 | 288 | hard_depends_on_containers = [] 289 | hard_depends_on_names = list(set(hard_depends_on_names)) 290 | for name in hard_depends_on_names: 291 | try: 292 | hard_depends_on_containers.append(self.client.containers.get(name)) 293 | except NotFound: 294 | self.logger.error("Could not find dependent container %s on socket %s. Ignoring", name, self.socket) 295 | 296 | depends_on_containers = [] 297 | depends_on_names = list(set(depends_on_names)) 298 | depends_on_names = [name for name in depends_on_names if name not in hard_depends_on_names] 299 | for name in depends_on_names: 300 | try: 301 | depends_on_containers.append(self.client.containers.get(name)) 302 | except NotFound: 303 | self.logger.error("Could not find dependent container %s on socket %s. Ignoring", name, self.socket) 304 | 305 | return updateable, depends_on_containers, hard_depends_on_containers 306 | 307 | def update(self): 308 | updated_count = 0 309 | try: 310 | updateable, depends_on_containers, hard_depends_on_containers = self.socket_check() 311 | mylocals = {} 312 | mylocals['updateable'] = updateable 313 | mylocals['depends_on_containers'] = depends_on_containers 314 | mylocals['hard_depends_on_containers'] = hard_depends_on_containers 315 | run_hook('updates_enumerated', None, mylocals) 316 | except TypeError: 317 | return 318 | 319 | for container in depends_on_containers + hard_depends_on_containers: 320 | mylocals = {} 321 | mylocals['container'] = container 322 | run_hook('before_stop_depends_container', None, mylocals) 323 | if not self.config.dry_run and not self.config.monitor_only: 324 | self.stop(container) 325 | 326 | for container, current_image, latest_image in updateable: 327 | if self.config.dry_run: 328 | # Ugly hack for repo digest 329 | repo_digest_id = current_image.attrs['RepoDigests'][0].split('@')[1] 330 | if repo_digest_id != latest_image.id: 331 | mylocals = {} 332 | mylocals['container'] = container 333 | mylocals['current_image'] = current_image 334 | mylocals['latest_image'] = latest_image 335 | run_hook('dry_run_update', None, mylocals) 336 | self.logger.info('dry run : %s would be updated', container.name) 337 | continue 338 | 339 | if self.config.monitor_only: 340 | # Ugly hack for repo digest 341 | repo_digest_id = current_image.attrs['RepoDigests'][0].split('@')[1] 342 | if repo_digest_id != latest_image.id: 343 | mylocals = {} 344 | mylocals['container'] = container 345 | mylocals['current_image'] = current_image 346 | mylocals['latest_image'] = latest_image 347 | run_hook('notify_update', None, mylocals) 348 | self.notification_manager.send( 349 | container_tuples=[(container.name, current_image, latest_image)], 350 | socket=self.socket, 351 | kind='monitor' 352 | ) 353 | continue 354 | 355 | if container.name in ['ouroboros', 'ouroboros-updated']: 356 | self.data_manager.total_updated[self.socket] += 1 357 | self.data_manager.add(label=container.name, socket=self.socket) 358 | self.data_manager.add(label='all', socket=self.socket) 359 | self.notification_manager.send(container_tuples=updateable, 360 | socket=self.socket, kind='update') 361 | self.update_self(old_container=container, new_image=latest_image, count=1) 362 | 363 | self.logger.info('%s will be updated', container.name) 364 | 365 | mylocals = {} 366 | mylocals['old_container'] = container 367 | mylocals['old_image'] = current_image 368 | mylocals['new_image'] = latest_image 369 | run_hook('before_update', None, mylocals) 370 | 371 | new_container = self.recreate(container, latest_image) 372 | 373 | mylocals['new_container'] = new_container 374 | run_hook('after_update', None, mylocals) 375 | 376 | if self.config.cleanup: 377 | try: 378 | mylocals = {} 379 | mylocals['image'] = current_image 380 | run_hook('before_image_cleanup', None, mylocals) 381 | self.client.images.remove(current_image.id) 382 | except APIError as e: 383 | self.logger.error("Could not delete old image for %s, Error: %s", container.name, e) 384 | 385 | if self.config.cleanup_unused_volumes: 386 | try: 387 | self.docker.client.volumes.prune() 388 | except APIError as e: 389 | self.logger.error("Could not delete unused volume for %s, Error: %s", container.name, e) 390 | 391 | updated_count += 1 392 | 393 | self.logger.debug("Incrementing total container updated count") 394 | 395 | self.data_manager.total_updated[self.socket] += 1 396 | self.data_manager.add(label=container.name, socket=self.socket) 397 | self.data_manager.add(label='all', socket=self.socket) 398 | 399 | for container in depends_on_containers: 400 | # Reload container to ensure it isn't referencing the old image 401 | mylocals = {} 402 | mylocals['container'] = container 403 | run_hook('before_start_depends_container', None, mylocals) 404 | if not self.config.dry_run and not self.config.monitor_only: 405 | container.reload() 406 | container.start() 407 | 408 | for container in hard_depends_on_containers: 409 | mylocals = {} 410 | mylocals['old_container'] = container 411 | run_hook('before_recreate_hard_depends_container', None, mylocals) 412 | if not self.config.dry_run and not self.config.monitor_only: 413 | new_container = self.recreate(container, container.image) 414 | mylocals['new_container'] = new_container 415 | else: 416 | mylocals['new_container'] = container 417 | run_hook('after_recreate_hard_depends_container', None, mylocals) 418 | 419 | if updated_count > 0: 420 | self.notification_manager.send(container_tuples=updateable, socket=self.socket, kind='update') 421 | 422 | def update_self(self, count=None, old_container=None, me_list=None, new_image=None): 423 | if count == 2: 424 | self.logger.debug('God im messy... cleaning myself up.') 425 | old_me_index = 0 if me_list[0].attrs['Created'] < me_list[1].attrs['Created'] else 1 426 | old_me_id = me_list[old_me_index].id 427 | old_me = self.client.containers.get(old_me_id) 428 | old_me_image_id = old_me.image.id 429 | 430 | mylocals = {} 431 | mylocals['old_container'] = old_me 432 | mylocals['new_container'] = self.client.containers.get(me_list[0].id if old_me_index == 1 else me_list[1].id) 433 | run_hook('before_self_cleanup', None, mylocals) 434 | 435 | old_me.stop() 436 | old_me.remove() 437 | 438 | self.client.images.remove(old_me_image_id) 439 | self.logger.debug('Ahhh. All better.') 440 | run_hook('after_self_cleanup', None, mylocals) 441 | 442 | self.data_manager.load() 443 | self.monitored = self.monitor_filter() 444 | elif count == 1: 445 | self.logger.debug('I need to update! Starting the ouroboros ;)') 446 | self_name = 'ouroboros-updated' if old_container.name == 'ouroboros' else 'ouroboros' 447 | new_config = set_properties(old=old_container, new=new_image, self_name=self_name) 448 | self.data_manager.save() 449 | mylocals = {} 450 | mylocals['self_name'] = self_name 451 | mylocals['old_container'] = old_container 452 | mylocals['new_image'] = new_image 453 | run_hook('before_self_update', None, mylocals) 454 | try: 455 | me_created = self.client.api.create_container(**new_config) 456 | new_me = self.client.containers.get(me_created.get("Id")) 457 | new_me.start() 458 | self.logger.debug('If you strike me down, I shall become ' 459 | 'more powerful than you could possibly imagine.') 460 | self.logger.debug('https://bit.ly/2VVY7GH') 461 | mylocals['new_container'] = new_me 462 | run_hook('after_self_update', None, mylocals) 463 | sleep(30) 464 | except APIError as e: 465 | self.logger.error("Self update failed.") 466 | self.logger.error(e) 467 | 468 | 469 | class Service(BaseImageObject): 470 | mode = 'service' 471 | 472 | def __init__(self, docker_client): 473 | super().__init__(docker_client) 474 | self.monitored = self.monitor_filter() 475 | 476 | def monitor_filter(self): 477 | """Return filtered service objects list""" 478 | services = self.client.services.list(filters={'label': 'com.ouroboros.enable'}) 479 | 480 | monitored_services = [] 481 | 482 | for service in services: 483 | ouro_label = service.attrs['Spec']['Labels'].get('com.ouroboros.enable') 484 | if not self.config.label_enable or ouro_label.lower() in ["true", "yes"]: 485 | monitored_services.append(service) 486 | 487 | self.data_manager.monitored_containers[self.socket] = len(monitored_services) 488 | self.data_manager.set(self.socket) 489 | 490 | return monitored_services 491 | 492 | def pull(self, tag): 493 | """Docker pull image tag""" 494 | return self._pull(tag) 495 | 496 | def update(self): 497 | updated_service_tuples = [] 498 | self.monitored = self.monitor_filter() 499 | 500 | if not self.monitored: 501 | self.logger.info('No services monitored') 502 | 503 | for service in self.monitored: 504 | image_string = service.attrs['Spec']['TaskTemplate']['ContainerSpec']['Image'] 505 | tag = image_string.split('@')[0] 506 | if '@' in image_string: 507 | sha256 = remove_sha_prefix(image_string.split('@')[1]) 508 | else: 509 | sha256 = remove_sha_prefix(self.client.images.get(tag).attrs['RepoDigests'][0]) 510 | if len(sha256) == 0: 511 | self.logger.error('No image SHA for %s. Skipping', image_string) 512 | continue 513 | 514 | latest_image = None 515 | 516 | if self.config.latest_only: 517 | image_name = tag.split(':')[0] 518 | try: 519 | latest_image = self.pull(f"{image_name}:latest") 520 | except ConnectionError: 521 | latest_image = None 522 | 523 | try: 524 | if latest_image is None: 525 | latest_image = self.pull(tag) 526 | except ConnectionError: 527 | continue 528 | 529 | latest_image_sha256 = get_digest(latest_image) 530 | self.logger.debug('Latest sha256 for %s is %s', tag, latest_image_sha256) 531 | 532 | if sha256 != latest_image_sha256: 533 | if self.config.dry_run: 534 | # Ugly hack for repo digest 535 | self.logger.info('dry run : %s would be updated', service.name) 536 | continue 537 | 538 | if self.config.monitor_only: 539 | # Ugly hack for repo digest 540 | self.notification_manager.send( 541 | container_tuples=[(service, sha256[-10], latest_image)], 542 | socket=self.socket, 543 | kind='monitor', 544 | mode='service' 545 | ) 546 | continue 547 | 548 | updated_service_tuples.append( 549 | (service, sha256[-10:], latest_image) 550 | ) 551 | 552 | if 'ouroboros' in service.name and self.config.self_update: 553 | self.data_manager.total_updated[self.socket] += 1 554 | self.data_manager.add(label=service.name, socket=self.socket) 555 | self.data_manager.add(label='all', socket=self.socket) 556 | self.notification_manager.send(container_tuples=updated_service_tuples, 557 | socket=self.socket, kind='update', mode='service') 558 | 559 | self.logger.info('%s will be updated', service.name) 560 | service.update(image=f"{tag}@sha256:{latest_image_sha256}") 561 | 562 | self.data_manager.total_updated[self.socket] += 1 563 | self.data_manager.add(label=service.name, socket=self.socket) 564 | self.data_manager.add(label='all', socket=self.socket) 565 | 566 | if updated_service_tuples: 567 | self.notification_manager.send( 568 | container_tuples=updated_service_tuples, 569 | socket=self.socket, 570 | kind='update', 571 | mode='service' 572 | ) 573 | -------------------------------------------------------------------------------- /pyouroboros/helpers.py: -------------------------------------------------------------------------------- 1 | from inspect import getframeinfo, currentframe 2 | from logging import getLogger 3 | from os.path import dirname, abspath 4 | from pathlib import Path 5 | 6 | def get_exec_dir() -> str: 7 | """ 8 | Returns the absolute path to the directory of this script, without trailing slash 9 | """ 10 | filename = getframeinfo(currentframe()).filename 11 | path = dirname(abspath(filename)) 12 | if path.endswith('/'): 13 | path = path[:-1] 14 | return path 15 | 16 | def run_hook(hookname:str, myglobals:dict|None=None, mylocals:dict|None=None): 17 | """ 18 | Looks for python scripts in the `hooks/hookname` sub-directory, relative to the location of this script, and executes them 19 | 20 | `myglobals` will be updated (and created, if None) with `__file__` set to the hook script and `__name__` to `__main__` 21 | 22 | Args: 23 | hookname (str): The name of the hook sub-directory to search 24 | myglobals (dict|None) : An optional dict of data made available under globals() 25 | mylocals (dict|None) : An optional dict of data made available under locals() 26 | """ 27 | try : 28 | pathlist = Path(get_exec_dir() + '/hooks/' + hookname).rglob('*.py') 29 | except: 30 | getLogger().error("An error was raised while scanning the directory for hook %s", hookname, exc_info=True) 31 | return 32 | for path in pathlist: 33 | execfile(str(path), myglobals, mylocals) 34 | 35 | # Copied from https://stackoverflow.com/a/41658338 36 | def execfile(filepath:str, myglobals:dict|None=None, mylocals:dict|None=None): 37 | """ 38 | Compiles and executes a python script with compile() and exec() 39 | 40 | Unhandled raised errors will be caught and sent to the logger 41 | 42 | `myglobals` will be updated (and created, if None) with `__file__` set to the hook script and `__name__` to `__main__` 43 | 44 | Args: 45 | filename (str): The path to the file to execute 46 | myglobals (dict|None) : An optional dict of data made available under globals() 47 | mylocals (dict|None) : An optional dict of data made available under locals() 48 | """ 49 | if myglobals is None: 50 | myglobals = {} 51 | myglobals.update({ 52 | "__file__": filepath, 53 | "__name__": "__main__", 54 | }) 55 | try : 56 | with open(filepath, 'rb') as file: 57 | try: 58 | exec(compile(file.read(), filepath, 'exec'), myglobals, mylocals) 59 | except: 60 | getLogger().error("An error was raised while executing hook script %s", filepath, exc_info=True) 61 | except: 62 | getLogger().error("An error was raised while reading hook script %s", filepath, exc_info=True) 63 | 64 | def isContainerNetwork(container) -> bool: 65 | """ 66 | Returns `True` if the network type of the provided container dict is "container" 67 | """ 68 | parts = container.attrs['HostConfig']['NetworkMode'].split(':') 69 | return len(parts) > 1 and parts[0] == 'container' 70 | 71 | def set_properties(old, new, self_name:str|None=None) -> dict: 72 | """ 73 | Cretates a configuration dict for a new container, based on the configuration of the old container 74 | 75 | Args: 76 | old: The old container 77 | new: The new image (unused) 78 | self_name (str|None): The name of the new container; `None` to use the old container name 79 | 80 | Returns: 81 | dict: The new configuration 82 | """ 83 | properties = { 84 | 'name': self_name if self_name else old.name, 85 | 'hostname': '' if isContainerNetwork(old) else old.attrs['Config']['Hostname'], 86 | 'user': old.attrs['Config']['User'], 87 | 'detach': True, 88 | 'domainname': old.attrs['Config']['Domainname'], 89 | 'tty': old.attrs['Config']['Tty'], 90 | 'ports': None if isContainerNetwork(old) or not old.attrs['Config'].get('ExposedPorts') else [ 91 | ((p.split('/')[0], p.split('/')[1]) if '/' in p else p) for p in old.attrs['Config']['ExposedPorts'].keys() 92 | ], 93 | 'volumes': None if not old.attrs['Config'].get('Volumes') else [ 94 | v for v in old.attrs['Config']['Volumes'].keys() 95 | ], 96 | 'working_dir': old.attrs['Config']['WorkingDir'], 97 | 'image': old.attrs['Config']['Image'], 98 | 'command': old.attrs['Config']['Cmd'], 99 | 'host_config': old.attrs['HostConfig'], 100 | 'labels': old.attrs['Config']['Labels'], 101 | 'entrypoint': old.attrs['Config']['Entrypoint'], 102 | 'environment': old.attrs['Config']['Env'], 103 | 'healthcheck': old.attrs['Config'].get('Healthcheck', None) 104 | } 105 | 106 | return properties 107 | 108 | 109 | def remove_sha_prefix(digest:str) -> str: 110 | """ 111 | Utility function to strip the `sha256:` prefix from a digest 112 | """ 113 | if digest.startswith("sha256:"): 114 | return digest[7:] 115 | return digest 116 | 117 | 118 | def get_digest(image) -> str: 119 | """ 120 | Utility to locate the digest of an image and return it 121 | """ 122 | digest = image.attrs.get( 123 | "Descriptor", {} 124 | ).get("digest") or image.attrs.get( 125 | "RepoDigests" 126 | )[0].split('@')[1] or image.id 127 | return remove_sha_prefix(digest) 128 | -------------------------------------------------------------------------------- /pyouroboros/logger.py: -------------------------------------------------------------------------------- 1 | from logging import Filter, getLogger, Formatter, StreamHandler 2 | 3 | 4 | class BlacklistFilter(Filter): 5 | """ 6 | Log filter for blacklisted tokens and passwords 7 | """ 8 | 9 | blacklisted_keys = ['repo_user', 'repo_pass', 'auth_json', 'docker_sockets', 'prometheus_addr', 10 | 'influx_username', 'influx_password', 'influx_url', 'notifiers'] 11 | 12 | def __init__(self, filteredstrings): 13 | super().__init__() 14 | self.filtered_strings = filteredstrings 15 | 16 | def filter(self, record): 17 | for item in self.filtered_strings: 18 | try: 19 | if item in record.msg: 20 | record.msg = record.msg.replace(item, 8 * '*' + item[-5:]) 21 | if any(item in str(arg) for arg in record.args): 22 | record.args = tuple(arg.replace(item, 8 * '*' + item[-5:]) if isinstance(arg, str) else arg 23 | for arg in record.args) 24 | except TypeError: 25 | pass 26 | return True 27 | 28 | 29 | class OuroborosLogger(object): 30 | def __init__(self, level='INFO'): 31 | # Create the Logger 32 | self.logger = getLogger() 33 | try: 34 | self.logger.setLevel(level.upper()) 35 | except ValueError: 36 | level = "INFO" 37 | self.logger.setLevel(level.upper()) 38 | 39 | # Create a Formatter for formatting the log messages 40 | logger_formatter = Formatter('%(asctime)s : %(levelname)s : %(module)s : %(message)s', '%Y-%m-%d %H:%M:%S') 41 | 42 | # Add the console logger 43 | console_logger = StreamHandler() 44 | console_logger.setFormatter(logger_formatter) 45 | 46 | console_logger.setLevel(level.upper()) 47 | 48 | # Add the Handler to the Logger 49 | self.logger.addHandler(console_logger) 50 | 51 | getLogger('apscheduler').setLevel(level.upper()) 52 | -------------------------------------------------------------------------------- /pyouroboros/notifiers.py: -------------------------------------------------------------------------------- 1 | import apprise 2 | import gettext 3 | 4 | from logging import getLogger 5 | from babel import dates 6 | from pytz import timezone 7 | 8 | class NotificationManager(object): 9 | def __init__(self, config, data_manager): 10 | self.config = config 11 | self.data_manager = data_manager 12 | self.logger = getLogger() 13 | 14 | self.apprise = self.build_apprise() 15 | self._ = None 16 | 17 | try: 18 | language = gettext.translation('notifiers', localedir='locales', languages=self.config.language) 19 | language.install() 20 | self._ = language.gettext 21 | except FileNotFoundError: 22 | if not self.config.language == 'en': 23 | self.logger.error("Can't find the '%s' language", self.config.language) 24 | self._ = gettext.gettext 25 | 26 | def build_apprise(self): 27 | asset = apprise.AppriseAsset( 28 | image_url_mask='https://raw.githubusercontent.com/gmt2001/ouroboros/main/assets/ouroboros_logo_icon_72.png', 29 | default_extension='.png' 30 | ) 31 | asset.app_id = "Ouroboros" 32 | asset.app_desc = "Ouroboros" 33 | asset.app_url = "https://github.com/gmt2001/ouroboros" 34 | asset.html_notify_map['info'] = '#5F87C6' 35 | asset.image_url_logo = 'https://raw.githubusercontent.com/gmt2001/ouroboros/main/assets/ouroboros_logo_icon_256.png' 36 | 37 | apprise_obj = apprise.Apprise(asset=asset) 38 | 39 | for notifier in self.config.notifiers: 40 | add = apprise_obj.add(notifier) 41 | if not add: 42 | self.logger.error(self._('Could not add notifier %s'), notifier) 43 | 44 | return apprise_obj 45 | 46 | def send(self, container_tuples=None, socket=None, kind='update', next_run=None, mode='container'): 47 | if kind == 'startup': 48 | title = self._('Ouroboros has started') 49 | body_fields = [ 50 | self._('Host: %s') % self.config.hostname, 51 | self._('Time: %s') % dates.format_datetime(None, format='full', tzinfo=timezone(self.config.tz), locale=self.config.language), 52 | self._('Next Run: %s') % dates.format_datetime(next_run, format='full', tzinfo=timezone(self.config.tz), locale=self.config.language)] 53 | elif kind == 'monitor': 54 | title = self._('Ouroboros has detected updates!') 55 | body_fields = [ 56 | self._('Host/Socket: %s / %s') % (self.config.hostname, socket.split('//')[1]), 57 | self._('Containers Monitored: %d') % self.data_manager.monitored_containers[socket], 58 | self._('Total Containers Updated: %d') % self.data_manager.total_updated[socket] 59 | ] 60 | body_fields.extend( 61 | [ 62 | self._("{} updated from {} to {}").format( 63 | container.name, 64 | old_image if mode == 'service' else old_image.short_id.split(':')[1], 65 | new_image.short_id.split(':')[1] 66 | ) for container, old_image, new_image in container_tuples 67 | ] 68 | ) 69 | else: 70 | title = self._('Ouroboros has updated containers!') 71 | body_fields = [ 72 | self._('Host/Socket: %s / %s') % (self.config.hostname, socket.split('//')[1]), 73 | self._('Containers Monitored: %d') % self.data_manager.monitored_containers[socket], 74 | self._('Total Containers Updated: %d') % self.data_manager.total_updated[socket], 75 | self._('Containers updated this pass: %d') % len(container_tuples) 76 | ] 77 | body_fields.extend( 78 | [ 79 | self._("{} updated from {} to {}").format( 80 | container.name, 81 | old_image if mode == 'service' else old_image.short_id.split(':')[1], 82 | new_image.short_id.split(':')[1] 83 | ) for container, old_image, new_image in container_tuples 84 | ] 85 | ) 86 | body = '\r\n'.join(body_fields) 87 | 88 | if self.apprise.servers: 89 | self.apprise.notify(title=title, body=body, body_format=apprise.NotifyFormat.TEXT) 90 | -------------------------------------------------------------------------------- /pyouroboros/ouroboros.py: -------------------------------------------------------------------------------- 1 | import gettext 2 | 3 | from time import sleep 4 | from os import environ 5 | 6 | from requests.exceptions import ConnectionError 7 | from datetime import datetime, timedelta 8 | from argparse import ArgumentParser, RawTextHelpFormatter 9 | from apscheduler.schedulers.background import BackgroundScheduler 10 | from pytz import timezone 11 | 12 | from pyouroboros.config import Config 13 | from pyouroboros import VERSION, BRANCH 14 | from pyouroboros.logger import OuroborosLogger 15 | from pyouroboros.dataexporters import DataManager 16 | from pyouroboros.notifiers import NotificationManager 17 | from pyouroboros.dockerclient import Docker, Container, Service 18 | 19 | 20 | def main(): 21 | """Declare command line options""" 22 | parser = ArgumentParser(description='ouroboros', formatter_class=RawTextHelpFormatter, 23 | epilog='EXAMPLE: ouroboros -d tcp://1.2.3.4:5678 -i 20 -m container1 container2 -l warn') 24 | 25 | core_group = parser.add_argument_group("Core", "Configuration of core functionality") 26 | core_group.add_argument('-v', '--version', action='version', version=VERSION) 27 | 28 | core_group.add_argument('-d', '--docker-sockets', nargs='+', default=Config.docker_sockets, dest='DOCKER_SOCKETS', 29 | help='Sockets for docker management\n' 30 | 'DEFAULT: "unix://var/run/docker.sock"\n' 31 | 'EXAMPLE: -d unix://var/run/docker.sock tcp://192.168.1.100:2376') 32 | 33 | core_group.add_argument('-t', '--docker-tls', default=Config.docker_tls, dest='DOCKER_TLS', 34 | action='store_true', help='Enable docker TLS\n' 35 | 'REQUIRES: docker cert mount') 36 | 37 | core_group.add_argument('-T', '--docker-tls-verify', default=Config.docker_tls_verify, dest='DOCKER_TLS_VERIFY', 38 | action='store_false', help='Verify the CA Certificate mounted for TLS\n' 39 | 'DEFAULT: True') 40 | 41 | core_group.add_argument('-i', '--interval', type=int, default=Config.interval, dest='INTERVAL', 42 | help='Interval in seconds between checking for updates\n' 43 | 'DEFAULT: 300') 44 | 45 | core_group.add_argument('-C', '--cron', default=Config.cron, dest='CRON', 46 | help='Cron formatted string for scheduling\n' 47 | 'EXAMPLE: "*/5 * * * *"') 48 | 49 | core_group.add_argument('-G', '--grace', default=Config.grace, dest='GRACE', 50 | help='Grace time for late jobs to execute anyway. -1 for always execute; 0 for never execute if late; number of seconds otherwise\n' 51 | 'DEFAULT: 15') 52 | 53 | core_group.add_argument('-l', '--log-level', choices=['debug', 'info', 'warn', 'error', 'critical'], 54 | dest='LOG_LEVEL', default=Config.log_level, help='Set logging level\n' 55 | 'DEFAULT: info') 56 | 57 | core_group.add_argument('-u', '--self-update', default=Config.self_update, dest='SELF_UPDATE', action='store_true', 58 | help='Let ouroboros update itself') 59 | 60 | core_group.add_argument('-o', '--run-once', default=Config.run_once, action='store_true', dest='RUN_ONCE', 61 | help='Single run') 62 | 63 | core_group.add_argument('-A', '--dry-run', default=Config.dry_run, action='store_true', dest='DRY_RUN', 64 | help='Run without making changes. Best used with run-once') 65 | 66 | core_group.add_argument('-mo', '--monitor-only', default=Config.monitor_only, action='store_true', dest='MONITOR_ONLY', 67 | help='Run and send notifications without making changes') 68 | 69 | core_group.add_argument('-N', '--notifiers', nargs='+', default=Config.notifiers, dest='NOTIFIERS', 70 | help='Apprise formatted notifiers\n' 71 | 'EXAMPLE: -N discord://1234123412341234/jasdfasdfasdfasddfasdf ' 72 | 'mailto://user:pass@gmail.com') 73 | 74 | core_group.add_argument('-la', '--language', default=Config.language, dest='LANGUAGE', 75 | help='Set the language of the translation\nDEFAULT: en') 76 | 77 | core_group.add_argument('-tz', '--timezone', default=Config.tz, dest='TZ', 78 | help='Set the timezone of notifications and cron\nDEFAULT: UTC') 79 | 80 | docker_group = parser.add_argument_group("Docker", "Configuration of docker functionality") 81 | docker_group.add_argument('--docker-timeout', type=int, default=Config.docker_timeout, dest='DOCKER_TIMEOUT', 82 | help='Docker client timeout, in seconds\n' 83 | 'DEFAULT: 60') 84 | 85 | docker_group.add_argument('-S', '--swarm', default=Config.swarm, dest='SWARM', action='store_true', 86 | help='Put ouroboros in swarm mode') 87 | 88 | docker_group.add_argument('-m', '--monitor', nargs='+', default=Config.monitor, dest='MONITOR', 89 | help='Which container(s) to monitor\n' 90 | 'DEFAULT: All') 91 | 92 | docker_group.add_argument('-n', '--ignore', nargs='+', default=Config.ignore, dest='IGNORE', 93 | help='Container(s) to ignore\n' 94 | 'EXAMPLE: -n container1 container2') 95 | 96 | docker_group.add_argument('-k', '--label-enable', default=Config.label_enable, dest='LABEL_ENABLE', 97 | action='store_true', help='Enable label monitoring for ouroboros label options\n' 98 | 'Note: labels take precedence' 99 | 'DEFAULT: False') 100 | 101 | docker_group.add_argument('-M', '--labels-only', default=Config.labels_only, dest='LABELS_ONLY', 102 | action='store_true', help='Only watch containers that utilize labels\n' 103 | 'This allows a more strict compliance for environments' 104 | 'DEFAULT: False') 105 | 106 | docker_group.add_argument('-c', '--cleanup', default=Config.cleanup, dest='CLEANUP', action='store_true', 107 | help='Remove old images after updating') 108 | 109 | docker_group.add_argument('-L', '--latest-only', default=Config.latest_only, dest='LATEST_ONLY', action='store_true', 110 | help='Always update to :latest tag regardless of current tag, if available') 111 | 112 | docker_group.add_argument('-r', '--repo-user', default=Config.repo_user, dest='REPO_USER', 113 | help='Private docker registry username\n' 114 | 'EXAMPLE: foo@bar.baz') 115 | 116 | docker_group.add_argument('-R', '--repo-pass', default=Config.repo_pass, dest='REPO_PASS', 117 | help='Private docker registry password\n' 118 | 'EXAMPLE: MyPa$$w0rd') 119 | 120 | data_group = parser.add_argument_group('Data Export', 'Configuration of data export functionality') 121 | data_group.add_argument('-sc', '--save-counters', default=Config.save_counters, dest='SAVE_COUNTERS', 122 | action='store_true', help='Save total-updated counters across self-updates') 123 | 124 | data_group.add_argument('-D', '--data-export', choices=['prometheus', 'influxdb'], default=Config.data_export, 125 | dest='DATA_EXPORT', help='Enable exporting of data for chosen option') 126 | 127 | data_group.add_argument('-a', '--prometheus-addr', default=Config.prometheus_addr, 128 | dest='PROMETHEUS_ADDR', help='Bind address to run Prometheus exporter on\n' 129 | 'DEFAULT: 127.0.0.1') 130 | 131 | data_group.add_argument('-p', '--prometheus-port', type=int, default=Config.prometheus_port, 132 | dest='PROMETHEUS_PORT', help='Port to run Prometheus exporter on\n' 133 | 'DEFAULT: 8000') 134 | 135 | data_group.add_argument('-I', '--influx-url', default=Config.influx_url, dest='INFLUX_URL', 136 | help='URL for influxdb\n' 137 | 'DEFAULT: 127.0.0.1') 138 | 139 | data_group.add_argument('-P', '--influx-port', type=int, default=Config.influx_port, dest='INFLUX_PORT', 140 | help='PORT for influxdb\n' 141 | 'DEFAULT: 8086') 142 | 143 | data_group.add_argument('-U', '--influx-username', default=Config.influx_username, dest='INFLUX_USERNAME', 144 | help='Username for influxdb\n' 145 | 'DEFAULT: root') 146 | 147 | data_group.add_argument('-x', '--influx-password', default=Config.influx_password, dest='INFLUX_PASSWORD', 148 | help='Password for influxdb\n' 149 | 'DEFAULT: root') 150 | 151 | data_group.add_argument('-X', '--influx-database', default=Config.influx_database, dest='INFLUX_DATABASE', 152 | help='Influx database name. Required if using influxdb') 153 | 154 | data_group.add_argument('-s', '--influx-ssl', default=Config.influx_ssl, dest='INFLUX_SSL', action='store_true', 155 | help='Use SSL when connecting to influxdb') 156 | 157 | data_group.add_argument('-V', '--influx-verify-ssl', default=Config.influx_verify_ssl, dest='INFLUX_VERIFY_SSL', 158 | action='store_true', help='Verify SSL certificate when connecting to influxdb') 159 | 160 | docker_group.add_argument('--skip-startup-notifications', default=Config.skip_startup_notifications, 161 | dest='SKIP_STARTUP_NOTIFICATIONS', action='store_true', 162 | help='Do not send ouroboros notifications when starting') 163 | 164 | _ = None 165 | args = parser.parse_args() 166 | 167 | try: 168 | language = gettext.translation('ouroboros', localedir='locales', languages=args.LANGUAGE) 169 | language.install() 170 | _ = language.gettext 171 | except FileNotFoundError: 172 | _ = gettext.gettext 173 | 174 | if environ.get('LOG_LEVEL'): 175 | log_level = environ.get('LOG_LEVEL') 176 | else: 177 | log_level = args.LOG_LEVEL 178 | ol = OuroborosLogger(level=log_level) 179 | ol.logger.info(_('Version: %s-%s'), VERSION, BRANCH) 180 | config = Config(environment_vars=environ, cli_args=args) 181 | config_dict = {key: value for key, value in vars(config).items() if key.upper() in config.options} 182 | ol.logger.debug(_("Ouroboros configuration: %s"), config_dict) 183 | 184 | data_manager = DataManager(config) 185 | notification_manager = NotificationManager(config, data_manager) 186 | scheduler = BackgroundScheduler() 187 | scheduler.start() 188 | 189 | for socket in config.docker_sockets: 190 | try: 191 | docker = Docker(socket, config, data_manager, notification_manager) 192 | if config.swarm: 193 | mode = Service(docker) 194 | else: 195 | mode = Container(docker) 196 | 197 | if config.run_once: 198 | scheduler.add_job(mode.update, name=_('Run Once container update for %s') % socket) 199 | else: 200 | if mode.mode == 'container': 201 | scheduler.add_job(mode.self_check, name=_('Self Check for %s') % socket) 202 | if config.cron: 203 | scheduler.add_job( 204 | mode.update, 205 | name=_('Cron container update for %s') % socket, 206 | trigger='cron', 207 | minute=config.cron[0], 208 | hour=config.cron[1], 209 | day=config.cron[2], 210 | month=config.cron[3], 211 | day_of_week=config.cron[4], 212 | timezone=timezone(config.tz), 213 | coalesce=True, 214 | misfire_grace_time=config.grace 215 | ) 216 | else: 217 | scheduler.add_job( 218 | mode.update, 219 | name=_('Initial run interval container update for %s') % socket 220 | ) 221 | scheduler.add_job( 222 | mode.update, 223 | name=_('Interval container update for %s') % socket, 224 | trigger='interval', seconds=config.interval, 225 | coalesce=True, 226 | misfire_grace_time=config.grace 227 | ) 228 | except ConnectionError: 229 | ol.logger.error(_("Could not connect to socket %s. Check your config"), socket) 230 | 231 | if config.run_once: 232 | next_run = None 233 | elif config.cron: 234 | next_run = scheduler.get_jobs()[0].next_run_time 235 | else: 236 | now = datetime.now(timezone('UTC')).astimezone() 237 | next_run = (now + timedelta(0, config.interval)) 238 | 239 | if not config.skip_startup_notifications: 240 | notification_manager.send(kind='startup', next_run=next_run) 241 | 242 | while scheduler.get_jobs(): 243 | sleep(1) 244 | 245 | scheduler.shutdown() 246 | 247 | 248 | if __name__ == "__main__": 249 | main() 250 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | docker>=4.3.1 2 | prometheus_client>=0.8.0 3 | requests>=2.25.0 4 | influxdb>=5.3.1 5 | apprise>=0.8.9 6 | apscheduler>=3.6.3 7 | Babel>=2.9.1 8 | pytz 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from pyouroboros import VERSION 3 | 4 | 5 | def read(filename): 6 | with open(filename) as f: 7 | return f.read() 8 | 9 | 10 | def get_requirements(filename="requirements.txt"): 11 | """returns a list of all requirements""" 12 | requirements = read(filename) 13 | return list(filter(None, [req.strip() for req in requirements.split() if not req.startswith('#')])) 14 | 15 | 16 | setup( 17 | name='ouroboros-cli', 18 | version=VERSION, 19 | author='circa10a', 20 | author_email='caleblemoine@gmail.com', 21 | maintainer='gmt2001', 22 | description='Automatically update running docker containers', 23 | long_description=read('README.md'), 24 | long_description_content_type='text/markdown', 25 | url='https://github.com/gmt2001/ouroboros', 26 | license='MIT', 27 | classifiers=['Programming Language :: Python', 28 | 'Programming Language :: Python :: 3.6', 29 | 'Programming Language :: Python :: 3.7'], 30 | packages=find_packages(), 31 | scripts=['ouroboros'], 32 | install_requires=get_requirements(), 33 | python_requires='>=3.6.2' 34 | ) 35 | --------------------------------------------------------------------------------