├── .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 |
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 |
--------------------------------------------------------------------------------