Full Code of circa10a/ouroboros for AI

master fee45f2dbf6e cached
23 files
95.7 KB
26.3k tokens
54 symbols
1 requests
Download .txt
Repository: circa10a/ouroboros
Branch: master
Commit: fee45f2dbf6e
Files: 23
Total size: 95.7 KB

Directory structure:
gitextract_imqw0m8b/

├── .github/
│   ├── FUNDING.yml
│   └── dependabot.yml
├── .gitignore
├── .gitlab-ci.yml
├── CHANGELOG.md
├── Dockerfile
├── Dockerfile.arm
├── Dockerfile.arm64
├── LICENSE
├── MANIFEST.in
├── README.md
├── docker-compose.yml
├── ouroboros
├── pyouroboros/
│   ├── __init__.py
│   ├── config.py
│   ├── dataexporters.py
│   ├── dockerclient.py
│   ├── helpers.py
│   ├── logger.py
│   ├── notifiers.py
│   └── ouroboros.py
├── requirements.txt
└── setup.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
custom: https://buymeacoff.ee/ouroboros

================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: github-actions
    directory: "/"
    schedule:
      interval: daily
    open-pull-requests-limit: 10

  - package-ecosystem: docker
    directory: "/"
    schedule:
      interval: daily
    open-pull-requests-limit: 10

  - package-ecosystem: pip
    directory: "/"
    schedule:
      interval: daily
    open-pull-requests-limit: 10


================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

#vscode
.vscode/

# JetBrains
.idea/
*.iml



================================================
FILE: .gitlab-ci.yml
================================================
stages:
  - test
  - build and push
  - manifests
  - release

.common_build: &common_build
  image: docker:stable
  stage: build and push
  variables:
    DOCKER_HOST: tcp://docker:2375/
    DOCKER_DRIVER: overlay2
  services:
    - docker:dind
  except:
    - tags
  only:
    changes:
      - "Dockerfile*"
      - ouroboros
      - pyouroboros/*
      - .gitlab-ci.yml
  before_script:
    - mkdir $HOME/.docker
    - echo '{"experimental":"enabled"}' > $HOME/.docker/config.json
    - if [[ $CI_COMMIT_REF_NAME == "master" ]]; then
          export TAG=$(grep -i version pyouroboros/__init__.py | cut -d \  -f3 | tr -d \");
      else
          export TAG="develop";
      fi
    - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USER" --password-stdin
    - export CI_PROJECT_PATH=$(echo $CI_PROJECT_PATH | tr "[:upper:]" "[:lower:]")

.common_release: &common_release
  <<: *common_build
  tags:
    - docker
    - shared
  only:
    refs:
      - master

flake8:
  image: python:3.7.3-alpine
  stage: test
  cache:
    key: "ouroboros-cache"
    paths:
      - .cache/pip
      - venv/
  variables:
    PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
  tags:
    - shared
  before_script:
    - python -m venv venv
    - source venv/bin/activate
    - pip install -U flake8
  script:
    - flake8 --max-line-length 120 ouroboros *.py pyouroboros/*.py

amd64:
  <<: *common_build
  tags:
    - docker
    - shared
  script:
    - docker build --pull
                   --cache-from "$CI_PROJECT_PATH":"$TAG-amd64"
                   -t "$CI_PROJECT_PATH":"$TAG-amd64" .
    - docker push "$CI_PROJECT_PATH":"$TAG-amd64"

armv6:
  <<: *common_build
  tags:
    - docker
    - arm
  script:
    - docker build --pull
                   --cache-from "$CI_PROJECT_PATH":"$TAG-arm"
                   -t "$CI_PROJECT_PATH":"$TAG-arm"
                   -f Dockerfile.arm .
    - docker push "$CI_PROJECT_PATH":"$TAG-arm"

arm64v8:
  <<: *common_build
  tags:
    - docker
    - arm64
  script:
    - docker build --pull
                   --cache-from "$CI_PROJECT_PATH":"$TAG-arm64"
                   -t "$CI_PROJECT_PATH":"$TAG-arm64"
                   -f Dockerfile.arm64 .
    - docker push "$CI_PROJECT_PATH":"$TAG-arm64"

versioned:
  <<: *common_build
  stage: manifests
  tags:
    - docker
    - shared
  script:
    - docker manifest create "$CI_PROJECT_PATH":"$TAG"
      "$CI_PROJECT_PATH":"$TAG-amd64"
      "$CI_PROJECT_PATH":"$TAG-arm64"
      "$CI_PROJECT_PATH":"$TAG-arm"
    - docker manifest push -p "$CI_PROJECT_PATH":"$TAG"

latest:
  <<: *common_release
  stage: manifests
  tags:
    - docker
  script:
    - docker manifest create "$CI_PROJECT_PATH":latest
      "$CI_PROJECT_PATH":"$TAG-amd64"
      "$CI_PROJECT_PATH":"$TAG-arm64"
      "$CI_PROJECT_PATH":"$TAG-arm"
    - docker manifest push -p "$CI_PROJECT_PATH":latest

github:
  <<: *common_release
  stage: release
  script:
    - apk add git
    - git remote set-url origin "https://$GITHUB_USER:$GITHUB_TOKEN@github.com/$CI_PROJECT_PATH.git"
    - git tag $TAG
    - git push --tags

pypi:
  <<: *common_release
  image: python:3.7.3-alpine
  stage: release
  cache:
    key: "ouroboros-cache"
    paths:
      - .cache/pip
      - venv/
  variables:
    PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
  tags:
    - shared
  before_script:
    - python -m venv venv
    - source venv/bin/activate
    - pip install -U twine
  script:
    - venv/bin/python setup.py sdist && venv/bin/python -m twine upload --skip-existing -u ${PYPI_CREDS_USR} -p ${PYPI_CREDS_PSW} dist/*

================================================
FILE: CHANGELOG.md
================================================
# Change Log

## [1.4.3](https://github.com/pyouroboros/ouroboros/tree/1.4.3) (2019-12-11)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.4.3...1.4.2)

**Fixed bugs:**

- Join Notifications truncated and icon returning 404 [\#325](https://github.com/pyouroboros/ouroboros/issues/325)

**Other Pull Requests**

- v1.4.3 Merge [\#354](https://github.com/pyouroboros/ouroboros/pull/354) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- v1.4.3 to develop [\#353](https://github.com/pyouroboros/ouroboros/pull/353) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Update requirements.txt [\#349](https://github.com/pyouroboros/ouroboros/pull/349) ([nemchik](https://github.com/nemchik))

## [1.4.2](https://github.com/pyouroboros/ouroboros/tree/1.4.2) (2019-08-01)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.4.1...1.4.2)

**Other Pull Requests**

- 1.4.2 Merge [\#327](https://github.com/pyouroboros/ouroboros/pull/327) ([circa10a](https://github.com/circa10a))
- 1.4.2 to develop [\#326](https://github.com/pyouroboros/ouroboros/pull/326) ([circa10a](https://github.com/circa10a))

## [1.4.1](https://github.com/pyouroboros/ouroboros/tree/1.4.1) (2019-06-04)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/v1.4.0...1.4.1)

**Other Pull Requests**

- v1.4.1 Merge [\#315](https://github.com/pyouroboros/ouroboros/pull/315) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- v1.4.1 to develop [\#314](https://github.com/pyouroboros/ouroboros/pull/314) ([DirtyCajunRice](https://github.com/DirtyCajunRice))

## [v1.4.0](https://github.com/pyouroboros/ouroboros/tree/v1.4.0) (2019-04-25)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.3.1...v1.4.0)

**Implemented enhancements:**

- Make startup notification optional [\#253](https://github.com/pyouroboros/ouroboros/issues/253)

**Fixed bugs:**

- Missing MANIFEST.in file causes pypi install to fail [\#284](https://github.com/pyouroboros/ouroboros/issues/284)
- Healthcheck section not re-applied after container update [\#275](https://github.com/pyouroboros/ouroboros/issues/275)
- docker-compose local and remote tls logger location [\#273](https://github.com/pyouroboros/ouroboros/issues/273)
- Self update errors. Not deleting old self [\#262](https://github.com/pyouroboros/ouroboros/issues/262)
- ouroboros sets fixed IP addresses [\#254](https://github.com/pyouroboros/ouroboros/issues/254)

**Closed issues:**

- Update apprise to v0.7.4 [\#266](https://github.com/pyouroboros/ouroboros/issues/266) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)]
- 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)]

**Other Pull Requests**

- v1.4.0 Merge [\#299](https://github.com/pyouroboros/ouroboros/pull/299) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Remove Watchtower Reference [\#298](https://github.com/pyouroboros/ouroboros/pull/298) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- v1.4.0 to develop [\#297](https://github.com/pyouroboros/ouroboros/pull/297) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- fixes \#284 [\#296](https://github.com/pyouroboros/ouroboros/pull/296) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Patches [\#295](https://github.com/pyouroboros/ouroboros/pull/295) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Fix healthcheck attr [\#294](https://github.com/pyouroboros/ouroboros/pull/294) ([circa10a](https://github.com/circa10a))
- Patch/catch up [\#271](https://github.com/pyouroboros/ouroboros/pull/271) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Arg skip startup notifications [\#261](https://github.com/pyouroboros/ouroboros/pull/261) ([circa10a](https://github.com/circa10a))
- Revert "add option to skip startup notifications" [\#260](https://github.com/pyouroboros/ouroboros/pull/260) ([circa10a](https://github.com/circa10a))
- add option to skip startup notifications [\#259](https://github.com/pyouroboros/ouroboros/pull/259) ([circa10a](https://github.com/circa10a))

## [1.3.1](https://github.com/pyouroboros/ouroboros/tree/1.3.1) (2019-02-28)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.3.0...1.3.1)

**Fixed bugs:**

- Since 1.3.0, docker login fails [\#243](https://github.com/pyouroboros/ouroboros/issues/243)
- Catch Failed self-updates [\#230](https://github.com/pyouroboros/ouroboros/issues/230)

**Other Pull Requests**

- v1.3.1 Merge [\#249](https://github.com/pyouroboros/ouroboros/pull/249) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- v1.3.1 to develop [\#248](https://github.com/pyouroboros/ouroboros/pull/248) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- fix name subscript for \#243 [\#247](https://github.com/pyouroboros/ouroboros/pull/247) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- fixes \#230 and \#243 [\#242](https://github.com/pyouroboros/ouroboros/pull/242) ([DirtyCajunRice](https://github.com/DirtyCajunRice))

## [1.3.0](https://github.com/pyouroboros/ouroboros/tree/1.3.0) (2019-02-25)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.2.1...1.3.0)

**Implemented enhancements:**

- Start new container in detached mode [\#222](https://github.com/pyouroboros/ouroboros/pull/222) ([nightvisi0n](https://github.com/nightvisi0n))
- Optimise dockerfile layers [\#218](https://github.com/pyouroboros/ouroboros/pull/218) ([nightvisi0n](https://github.com/nightvisi0n))

**Fixed bugs:**

- Cron scheduled missed following successful runs [\#229](https://github.com/pyouroboros/ouroboros/issues/229)
- Catch attribute.id error [\#226](https://github.com/pyouroboros/ouroboros/issues/226)
- AttachStdout and AttachStderr are not carried over properly [\#221](https://github.com/pyouroboros/ouroboros/issues/221)
- Exception when updating container started with --rm \(autoremove\)  [\#219](https://github.com/pyouroboros/ouroboros/issues/219)
- Issue with Swarm Mode V2 [\#216](https://github.com/pyouroboros/ouroboros/issues/216)
- Fix docker swarm mode [\#227](https://github.com/pyouroboros/ouroboros/pull/227) ([mathcantin](https://github.com/mathcantin))

**Other Pull Requests**

- v1.3.0 Merge [\#241](https://github.com/pyouroboros/ouroboros/pull/241) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- v1.3.0 to develop [\#240](https://github.com/pyouroboros/ouroboros/pull/240) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Catch self update apierror [\#238](https://github.com/pyouroboros/ouroboros/pull/238) ([circa10a](https://github.com/circa10a))
- Catch attribute error [\#237](https://github.com/pyouroboros/ouroboros/pull/237) ([circa10a](https://github.com/circa10a))
- Check for autoremove [\#236](https://github.com/pyouroboros/ouroboros/pull/236) ([circa10a](https://github.com/circa10a))
- Add misfire\_grace\_time for cron scheduler [\#234](https://github.com/pyouroboros/ouroboros/pull/234) ([circa10a](https://github.com/circa10a))
- 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))
- remove git in pypi + branch develop + version bump + maintainer\_email [\#214](https://github.com/pyouroboros/ouroboros/pull/214) ([DirtyCajunRice](https://github.com/DirtyCajunRice))

## [1.2.1](https://github.com/pyouroboros/ouroboros/tree/1.2.1) (2019-02-14)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.2.0...1.2.1)

**Fixed bugs:**

- Broken when no :tag specified  [\#210](https://github.com/pyouroboros/ouroboros/issues/210)

**Other Pull Requests**

- v1.2.1 Merge [\#213](https://github.com/pyouroboros/ouroboros/pull/213) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- v1.2.1 to develop [\#212](https://github.com/pyouroboros/ouroboros/pull/212) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- fixes \#210 [\#211](https://github.com/pyouroboros/ouroboros/pull/211) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- version bump to 1.2.1 + develop branch + twine fix + … [\#209](https://github.com/pyouroboros/ouroboros/pull/209) ([DirtyCajunRice](https://github.com/DirtyCajunRice))

## [1.2.0](https://github.com/pyouroboros/ouroboros/tree/1.2.0) (2019-02-14)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.1.2...1.2.0)

**Implemented enhancements:**

- Move "Interval container update" messages to debug log level [\#194](https://github.com/pyouroboros/ouroboros/issues/194)
- \[Feature Request\] Support for Swarm Services [\#178](https://github.com/pyouroboros/ouroboros/issues/178)
- Add Warning for label\_enable not set while using labels\_only [\#202](https://github.com/pyouroboros/ouroboros/pull/202) ([larsderidder](https://github.com/larsderidder))

**Fixed bugs:**

- Change depends\_on logic [\#198](https://github.com/pyouroboros/ouroboros/issues/198)
- 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)
- Exception when trying to update container with complex compose networks [\#196](https://github.com/pyouroboros/ouroboros/issues/196)
- Problem with network IPv4 address carry-over [\#193](https://github.com/pyouroboros/ouroboros/issues/193)
- 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))

**Closed issues:**

- 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)]
- Add environment variables in Wiki [\#203](https://github.com/pyouroboros/ouroboros/issues/203) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)]
- Slack notifications via webhook not working [\#187](https://github.com/pyouroboros/ouroboros/issues/187)

**Other Pull Requests**

- v1.2.0 Merge [\#208](https://github.com/pyouroboros/ouroboros/pull/208) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- v1.2.0 to develop [\#207](https://github.com/pyouroboros/ouroboros/pull/207) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Patch/tag bug [\#205](https://github.com/pyouroboros/ouroboros/pull/205) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Patch/group 5 [\#201](https://github.com/pyouroboros/ouroboros/pull/201) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Fix bug in user defined network detection [\#200](https://github.com/pyouroboros/ouroboros/pull/200) ([nightvisi0n](https://github.com/nightvisi0n))
- Adjust apscheduler logger [\#199](https://github.com/pyouroboros/ouroboros/pull/199) ([circa10a](https://github.com/circa10a))
- Carry over network config [\#195](https://github.com/pyouroboros/ouroboros/pull/195) ([nightvisi0n](https://github.com/nightvisi0n))
- Jenkins tweaks [\#192](https://github.com/pyouroboros/ouroboros/pull/192) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Swarm + Jenkins [\#188](https://github.com/pyouroboros/ouroboros/pull/188) ([DirtyCajunRice](https://github.com/DirtyCajunRice))

## [1.1.2](https://github.com/pyouroboros/ouroboros/tree/1.1.2) (2019-02-02)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.1.1...1.1.2)

**Fixed bugs:**

- No default timezone [\#176](https://github.com/pyouroboros/ouroboros/issues/176)

**Closed issues:**

- cron documentation example update [\#182](https://github.com/pyouroboros/ouroboros/issues/182) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)]

**Other Pull Requests**

- v1.1.2 Merge [\#186](https://github.com/pyouroboros/ouroboros/pull/186) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- v1.1.2 to develop [\#183](https://github.com/pyouroboros/ouroboros/pull/183) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Fix default timezone [\#177](https://github.com/pyouroboros/ouroboros/pull/177) ([circa10a](https://github.com/circa10a))

## [1.1.1](https://github.com/pyouroboros/ouroboros/tree/1.1.1) (2019-02-01)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.1.0...1.1.1)

**Implemented enhancements:**

- Support for adding an identifier \(hostname?\) to notifications [\#158](https://github.com/pyouroboros/ouroboros/issues/158)
- 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))
- add cli arg for cron [\#157](https://github.com/pyouroboros/ouroboros/pull/157) ([DirtyCajunRice](https://github.com/DirtyCajunRice))

**Fixed bugs:**

- Ouroboros does not respect MONITOR= [\#166](https://github.com/pyouroboros/ouroboros/issues/166)
- Docker TLS over TCP connections [\#154](https://github.com/pyouroboros/ouroboros/issues/154) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)]
- Patch/group 4 [\#169](https://github.com/pyouroboros/ouroboros/pull/169) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Recheck properly for only non lists [\#164](https://github.com/pyouroboros/ouroboros/pull/164) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- add some missing passthrough info for restart [\#163](https://github.com/pyouroboros/ouroboros/pull/163) ([DirtyCajunRice](https://github.com/DirtyCajunRice))

**Other Pull Requests**

- v1.1.1 Merge [\#173](https://github.com/pyouroboros/ouroboros/pull/173) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- v1.1.1 to develop [\#172](https://github.com/pyouroboros/ouroboros/pull/172) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Patch/group 3 [\#167](https://github.com/pyouroboros/ouroboros/pull/167) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Add hostname to the notifications [\#161](https://github.com/pyouroboros/ouroboros/pull/161) ([tlkamp](https://github.com/tlkamp))
- Patch/group 2 [\#155](https://github.com/pyouroboros/ouroboros/pull/155) ([DirtyCajunRice](https://github.com/DirtyCajunRice))

## [1.1.0](https://github.com/pyouroboros/ouroboros/tree/1.1.0) (2019-01-26)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.0.0...1.1.0)

**Implemented enhancements:**

- Notification via Telegram [\#146](https://github.com/pyouroboros/ouroboros/issues/146)
- Add flag to allow a labels\_only condition [\#142](https://github.com/pyouroboros/ouroboros/issues/142)
- DRY\_RUN flag [\#140](https://github.com/pyouroboros/ouroboros/issues/140)
- Notification on startup [\#138](https://github.com/pyouroboros/ouroboros/issues/138)
- Start/Stop containers in sequence [\#106](https://github.com/pyouroboros/ouroboros/issues/106)
- 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))

**Fixed bugs:**

- Catch invalid docker socket config [\#148](https://github.com/pyouroboros/ouroboros/issues/148)
- Explicitly Define true/false [\#141](https://github.com/pyouroboros/ouroboros/issues/141) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)]

**Other Pull Requests**

- v1.1.0 Merge [\#153](https://github.com/pyouroboros/ouroboros/pull/153) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- v1.1.0 to develop [\#152](https://github.com/pyouroboros/ouroboros/pull/152) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Patch/group 1 [\#150](https://github.com/pyouroboros/ouroboros/pull/150) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Add volume for docker socket path [\#144](https://github.com/pyouroboros/ouroboros/pull/144) ([mauvehed](https://github.com/mauvehed))

## [1.0.0](https://github.com/pyouroboros/ouroboros/tree/1.0.0) (2019-01-23)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.6.0...1.0.0)

**Implemented enhancements:**

- Stop containers with alternate signal [\#107](https://github.com/pyouroboros/ouroboros/issues/107)
- Docker Socket secure connections [\#105](https://github.com/pyouroboros/ouroboros/issues/105)
- Selectively monitor containers with label [\#104](https://github.com/pyouroboros/ouroboros/issues/104)
- Allow stop-signal label [\#133](https://github.com/pyouroboros/ouroboros/pull/133) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Docker TLS Verify option [\#132](https://github.com/pyouroboros/ouroboros/pull/132) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- add label priority feature for watch/ignore. Addresses \#104 [\#121](https://github.com/pyouroboros/ouroboros/pull/121) ([DirtyCajunRice](https://github.com/DirtyCajunRice))

**Fixed bugs:**

- Unexpected docker API causes program to quit ‘500 Server Error: Internal Server Error’ [\#130](https://github.com/pyouroboros/ouroboros/issues/130)
- Error tag handling under the registry with port [\#129](https://github.com/pyouroboros/ouroboros/issues/129)
- a fatal error when none tag image [\#122](https://github.com/pyouroboros/ouroboros/issues/122)
- Bug/ignore logic [\#135](https://github.com/pyouroboros/ouroboros/pull/135) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Bug/registry logic [\#131](https://github.com/pyouroboros/ouroboros/pull/131) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- catch no tags in get\_running [\#124](https://github.com/pyouroboros/ouroboros/pull/124) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- 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))

**Closed issues:**

- Missing docker-compose.yml from documentation [\#120](https://github.com/pyouroboros/ouroboros/issues/120) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)]
- Wiki usage docs reference old argument names [\#115](https://github.com/pyouroboros/ouroboros/issues/115) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)]

**Other Pull Requests**

- v1.0.0 Merge [\#137](https://github.com/pyouroboros/ouroboros/pull/137) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- v1.0.0 to develop [\#136](https://github.com/pyouroboros/ouroboros/pull/136) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- 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))
- Cleanup/qemu logic [\#128](https://github.com/pyouroboros/ouroboros/pull/128) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- 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))

## [0.6.0](https://github.com/pyouroboros/ouroboros/tree/0.6.0) (2019-01-17)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.5.0...0.6.0)

**Implemented enhancements:**

- Support multi-architecture Docker images [\#78](https://github.com/pyouroboros/ouroboros/issues/78)
- Mail notification [\#59](https://github.com/pyouroboros/ouroboros/issues/59)
- Multi architecture docker [\#110](https://github.com/pyouroboros/ouroboros/pull/110) ([circa10a](https://github.com/circa10a))
- added logo to readme [\#109](https://github.com/pyouroboros/ouroboros/pull/109) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Feature/ouroboros self\_update [\#103](https://github.com/pyouroboros/ouroboros/pull/103) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- add version cli arg [\#100](https://github.com/pyouroboros/ouroboros/pull/100) ([circa10a](https://github.com/circa10a))
- added email notifications. Addresses \#59 [\#97](https://github.com/pyouroboros/ouroboros/pull/97) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Documentation [\#96](https://github.com/pyouroboros/ouroboros/pull/96) ([DirtyCajunRice](https://github.com/DirtyCajunRice))

**Fixed bugs:**

- Ignore not working as expected [\#98](https://github.com/pyouroboros/ouroboros/issues/98)
- specify for specificity! [\#114](https://github.com/pyouroboros/ouroboros/pull/114) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- manifesting failures [\#113](https://github.com/pyouroboros/ouroboros/pull/113) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- sigh. [\#112](https://github.com/pyouroboros/ouroboros/pull/112) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Multiarch/fine tuning [\#111](https://github.com/pyouroboros/ouroboros/pull/111) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- catch index error and account for shared images, x [\#102](https://github.com/pyouroboros/ouroboros/pull/102) ([circa10a](https://github.com/circa10a))
- add monitor/ignore to list sanity check. Fixes \#98 [\#99](https://github.com/pyouroboros/ouroboros/pull/99) ([DirtyCajunRice](https://github.com/DirtyCajunRice))

**Other Pull Requests**

- v0.6.0 to develop [\#118](https://github.com/pyouroboros/ouroboros/pull/118) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- v0.6.0 Merge [\#117](https://github.com/pyouroboros/ouroboros/pull/117) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- 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))

## [0.5.0](https://github.com/pyouroboros/ouroboros/tree/0.5.0) (2019-01-13)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.4.3...0.5.0)

**Implemented enhancements:**

- Auto discover slack/discord notifications in WEBHOOK\_URLS [\#83](https://github.com/pyouroboros/ouroboros/issues/83)
- Add to schedule logic run now [\#75](https://github.com/pyouroboros/ouroboros/issues/75)
- add pushover functionality. Finishes other half of \#80 [\#93](https://github.com/pyouroboros/ouroboros/pull/93) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- add keep\_alive url for healthchecks. Addresses half of \#80 [\#89](https://github.com/pyouroboros/ouroboros/pull/89) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- changed webhook json to auto-deciding + fixed RUN\_ONCE no underscore [\#86](https://github.com/pyouroboros/ouroboros/pull/86) ([DirtyCajunRice](https://github.com/DirtyCajunRice))
- Refactor [\#79](https://github.com/pyouroboros/ouroboros/pull/79) ([DirtyCajunRice](https://github.com/DirtyCajunRice))

**Fixed bugs:**

- Fix log level case sensitivity [\#82](https://github.com/pyouroboros/ouroboros/issues/82)
- Invalid URL 'h': No schema supplied. Perhaps you meant http://h? [\#76](https://github.com/pyouroboros/ouroboros/issues/76)
- Installation via pip fails [\#73](https://github.com/pyouroboros/ouroboros/issues/73)
- Added try except [\#95](https://github.com/pyouroboros/ouroboros/pull/95) ([circa10a](https://github.com/circa10a))
- Fix dockerfile [\#92](https://github.com/pyouroboros/ouroboros/pull/92) ([circa10a](https://github.com/circa10a))
- use ouroboros script in dockerfile [\#91](https://github.com/pyouroboros/ouroboros/pull/91) ([circa10a](https://github.com/circa10a))
- fix deploy script to push git tags [\#90](https://github.com/pyouroboros/ouroboros/pull/90) ([circa10a](https://github.com/circa10a))
- change pypi travis username [\#88](https://github.com/pyouroboros/ouroboros/pull/88) ([circa10a](https://github.com/circa10a))
- install flake8 for travis, run on appropriate directories [\#87](https://github.com/pyouroboros/ouroboros/pull/87) ([circa10a](https://github.com/circa10a))
- 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))
- change loglevel to use upper\(\) [\#84](https://github.com/pyouroboros/ouroboros/pull/84) ([circa10a](https://github.com/circa10a))
- Prometheus bind fix, org rename [\#81](https://github.com/pyouroboros/ouroboros/pull/81) ([circa10a](https://github.com/circa10a))

## [0.4.3](https://github.com/pyouroboros/ouroboros/tree/0.4.3) (2019-01-09)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.4.2...0.4.3)

**Implemented enhancements:**

- grafana to metrics/prometheus endpoint [\#74](https://github.com/pyouroboros/ouroboros/issues/74)
- add aarch64 docker image [\#77](https://github.com/pyouroboros/ouroboros/pull/77) ([circa10a](https://github.com/circa10a))

## [0.4.2](https://github.com/pyouroboros/ouroboros/tree/0.4.2) (2019-01-08)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.4.1...0.4.2)

**Implemented enhancements:**

- Add autopep8 to the pre-merge checks [\#30](https://github.com/pyouroboros/ouroboros/issues/30)

## [0.4.1](https://github.com/pyouroboros/ouroboros/tree/0.4.1) (2018-12-30)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.4.0...0.4.1)

**Implemented enhancements:**

- Pre merge code quality checks [\#72](https://github.com/pyouroboros/ouroboros/pull/72) ([circa10a](https://github.com/circa10a))

## [0.4.0](https://github.com/pyouroboros/ouroboros/tree/0.4.0) (2018-12-30)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.7...0.4.0)

**Implemented enhancements:**

- Slack notification  [\#61](https://github.com/pyouroboros/ouroboros/issues/61)
- Webhook notifications [\#71](https://github.com/pyouroboros/ouroboros/pull/71) ([circa10a](https://github.com/circa10a))

## [0.3.7](https://github.com/pyouroboros/ouroboros/tree/0.3.7) (2018-12-26)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.6...0.3.7)

**Implemented enhancements:**

- Timezone Support [\#68](https://github.com/pyouroboros/ouroboros/issues/68)
- Add output to log at container start [\#66](https://github.com/pyouroboros/ouroboros/issues/66)
- Enable Timezone Configuration [\#69](https://github.com/pyouroboros/ouroboros/pull/69) ([circa10a](https://github.com/circa10a))

## [0.3.6](https://github.com/pyouroboros/ouroboros/tree/0.3.6) (2018-12-21)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.5...0.3.6)

**Implemented enhancements:**

- print ouroboros configuration on startup [\#67](https://github.com/pyouroboros/ouroboros/pull/67) ([circa10a](https://github.com/circa10a))

## [0.3.5](https://github.com/pyouroboros/ouroboros/tree/0.3.5) (2018-12-20)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.4...0.3.5)

**Implemented enhancements:**

- Raspberry Pi compatible docker image [\#62](https://github.com/pyouroboros/ouroboros/issues/62)
- Scheduling docs [\#65](https://github.com/pyouroboros/ouroboros/pull/65) ([circa10a](https://github.com/circa10a))

## [0.3.4](https://github.com/pyouroboros/ouroboros/tree/0.3.4) (2018-12-19)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.3...0.3.4)

**Implemented enhancements:**

- Rpi docker image [\#64](https://github.com/pyouroboros/ouroboros/pull/64) ([circa10a](https://github.com/circa10a))

## [0.3.3](https://github.com/pyouroboros/ouroboros/tree/0.3.3) (2018-11-29)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.2...0.3.3)

**Implemented enhancements:**

- add docs, bump version [\#58](https://github.com/pyouroboros/ouroboros/pull/58) ([circa10a](https://github.com/circa10a))

**Fixed bugs:**

- Problem accessing private registry [\#55](https://github.com/pyouroboros/ouroboros/issues/55)

**Closed issues:**

- Q: Add config file? [\#46](https://github.com/pyouroboros/ouroboros/issues/46) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)]

## [0.3.2](https://github.com/pyouroboros/ouroboros/tree/0.3.2) (2018-11-28)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.1...0.3.2)

**Fixed bugs:**

- unrecognized arguments [\#52](https://github.com/pyouroboros/ouroboros/issues/52)
- Fix config json [\#56](https://github.com/pyouroboros/ouroboros/pull/56) ([circa10a](https://github.com/circa10a))

## [0.3.1](https://github.com/pyouroboros/ouroboros/tree/0.3.1) (2018-11-16)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.0...0.3.1)

**Implemented enhancements:**

- Add Prometheus endpoint [\#23](https://github.com/pyouroboros/ouroboros/issues/23) [[hacktoberfest](https://github.com/pyouroboros/ouroboros/labels/hacktoberfest)]

**Fixed bugs:**

- fix bind address bug [\#53](https://github.com/pyouroboros/ouroboros/pull/53) ([circa10a](https://github.com/circa10a))

## [0.3.0](https://github.com/pyouroboros/ouroboros/tree/0.3.0) (2018-11-15)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.2.3...0.3.0)

**Implemented enhancements:**

- Q: continue to update to latest or same tag [\#43](https://github.com/pyouroboros/ouroboros/issues/43)
- Metrics [\#51](https://github.com/pyouroboros/ouroboros/pull/51) ([circa10a](https://github.com/circa10a))
- Disable pip cache in Dockerfile [\#50](https://github.com/pyouroboros/ouroboros/pull/50) ([Strayer](https://github.com/Strayer))

## [0.2.3](https://github.com/pyouroboros/ouroboros/tree/0.2.3) (2018-11-08)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.2.2...0.2.3)

**Implemented enhancements:**

- Keep tags [\#48](https://github.com/pyouroboros/ouroboros/pull/48) ([circa10a](https://github.com/circa10a))

## [0.2.2](https://github.com/pyouroboros/ouroboros/tree/0.2.2) (2018-11-03)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.2.1...0.2.2)

**Implemented enhancements:**

- Add ability to ignore select containers [\#35](https://github.com/pyouroboros/ouroboros/issues/35)
- Ignore containers [\#45](https://github.com/pyouroboros/ouroboros/pull/45) ([tlkamp](https://github.com/tlkamp))
- Update setup.py, travis param [\#42](https://github.com/pyouroboros/ouroboros/pull/42) ([circa10a](https://github.com/circa10a))

## [0.2.1](https://github.com/pyouroboros/ouroboros/tree/0.2.1) (2018-10-28)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.1.3...0.2.1)

**Implemented enhancements:**

- Option precedence [\#32](https://github.com/pyouroboros/ouroboros/issues/32)
- Add ouroboros to the user's path automagically [\#28](https://github.com/pyouroboros/ouroboros/issues/28)
- Deploy to Pypi [\#41](https://github.com/pyouroboros/ouroboros/pull/41) ([circa10a](https://github.com/circa10a))
- Add setup.py [\#40](https://github.com/pyouroboros/ouroboros/pull/40) ([tlkamp](https://github.com/tlkamp))
- change branch to master [\#39](https://github.com/pyouroboros/ouroboros/pull/39) ([circa10a](https://github.com/circa10a))
- Move api client out of cli.py [\#38](https://github.com/pyouroboros/ouroboros/pull/38) ([tlkamp](https://github.com/tlkamp))
- Handle the exceptions better in cli.py [\#36](https://github.com/pyouroboros/ouroboros/pull/36) ([tlkamp](https://github.com/tlkamp))

**Closed issues:**

- \[question\] network\_mode: "service:XXX" ? [\#33](https://github.com/pyouroboros/ouroboros/issues/33) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)]

**Other Pull Requests**

- 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))
- update docs [\#34](https://github.com/pyouroboros/ouroboros/pull/34) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] ([circa10a](https://github.com/circa10a))

## [0.1.3](https://github.com/pyouroboros/ouroboros/tree/0.1.3) (2018-10-25)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.1.2...0.1.3)

**Implemented enhancements:**

- Make CLI expose fewer globals, formatting [\#31](https://github.com/pyouroboros/ouroboros/pull/31) ([tlkamp](https://github.com/tlkamp))

## [0.1.2](https://github.com/pyouroboros/ouroboros/tree/0.1.2) (2018-10-24)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.1.1...0.1.2)

**Implemented enhancements:**

- Rewrite script to use vendor packages if possible [\#25](https://github.com/pyouroboros/ouroboros/pull/25) ([dannysauer](https://github.com/dannysauer))
- Improve URL matching Regex [\#24](https://github.com/pyouroboros/ouroboros/pull/24) ([dannysauer](https://github.com/dannysauer))
- Add environment files to the project for those working with Conda [\#22](https://github.com/pyouroboros/ouroboros/pull/22) ([tlkamp](https://github.com/tlkamp))

**Other Pull Requests**

- 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))
- 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))

## [0.1.1](https://github.com/pyouroboros/ouroboros/tree/0.1.1) (2018-10-21)
[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.1.0...0.1.1)

## [0.1.0](https://github.com/pyouroboros/ouroboros/tree/0.1.0) (2018-10-21)
**Implemented enhancements:**

- account for environment variables [\#19](https://github.com/pyouroboros/ouroboros/issues/19)
- Support private repos [\#10](https://github.com/pyouroboros/ouroboros/issues/10)
- Deploy to pypi [\#5](https://github.com/pyouroboros/ouroboros/issues/5)
- Create travis build [\#4](https://github.com/pyouroboros/ouroboros/issues/4)
- Rewrite new container class [\#3](https://github.com/pyouroboros/ouroboros/issues/3)
- Write Unit Tests [\#2](https://github.com/pyouroboros/ouroboros/issues/2)
- Add CLI Args [\#1](https://github.com/pyouroboros/ouroboros/issues/1)
- added support for private registries [\#12](https://github.com/pyouroboros/ouroboros/pull/12) ([circa10a](https://github.com/circa10a))
- Torpus cli args [\#11](https://github.com/pyouroboros/ouroboros/pull/11) ([Torpus](https://github.com/Torpus))
- single client [\#9](https://github.com/pyouroboros/ouroboros/pull/9) ([circa10a](https://github.com/circa10a))
- the less code the better [\#8](https://github.com/pyouroboros/ouroboros/pull/8) ([circa10a](https://github.com/circa10a))
- Initial stuff [\#6](https://github.com/pyouroboros/ouroboros/pull/6) ([circa10a](https://github.com/circa10a))

**Closed issues:**

- Create good docs [\#7](https://github.com/pyouroboros/ouroboros/issues/7) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)]

**Other Pull Requests**

- Docs [\#21](https://github.com/pyouroboros/ouroboros/pull/21) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] ([circa10a](https://github.com/circa10a))
- Tests [\#20](https://github.com/pyouroboros/ouroboros/pull/20) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a))
- Add travis [\#18](https://github.com/pyouroboros/ouroboros/pull/18) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a))
- Tests [\#17](https://github.com/pyouroboros/ouroboros/pull/17) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a))
- Tests [\#16](https://github.com/pyouroboros/ouroboros/pull/16) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a))
- Tests [\#15](https://github.com/pyouroboros/ouroboros/pull/15) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a))
- Tests [\#14](https://github.com/pyouroboros/ouroboros/pull/14) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a))
- Tests [\#13](https://github.com/pyouroboros/ouroboros/pull/13) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a))


================================================
FILE: Dockerfile
================================================
FROM amd64/python:3.8.5-alpine

LABEL maintainers="dirtycajunrice,circa10a"

ENV TZ UTC

WORKDIR /app

COPY /requirements.txt /setup.py /ouroboros /README.md /app/

RUN apk add --no-cache tzdata && \
    pip install --no-cache-dir -r requirements.txt

COPY /pyouroboros /app/pyouroboros

RUN pip install --no-cache-dir .

ENTRYPOINT ["ouroboros"]

================================================
FILE: Dockerfile.arm
================================================
FROM arm32v6/python:3.8.5-alpine

LABEL maintainers="dirtycajunrice,circa10a"

ENV TZ UTC

WORKDIR /app

COPY /requirements.txt /setup.py /ouroboros /README.md /app/

RUN apk add --no-cache tzdata && \
    pip install --no-cache-dir -r requirements.txt

COPY /pyouroboros /app/pyouroboros

RUN pip install --no-cache-dir .

ENTRYPOINT ["ouroboros"]

================================================
FILE: Dockerfile.arm64
================================================
FROM arm64v8/python:3.8.5-alpine

LABEL maintainers="dirtycajunrice,circa10a"

ENV TZ UTC

WORKDIR /app

COPY /requirements.txt /setup.py /ouroboros /README.md /app/

RUN apk add --no-cache tzdata && \
    pip install --no-cache-dir -r requirements.txt

COPY /pyouroboros /app/pyouroboros

RUN pip install --no-cache-dir .

ENTRYPOINT ["ouroboros"]

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2018 Caleb Lemoine

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: MANIFEST.in
================================================
include requirements.txt
include README.md

================================================
FILE: README.md
================================================
⚠️⚠️⚠️ ouroboros is no longer in development. It does its job (more or less) and the devs have succumb to real life! Please feel free to fork and maintain as you wish. We appreciate all of the support in the last year :). After support from the community, automated version bumps will continue to try to keep ouroboros in check with dependencies. ⚠️⚠️⚠️
<img width="800" src="https://raw.githubusercontent.com/pyouroboros/ouroboros/master/assets/ouroboros_logo_primary_long_cropped.jpg" alt="Ouroboros Logo">

[![Discord](https://img.shields.io/discord/532695326117593112.svg?colorB=7289DA&label=Discord&logo=Discord&logoColor=7289DA&style=flat-square)](https://discord.gg/qHNByUW)
[![Release](https://img.shields.io/github/release/pyouroboros/ouroboros.svg?style=flat-square)](https://hub.docker.com/r/pyouroboros/ouroboros/)
[![Python Version](https://img.shields.io/pypi/pyversions/ouroboros-cli.svg?style=flat-square)](https://pypi.org/project/ouroboros-cli/)
[![Docker Pulls](https://img.shields.io/docker/pulls/pyouroboros/ouroboros.svg?style=flat-square)](https://hub.docker.com/r/pyouroboros/ouroboros/)
[![Layers](https://images.microbadger.com/badges/image/pyouroboros/ouroboros.svg)](https://microbadger.com/images/pyouroboros/ouroboros)  

Automatically update your running Docker containers to the latest available image.

The de-facto standard for docker update automation

## Overview

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.

- Push your image to your registry and simply wait your defined interval for ouroboros to find the new image and redeploy your container autonomously.
- Notify you via many platforms courtesy of [Apprise](https://github.com/caronc/apprise) 
- Serve metrics for trend monitoring (Currently: Prometheus/Influxdb)
- Limit your server ssh access
- `ssh -i key server.domainname "docker pull ... && docker run ..."` is for scrubs
- `docker-compose pull && docker-compose up -d` is for fancier scrubs

## Getting Started

More detailed usage and configuration can be found on [the wiki](https://github.com/pyouroboros/ouroboros/wiki).

### Docker

Ouroboros is deployed via docker image like so:

```bash
docker run -d --name ouroboros \
  -v /var/run/docker.sock:/var/run/docker.sock \
  pyouroboros/ouroboros
```

> This is image is compatible for amd64, arm32, and arm64 CPU architectures

or via `docker-compose`:

[Official Example](https://github.com/pyouroboros/ouroboros/blob/master/docker-compose.yml)

### Pip

Ouroboros can also be installed via `pip`:

```bash
pip install ouroboros-cli
```

And can then be invoked using the `ouroboros` command:

```bash
$ ouroboros --interval 300 --log-level debug
```

> This can be useful if you would like to create a `systemd` service or similar daemon that doesn't run in a container

## Examples
Per-command and scenario examples can be found in the [wiki](https://github.com/pyouroboros/ouroboros/wiki/Usage)

## Contributing

All contributions are welcome! Contributing guidelines are in the works


================================================
FILE: docker-compose.yml
================================================
version: '3'
services:
  ouroboros:
    container_name: ouroboros
    hostname: ouroboros
    image: pyouroboros/ouroboros
    environment:
      - CLEANUP=true
      - INTERVAL=300
      - LOG_LEVEL=info
      - SELF_UPDATE=true
      - IGNORE=mongo influxdb postgres mariadb
      - TZ=America/Chicago
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock


================================================
FILE: ouroboros
================================================
#!/usr/bin/env python3
from pyouroboros.ouroboros import main
# stub to allow cli call
if __name__ == "__main__":
    main()


================================================
FILE: pyouroboros/__init__.py
================================================
VERSION = "1.4.3"
BRANCH = "master"


================================================
FILE: pyouroboros/config.py
================================================
from os import environ
from logging import getLogger
from pyouroboros.logger import BlacklistFilter


class Config(object):
    options = ['INTERVAL', 'PROMETHEUS', 'DOCKER_SOCKETS', 'MONITOR', 'IGNORE', 'LOG_LEVEL', 'PROMETHEUS_ADDR',
               'PROMETHEUS_PORT', 'NOTIFIERS', 'REPO_USER', 'REPO_PASS', 'CLEANUP', 'RUN_ONCE', 'CRON',
               'INFLUX_URL', 'INFLUX_PORT', 'INFLUX_USERNAME', 'INFLUX_PASSWORD', 'INFLUX_DATABASE', 'INFLUX_SSL',
               'INFLUX_VERIFY_SSL', 'DATA_EXPORT', 'SELF_UPDATE', 'LABEL_ENABLE', 'DOCKER_TLS', 'LABELS_ONLY',
               'DRY_RUN', 'HOSTNAME', 'DOCKER_TLS_VERIFY', 'SWARM', 'SKIP_STARTUP_NOTIFICATIONS']

    hostname = environ.get('HOSTNAME')
    interval = 300
    cron = None
    docker_sockets = 'unix://var/run/docker.sock'
    docker_tls = False
    docker_tls_verify = True
    swarm = False
    monitor = []
    ignore = []
    data_export = None
    log_level = 'info'
    cleanup = False
    run_once = False
    dry_run = False
    self_update = False
    label_enable = False
    labels_only = False

    repo_user = None
    repo_pass = None
    auth_json = None

    prometheus = False
    prometheus_addr = '127.0.0.1'
    prometheus_port = 8000

    influx_url = '127.0.0.1'
    influx_port = 8086
    influx_ssl = False
    influx_verify_ssl = False
    influx_username = 'root'
    influx_password = 'root'
    influx_database = None

    notifiers = []
    skip_startup_notifications = False

    def __init__(self, environment_vars, cli_args):
        self.cli_args = cli_args
        self.environment_vars = environment_vars
        self.filtered_strings = None

        self.logger = getLogger()
        self.parse()

    def config_blacklist(self):
        filtered_strings = [getattr(self, key.lower()) for key in Config.options
                            if key.lower() in BlacklistFilter.blacklisted_keys]
        # Clear None values
        self.filtered_strings = list(filter(None, filtered_strings))
        # take lists inside of list and append to list
        for index, value in enumerate(self.filtered_strings, 0):
            if isinstance(value, list):
                self.filtered_strings.extend(self.filtered_strings.pop(index))
                self.filtered_strings.insert(index, self.filtered_strings[-1:][0])
        # Added matching for ports
        ports = [string.split(':')[0] for string in self.filtered_strings if ':' in string]
        self.filtered_strings.extend(ports)
        # Added matching for tcp sockets. ConnectionPool ignores the tcp://
        tcp_sockets = [string.split('//')[1] for string in self.filtered_strings if '//' in string]
        self.filtered_strings.extend(tcp_sockets)
        # Get JUST hostname from tcp//unix
        for socket in getattr(self, 'docker_sockets'):
            self.filtered_strings.append(socket.split('//')[1].split(':')[0])

        for handler in self.logger.handlers:
            handler.addFilter(BlacklistFilter(set(self.filtered_strings)))

    def parse(self):
        for option in Config.options:
            if self.environment_vars.get(option):
                env_opt = self.environment_vars[option]
                if isinstance(env_opt, str):
                    # Clean out quotes, both single/double and whitespace
                    env_opt = env_opt.strip("'").strip('"').strip(' ')
                if option in ['INTERVAL', 'PROMETHEUS_PORT', 'INFLUX_PORT']:
                    try:
                        opt = int(env_opt)
                        setattr(self, option.lower(), opt)
                    except ValueError as e:
                        print(e)
                elif option in ['CLEANUP', 'RUN_ONCE', 'INFLUX_SSL', 'INFLUX_VERIFY_SSL', 'DRY_RUN', 'SWARM',
                                'SELF_UPDATE', 'LABEL_ENABLE', 'DOCKER_TLS', 'LABELS_ONLY', 'DOCKER_TLS_VERIFY',
                                'SKIP_STARTUP_NOTIFICATIONS']:
                    if env_opt.lower() in ['true', 'yes']:
                        setattr(self, option.lower(), True)
                    elif env_opt.lower() in ['false', 'no']:
                        setattr(self, option.lower(), False)
                    else:
                        self.logger.error('%s is not true/yes, nor false/no for %s. Assuming %s',
                                          env_opt, option, getattr(self, option))
                else:
                    setattr(self, option.lower(), env_opt)
            elif vars(self.cli_args).get(option):
                setattr(self, option.lower(), vars(self.cli_args).get(option))

        # Specific var changes
        if self.repo_user and self.repo_pass:
            self.auth_json = {'Username': self.repo_user, 'Password': self.repo_pass}

        if self.interval < 30:
            self.interval = 30

        if self.labels_only and not self.label_enable:
            self.logger.warning('labels_only enabled but not in use without label_enable')

        for option in ['docker_sockets', 'notifiers', 'monitor', 'ignore']:
            if isinstance(getattr(self, option), str):
                string_list = getattr(self, option)
                setattr(self, option, [string for string in string_list.split(' ')])

        # Config sanity checks
        if self.cron:
            cron_times = self.cron.strip().split(' ')
            if len(cron_times) != 5:
                self.logger.error("Cron must be in cron syntax. e.g. * * * * * (5 places). Ignoring and using interval")
                self.cron = None
            else:
                self.logger.info("Cron configuration is valid. Using Cron schedule %s", cron_times)
                self.cron = cron_times
                self.interval = None

        if self.data_export == 'influxdb' and not self.influx_database:
            self.logger.error("You need to specify an influx database if you want to export to influxdb. Disabling "
                              "influxdb data export.")

        if self.data_export == 'prometheus' and self.self_update:
            self.logger.warning("If you bind a port to ouroboros, it will be lost when it updates itself.")

        if self.dry_run and not self.run_once:
            self.logger.warning("Dry run is designed to be ran with run once. Setting for you.")
            self.run_once = True

        # Remove default config that is not used for cleaner logs
        if self.data_export != 'prometheus':
            self.prometheus_addr, self.prometheus_port = None, None

        if self.data_export != 'influxdb':
            self.influx_url, self.influx_port, self.influx_username, self.influx_password = None, None, None, None

        self.config_blacklist()


================================================
FILE: pyouroboros/dataexporters.py
================================================
import prometheus_client

from logging import getLogger
from influxdb import InfluxDBClient
from datetime import datetime, timezone


class DataManager(object):
    def __init__(self, config):
        self.config = config
        self.logger = getLogger()
        self.enabled = True

        self.monitored_containers = {}
        self.total_updated = {}

        self.prometheus = PrometheusExporter(self, config) if self.config.data_export == "prometheus" else None
        self.influx = InfluxClient(self, config) if self.config.data_export == "influxdb" else None

    def add(self, label, socket):
        if self.config.data_export == "prometheus" and self.enabled:
            self.prometheus.update(label, socket)

        elif self.config.data_export == "influxdb" and self.enabled:
            if label == "all":
                self.logger.debug("Total containers updated %s", self.total_updated[socket])

            self.influx.write_points(label, socket)

    def set(self, socket):
        if self.config.data_export == "prometheus" and self.enabled:
            self.prometheus.set_monitored(socket)


class PrometheusExporter(object):
    def __init__(self, data_manager, config):
        self.config = config
        self.data_manager = data_manager
        self.http_server = prometheus_client.start_http_server(
            self.config.prometheus_port,
            addr=self.config.prometheus_addr
        )
        self.updated_containers_counter = prometheus_client.Counter(
            'containers_updated',
            'Count of containers updated',
            ['socket', 'container']
        )
        self.monitored_containers_gauge = prometheus_client.Gauge(
            'containers_being_monitored',
            'Gauge of containers being monitored',
            ['socket']
        )
        self.updated_all_containers_gauge = prometheus_client.Gauge(
            'all_containers_updated',
            'Count of total updated',
            ['socket']
        )
        self.logger = getLogger()

    def set_monitored(self, socket):
        """Set number of containers being monitoring with a gauge"""
        self.monitored_containers_gauge.labels(socket=socket).set(self.data_manager.monitored_containers[socket])
        self.logger.debug("Prometheus Exporter monitored containers gauge set to %s",
                          self.data_manager.monitored_containers[socket])

    def update(self, label, socket):
        """Set container update count based on label"""
        if label == "all":
            self.updated_all_containers_gauge.labels(socket=socket).set(self.data_manager.total_updated[socket])
        else:
            self.updated_containers_counter.labels(socket=socket, container=label).inc()

        self.logger.debug("Prometheus Exporter container update counter incremented for %s", label)


class InfluxClient(object):
    def __init__(self, data_manger, config):
        self.data_manager = data_manger
        self.config = config
        self.logger = getLogger()
        self.influx = InfluxDBClient(
            host=self.config.influx_url,
            port=self.config.influx_port,
            username=self.config.influx_username,
            password=self.config.influx_password,
            database=self.config.influx_database,
            ssl=self.config.influx_ssl,
            verify_ssl=self.config.influx_verify_ssl
        )
        self.db_check()

    def db_check(self):
        database_dicts = self.influx.get_list_database()
        databases = [d['name'] for d in database_dicts]
        if self.config.influx_database in databases:
            self.logger.debug("Influxdb database existence check passed for %s", self.config.influx_database)
        else:
            self.logger.debug("Influxdb database existence failed for %s. Disabling exports.",
                              self.config.influx_database)
            self.data_manager.enabled = False

    def write_points(self, label, socket):
        clean_socket = socket.split("//")[1]
        now = datetime.now(timezone.utc).astimezone().isoformat()
        influx_payload = [
            {
                "measurement": "Ouroboros",
                "tags": {'socket': clean_socket},
                "time": now,
                "fields": {}
            },
            {
                "measurement": "Ouroboros",
                "tags": {'configuration': self.config.hostname},
                "time": now,
                "fields": {key: (value if not isinstance(value, list) else ' '.join(value)) for key, value in
                           vars(self.config).items() if key.upper() in self.config.options}
            }
        ]
        if label == "all":
            influx_payload[0]['tags']["type"] = "stats"
            influx_payload[0]['fields'] = {
                "monitored_containers": self.data_manager.monitored_containers[socket],
                "updated_count": self.data_manager.total_updated[socket]
            }
        else:
            influx_payload[0]['tags'].update(
                {
                    "type": "container_update",
                    "container": label
                }
            )
            influx_payload[0]['fields'] = {"count": 1}

        self.logger.debug("Writing data to influxdb: %s", influx_payload)
        self.influx.write_points(influx_payload)


================================================
FILE: pyouroboros/dockerclient.py
================================================
from time import sleep
from logging import getLogger
from docker import DockerClient, tls
from os.path import isdir, isfile, join
from docker.errors import DockerException, APIError, NotFound

from pyouroboros.helpers import set_properties, remove_sha_prefix, get_digest


class Docker(object):
    def __init__(self, socket, config, data_manager, notification_manager):
        self.config = config
        self.socket = socket
        self.client = self.connect()
        self.data_manager = data_manager
        self.logger = getLogger()

        self.notification_manager = notification_manager

    def connect(self):
        if self.config.docker_tls:
            try:
                cert_paths = {
                    'cert_top_dir': '/etc/docker/certs.d/',
                    'clean_socket': self.socket.split('//')[1]
                }
                cert_paths['cert_dir'] = join(cert_paths['cert_top_dir'], cert_paths['clean_socket'])
                cert_paths['cert_files'] = {
                    'client_cert': join(cert_paths['cert_dir'], 'client.cert'),
                    'client_key': join(cert_paths['cert_dir'], 'client.key'),
                    'ca_crt': join(cert_paths['cert_dir'], 'ca.crt')
                }

                if not isdir(cert_paths['cert_dir']):
                    self.logger.error('%s is not a valid cert folder', cert_paths['cert_dir'])
                    raise ValueError

                for cert_file in cert_paths['cert_files'].values():
                    if not isfile(cert_file):
                        self.logger.error('%s does not exist', cert_file)
                        raise ValueError

                tls_config = tls.TLSConfig(
                    ca_cert=cert_paths['cert_files']['ca_crt'],
                    verify=cert_paths['cert_files']['ca_crt'] if self.config.docker_tls_verify else False,
                    client_cert=(cert_paths['cert_files']['client_cert'], cert_paths['cert_files']['client_key'])
                )
                client = DockerClient(base_url=self.socket, tls=tls_config)
            except ValueError:
                self.logger.error('Invalid Docker TLS config for %s, reverting to unsecured', self.socket)
                client = DockerClient(base_url=self.socket)
        else:
            client = DockerClient(base_url=self.socket)

        return client


class BaseImageObject(object):
    def __init__(self, docker_client):
        self.docker = docker_client
        self.logger = self.docker.logger
        self.config = self.docker.config
        self.client = self.docker.client
        self.socket = self.docker.socket
        self.data_manager = self.docker.data_manager
        self.data_manager.total_updated[self.socket] = 0
        self.notification_manager = self.docker.notification_manager

    def _pull(self, tag):
        """Docker pull image tag"""
        self.logger.debug('Checking tag: %s', tag)
        try:
            if self.config.dry_run:
                # The authentication doesn't work with this call
                # See bugs https://github.com/docker/docker-py/issues/2225
                return self.client.images.get_registry_data(tag)
            else:
                if self.config.auth_json:
                    return_image = self.client.images.pull(tag, auth_config=self.config.auth_json)
                else:
                    return_image = self.client.images.pull(tag)
                return return_image
        except APIError as e:
            if '<html>' in str(e):
                self.logger.debug("Docker api issue. Ignoring")
                raise ConnectionError
            elif 'unauthorized' in str(e):
                if self.config.dry_run:
                    self.logger.error('dry run : Upstream authentication issue while checking %s. See: '
                                      'https://github.com/docker/docker-py/issues/2225', tag)
                    raise ConnectionError
                else:
                    self.logger.critical("Invalid Credentials. Exiting")
                    exit(1)
            elif 'Client.Timeout' in str(e):
                self.logger.critical(
                    "Couldn't find an image on docker.com for %s. Local Build?", tag)
                raise ConnectionError
            elif ('pull access' or 'TLS handshake') in str(e):
                self.logger.critical("Couldn't pull. Skipping. Error: %s", e)
                raise ConnectionError


class Container(BaseImageObject):
    mode = 'container'

    def __init__(self, docker_client):
        super().__init__(docker_client)
        self.monitored = self.monitor_filter()

    # Container sub functions
    def stop(self, container):
        self.logger.debug('Stopping container: %s', container.name)
        stop_signal = container.labels.get('com.ouroboros.stop_signal', False)
        if stop_signal:
            try:
                container.kill(signal=stop_signal)
            except APIError as e:
                self.logger.error('Cannot kill container using signal %s. stopping normally. Error: %s',
                                  stop_signal, e)
                container.stop()
        else:
            container.stop()

    def remove(self, container):
        self.logger.debug('Removing container: %s', container.name)
        try:
            container.remove()
        except NotFound as e:
            self.logger.error("Could not remove container. Error: %s", e)
            return

    def recreate(self, container, latest_image):
        new_config = set_properties(old=container, new=latest_image)

        self.stop(container)
        self.remove(container)

        created = self.client.api.create_container(**new_config)
        new_container = self.client.containers.get(created.get("Id"))

        # connect the new container to all networks of the old container
        for network_name, network_config in container.attrs['NetworkSettings']['Networks'].items():
            network = self.client.networks.get(network_config['NetworkID'])
            try:
                network.disconnect(new_container.id, force=True)
            except APIError:
                pass
            new_network_config = {
                'container': new_container,
                'aliases': network_config['Aliases'],
                'links': network_config['Links']
            }
            if network_config['IPAMConfig']:
                new_network_config.update(
                    {
                        'ipv4_address': network_config['IPAddress'],
                        'ipv6_address': network_config['GlobalIPv6Address']
                    }
                )
            try:
                network.connect(**new_network_config)
            except APIError as e:
                if any(err in str(e) for err in ['user configured subnets', 'user defined networks']):
                    if new_network_config.get('ipv4_address'):
                        del new_network_config['ipv4_address']
                    if new_network_config.get('ipv6_address'):
                        del new_network_config['ipv6_address']
                    network.connect(**new_network_config)
                else:
                    self.logger.error('Unable to attach updated container to network "%s". Error: %s', network.name, e)

        new_container.start()

    def pull(self, current_tag):
        """Docker pull image tag"""
        tag = current_tag
        if not tag:
            self.logger.error('Missing tag. Skipping...')
            raise ConnectionError
        elif ':' not in tag:
            tag = f'{tag}:latest'
        return self._pull(tag)

    # Filters
    def running_filter(self):
        """Return running container objects list, except ouroboros itself"""
        running_containers = []
        try:
            for container in self.client.containers.list(filters={'status': 'running'}):
                if self.config.self_update:
                    running_containers.append(container)
                else:
                    try:
                        if 'ouroboros' not in container.image.tags[0]:
                            if container.attrs['HostConfig']['AutoRemove']:
                                self.logger.debug("Skipping %s due to --rm property.", container.name)
                            else:
                                running_containers.append(container)
                    except IndexError:
                        self.logger.error("%s has no tags.. you should clean it up! Ignoring.", container.id)
                        continue

        except DockerException:
            self.logger.critical("Can't connect to Docker API at %s", self.config.docker_socket)
            exit(1)

        return running_containers

    def monitor_filter(self):
        """Return filtered running container objects list"""
        running_containers = self.running_filter()
        monitored_containers = []

        for container in running_containers:
            ouro_label = container.labels.get('com.ouroboros.enable', False)
            # if labels enabled, use the label. 'true/yes' trigger monitoring.
            if self.config.label_enable and ouro_label:
                if ouro_label.lower() in ["true", "yes"]:
                    monitored_containers.append(container)
                else:
                    continue
            elif not self.config.labels_only:
                if self.config.monitor:
                    if container.name in self.config.monitor and container.name not in self.config.ignore:
                        monitored_containers.append(container)
                elif container.name not in self.config.ignore:
                    monitored_containers.append(container)

        self.data_manager.monitored_containers[self.socket] = len(monitored_containers)
        self.data_manager.set(self.socket)

        return monitored_containers

    # Socket Functions
    def self_check(self):
        if self.config.self_update:
            me_list = [container for container in self.client.containers.list() if 'ouroboros' in container.name]
            if len(me_list) > 1:
                self.update_self(count=2, me_list=me_list)

    def socket_check(self):
        depends_on_names = []
        hard_depends_on_names = []
        updateable = []
        self.monitored = self.monitor_filter()

        if not self.monitored:
            self.logger.info('No containers are running or monitored on %s', self.socket)
            return

        for container in self.monitored:
            current_image = container.image
            current_tag = container.attrs['Config']['Image']

            try:
                latest_image = self.pull(current_tag)
            except ConnectionError:
                continue

            try:
                if current_image.id != latest_image.id:
                    updateable.append((container, current_image, latest_image))
                else:
                    continue
            except AttributeError:
                self.logger.error("Issue detecting %s's image tag. Skipping...", container.name)

            # Get container list to restart after update complete
            depends_on = container.labels.get('com.ouroboros.depends_on', False)
            hard_depends_on = container.labels.get('com.ouroboros.hard_depends_on', False)
            if depends_on:
                depends_on_names.extend([name.strip() for name in depends_on.split(',')])
            if hard_depends_on:
                hard_depends_on_names.extend([name.strip() for name in hard_depends_on.split(',')])

        hard_depends_on_containers = []
        hard_depends_on_names = list(set(hard_depends_on_names))
        for name in hard_depends_on_names:
            try:
                hard_depends_on_containers.append(self.client.containers.get(name))
            except NotFound:
                self.logger.error("Could not find dependant container %s on socket %s. Ignoring", name, self.socket)

        depends_on_containers = []
        depends_on_names = list(set(depends_on_names))
        depends_on_names = [name for name in depends_on_names if name not in hard_depends_on_names]
        for name in depends_on_names:
            try:
                depends_on_containers.append(self.client.containers.get(name))
            except NotFound:
                self.logger.error("Could not find dependant container %s on socket %s. Ignoring", name, self.socket)

        return updateable, depends_on_containers, hard_depends_on_containers

    def update(self):
        updated_count = 0
        try:
            updateable, depends_on_containers, hard_depends_on_containers = self.socket_check()
        except TypeError:
            return

        for container in depends_on_containers + hard_depends_on_containers:
            self.stop(container)

        for container, current_image, latest_image in updateable:
            if self.config.dry_run:
                # Ugly hack for repo digest
                repo_digest_id = current_image.attrs['RepoDigests'][0].split('@')[1]
                if repo_digest_id != latest_image.id:
                    self.logger.info('dry run : %s would be updated', container.name)
                continue

            if container.name in ['ouroboros', 'ouroboros-updated']:
                self.data_manager.total_updated[self.socket] += 1
                self.data_manager.add(label=container.name, socket=self.socket)
                self.data_manager.add(label='all', socket=self.socket)
                self.notification_manager.send(container_tuples=updateable,
                                               socket=self.socket, kind='update')
                self.update_self(old_container=container, new_image=latest_image, count=1)

            self.logger.info('%s will be updated', container.name)

            self.recreate(container, latest_image)

            if self.config.cleanup:
                try:
                    self.client.images.remove(current_image.id)
                except APIError as e:
                    self.logger.error("Could not delete old image for %s, Error: %s", container.name, e)
            updated_count += 1

            self.logger.debug("Incrementing total container updated count")

            self.data_manager.total_updated[self.socket] += 1
            self.data_manager.add(label=container.name, socket=self.socket)
            self.data_manager.add(label='all', socket=self.socket)

        for container in depends_on_containers:
            # Reload container to ensure it isn't referencing the old image
            container.reload()
            container.start()

        for container in hard_depends_on_containers:
            self.recreate(container, container.image)

        if updated_count > 0:
            self.notification_manager.send(container_tuples=updateable, socket=self.socket, kind='update')

    def update_self(self, count=None, old_container=None, me_list=None, new_image=None):
        if count == 2:
            self.logger.debug('God im messy... cleaning myself up.')
            old_me_id = me_list[0].id if me_list[0].attrs['Created'] < me_list[1].attrs['Created'] else me_list[1].id
            old_me = self.client.containers.get(old_me_id)
            old_me_image_id = old_me.image.id

            old_me.stop()
            old_me.remove()

            self.client.images.remove(old_me_image_id)
            self.logger.debug('Ahhh. All better.')

            self.monitored = self.monitor_filter()
        elif count == 1:
            self.logger.debug('I need to update! Starting the ouroboros ;)')
            self_name = 'ouroboros-updated' if old_container.name == 'ouroboros' else 'ouroboros'
            new_config = set_properties(old=old_container, new=new_image, self_name=self_name)
            try:
                me_created = self.client.api.create_container(**new_config)
                new_me = self.client.containers.get(me_created.get("Id"))
                new_me.start()
                self.logger.debug('If you strike me down, I shall become '
                                  'more powerful than you could possibly imagine.')
                self.logger.debug('https://bit.ly/2VVY7GH')
                sleep(30)
            except APIError as e:
                self.logger.error("Self update failed.")
                self.logger.error(e)


class Service(BaseImageObject):
    mode = 'service'

    def __init__(self, docker_client):
        super().__init__(docker_client)
        self.monitored = self.monitor_filter()

    def monitor_filter(self):
        """Return filtered service objects list"""
        services = self.client.services.list(filters={'label': 'com.ouroboros.enable'})

        monitored_services = []

        for service in services:
            ouro_label = service.attrs['Spec']['Labels'].get('com.ouroboros.enable')
            if not self.config.label_enable or ouro_label.lower() in ["true", "yes"]:
                monitored_services.append(service)

        self.data_manager.monitored_containers[self.socket] = len(monitored_services)
        self.data_manager.set(self.socket)

        return monitored_services

    def pull(self, tag):
        """Docker pull image tag"""
        return self._pull(tag)

    def update(self):
        updated_service_tuples = []
        self.monitored = self.monitor_filter()

        if not self.monitored:
            self.logger.info('No services monitored')

        for service in self.monitored:
            image_string = service.attrs['Spec']['TaskTemplate']['ContainerSpec']['Image']
            if '@' in image_string:
                tag = image_string.split('@')[0]
                sha256 = remove_sha_prefix(image_string.split('@')[1])
            else:
                self.logger.error('No image SHA for %s. Skipping', image_string)
                continue

            try:
                latest_image = self.pull(tag)
            except ConnectionError:
                continue

            latest_image_sha256 = get_digest(latest_image)
            self.logger.debug('Latest sha256 for %s is %s', tag, latest_image_sha256)

            if sha256 != latest_image_sha256:
                if self.config.dry_run:
                    # Ugly hack for repo digest
                    self.logger.info('dry run : %s would be updated', service.name)
                    continue

                updated_service_tuples.append(
                    (service, sha256[-10:], latest_image)
                )

                if 'ouroboros' in service.name and self.config.self_update:
                    self.data_manager.total_updated[self.socket] += 1
                    self.data_manager.add(label=service.name, socket=self.socket)
                    self.data_manager.add(label='all', socket=self.socket)
                    self.notification_manager.send(container_tuples=updated_service_tuples,
                                                   socket=self.socket, kind='update', mode='service')

                self.logger.info('%s will be updated', service.name)
                service.update(image=f"{tag}@sha256:{latest_image_sha256}")

                self.data_manager.total_updated[self.socket] += 1
                self.data_manager.add(label=service.name, socket=self.socket)
                self.data_manager.add(label='all', socket=self.socket)

        if updated_service_tuples:
            self.notification_manager.send(
                container_tuples=updated_service_tuples,
                socket=self.socket,
                kind='update',
                mode='service'
            )


================================================
FILE: pyouroboros/helpers.py
================================================
def set_properties(old, new, self_name=None):
    """Store object for spawning new container in place of the one with outdated image"""
    properties = {
        'name': self_name if self_name else old.name,
        'hostname': old.attrs['Config']['Hostname'],
        'user': old.attrs['Config']['User'],
        'detach': True,
        'domainname': old.attrs['Config']['Domainname'],
        'tty': old.attrs['Config']['Tty'],
        'ports': None if not old.attrs['Config'].get('ExposedPorts') else [
            (p.split('/')[0], p.split('/')[1]) for p in old.attrs['Config']['ExposedPorts'].keys()
        ],
        'volumes': None if not old.attrs['Config'].get('Volumes') else [
            v for v in old.attrs['Config']['Volumes'].keys()
        ],
        'working_dir': old.attrs['Config']['WorkingDir'],
        'image': new.tags[0],
        'command': old.attrs['Config']['Cmd'],
        'host_config': old.attrs['HostConfig'],
        'labels': old.attrs['Config']['Labels'],
        'entrypoint': old.attrs['Config']['Entrypoint'],
        'environment': old.attrs['Config']['Env'],
        'healthcheck': old.attrs['Config'].get('Healthcheck', None)
    }

    return properties


def remove_sha_prefix(digest):
    if digest.startswith("sha256:"):
        return digest[7:]
    return digest


def get_digest(image):
    digest = image.attrs.get(
            "Descriptor", {}
        ).get("digest") or image.attrs.get(
            "RepoDigests"
        )[0].split('@')[1] or image.id
    return remove_sha_prefix(digest)


================================================
FILE: pyouroboros/logger.py
================================================
from logging import Filter, getLogger, Formatter, StreamHandler


class BlacklistFilter(Filter):
    """
    Log filter for blacklisted tokens and passwords
    """

    blacklisted_keys = ['repo_user', 'repo_pass', 'auth_json', 'docker_sockets', 'prometheus_addr',
                        'influx_username', 'influx_password', 'influx_url', 'notifiers']

    def __init__(self, filteredstrings):
        super().__init__()
        self.filtered_strings = filteredstrings

    def filter(self, record):
        for item in self.filtered_strings:
            try:
                if item in record.msg:
                    record.msg = record.msg.replace(item, 8 * '*' + item[-5:])
                if any(item in str(arg) for arg in record.args):
                    record.args = tuple(arg.replace(item, 8 * '*' + item[-5:]) if isinstance(arg, str) else arg
                                        for arg in record.args)
            except TypeError:
                pass
        return True


class OuroborosLogger(object):
    def __init__(self, level='INFO'):
        # Create the Logger
        self.logger = getLogger()
        try:
            self.logger.setLevel(level.upper())
        except ValueError:
            level = "INFO"
            self.logger.setLevel(level.upper())

        # Create a Formatter for formatting the log messages
        logger_formatter = Formatter('%(asctime)s : %(levelname)s : %(module)s : %(message)s', '%Y-%m-%d %H:%M:%S')

        # Add the console logger
        console_logger = StreamHandler()
        console_logger.setFormatter(logger_formatter)

        console_logger.setLevel(level.upper())

        # Add the Handler to the Logger
        self.logger.addHandler(console_logger)

        # Less verbose apscheduler logging if info
        if self.logger.getEffectiveLevel() == 20:
            getLogger('apscheduler').setLevel('WARNING')


================================================
FILE: pyouroboros/notifiers.py
================================================
import apprise

from logging import getLogger
from datetime import datetime, timezone


class NotificationManager(object):
    def __init__(self, config, data_manager):
        self.config = config
        self.data_manager = data_manager
        self.logger = getLogger()

        self.apprise = self.build_apprise()

    def build_apprise(self):
        asset = apprise.AppriseAsset(
            image_url_mask='https://i.imgur.com/L40ksWY.png',
            default_extension='.png'
        )
        asset.app_id = "Ouroboros"
        asset.app_desc = "Ouroboros"
        asset.app_url = "https://github.com/pyouroboros/ouroboros"
        asset.html_notify_map['info'] = '#5F87C6'
        asset.image_url_logo = 'https://bin.cajun.pro/images/ouroboros/notifications/ouroboros-logo-256x256.png'

        apprise_obj = apprise.Apprise(asset=asset)

        for notifier in self.config.notifiers:
            add = apprise_obj.add(notifier)
            if not add:
                self.logger.error('Could not add notifier %s', notifier)

        return apprise_obj

    def send(self, container_tuples=None, socket=None, kind='update', next_run=None, mode='container'):
        if kind == 'startup':
            now = datetime.now(timezone.utc).astimezone()
            title = f'Ouroboros has started'
            body_fields = [
                f'Host: {self.config.hostname}',
                f'Time: {now.strftime("%Y-%m-%d %H:%M:%S")}',
                f'Next Run: {next_run}']
        else:
            title = 'Ouroboros has updated containers!'
            body_fields = [
                f"Host/Socket: {self.config.hostname} / {socket.split('//')[1]}",
                f"Containers Monitored: {self.data_manager.monitored_containers[socket]}",
                f"Total Containers Updated: {self.data_manager.total_updated[socket]}",
                f"Containers updated this pass: {len(container_tuples)}"
            ]
            body_fields.extend(
                [
                    "{} updated from {} to {}".format(
                        container.name,
                        old_image if mode == 'service' else old_image.short_id.split(':')[1],
                        new_image.short_id.split(':')[1]
                    ) for container, old_image, new_image in container_tuples
                ]
            )
        body = '\r\n'.join(body_fields)

        if self.apprise.servers:
            self.apprise.notify(title=title, body=body)


================================================
FILE: pyouroboros/ouroboros.py
================================================
from time import sleep
from os import environ

from requests.exceptions import ConnectionError
from datetime import datetime, timezone, timedelta
from argparse import ArgumentParser, RawTextHelpFormatter
from apscheduler.schedulers.background import BackgroundScheduler

from pyouroboros.config import Config
from pyouroboros import VERSION, BRANCH
from pyouroboros.logger import OuroborosLogger
from pyouroboros.dataexporters import DataManager
from pyouroboros.notifiers import NotificationManager
from pyouroboros.dockerclient import Docker, Container, Service


def main():
    """Declare command line options"""
    parser = ArgumentParser(description='ouroboros', formatter_class=RawTextHelpFormatter,
                            epilog='EXAMPLE: ouroboros -d tcp://1.2.3.4:5678 -i 20 -m container1 container2 -l warn')

    core_group = parser.add_argument_group("Core", "Configuration of core functionality")
    core_group.add_argument('-v', '--version', action='version', version=VERSION)

    core_group.add_argument('-d', '--docker-sockets', nargs='+', default=Config.docker_sockets, dest='DOCKER_SOCKETS',
                            help='Sockets for docker management\n'
                                 'DEFAULT: "unix://var/run/docker.sock"\n'
                                 'EXAMPLE: -d unix://var/run/docker.sock tcp://192.168.1.100:2376')

    core_group.add_argument('-t', '--docker-tls', default=Config.docker_tls, dest='DOCKER_TLS',
                            action='store_true', help='Enable docker TLS\n'
                                                      'REQUIRES: docker cert mount')

    core_group.add_argument('-T', '--docker-tls-verify', default=Config.docker_tls_verify, dest='DOCKER_TLS_VERIFY',
                            action='store_false', help='Verify the CA Certificate mounted for TLS\n'
                                                       'DEFAULT: True')

    core_group.add_argument('-i', '--interval', type=int, default=Config.interval, dest='INTERVAL',
                            help='Interval in seconds between checking for updates\n'
                                 'DEFAULT: 300')

    core_group.add_argument('-C', '--cron', default=Config.cron, dest='CRON',
                            help='Cron formatted string for scheduling\n'
                                 'EXAMPLE: "*/5 * * * *"')

    core_group.add_argument('-l', '--log-level', choices=['debug', 'info', 'warn', 'error', 'critical'],
                            dest='LOG_LEVEL', default=Config.log_level, help='Set logging level\n'
                                                                             'DEFAULT: info')

    core_group.add_argument('-u', '--self-update', default=Config.self_update, dest='SELF_UPDATE', action='store_true',
                            help='Let ouroboros update itself')

    core_group.add_argument('-S', '--swarm', default=Config.swarm, dest='SWARM', action='store_true',
                            help='Put ouroboros in swarm mode')

    core_group.add_argument('-o', '--run-once', default=Config.run_once, action='store_true', dest='RUN_ONCE',
                            help='Single run')

    core_group.add_argument('-A', '--dry-run', default=Config.dry_run, action='store_true', dest='DRY_RUN',
                            help='Run without making changes. Best used with run-once')

    core_group.add_argument('-N', '--notifiers', nargs='+', default=Config.notifiers, dest='NOTIFIERS',
                            help='Apprise formatted notifiers\n'
                                 'EXAMPLE: -N discord://1234123412341234/jasdfasdfasdfasddfasdf '
                                 'mailto://user:pass@gmail.com')

    docker_group = parser.add_argument_group("Docker", "Configuration of docker functionality")
    docker_group.add_argument('-m', '--monitor', nargs='+', default=Config.monitor, dest='MONITOR',
                              help='Which container(s) to monitor\n'
                                   'DEFAULT: All')

    docker_group.add_argument('-n', '--ignore', nargs='+', default=Config.ignore, dest='IGNORE',
                              help='Container(s) to ignore\n'
                                   'EXAMPLE: -n container1 container2')

    docker_group.add_argument('-k', '--label-enable', default=Config.label_enable, dest='LABEL_ENABLE',
                              action='store_true', help='Enable label monitoring for ouroboros label options\n'
                                                        'Note: labels take precedence'
                                                        'DEFAULT: False')

    docker_group.add_argument('-M', '--labels-only', default=Config.labels_only, dest='LABELS_ONLY',
                              action='store_true', help='Only watch containers that utilize labels\n'
                                                        'This allows a more strict compliance for environments'
                                                        'DEFAULT: False')

    docker_group.add_argument('-c', '--cleanup', default=Config.cleanup, dest='CLEANUP', action='store_true',
                              help='Remove old images after updating')

    docker_group.add_argument('-r', '--repo-user', default=Config.repo_user, dest='REPO_USER',
                              help='Private docker registry username\n'
                                   'EXAMPLE: foo@bar.baz')

    docker_group.add_argument('-R', '--repo-pass', default=Config.repo_pass, dest='REPO_PASS',
                              help='Private docker registry password\n'
                                   'EXAMPLE: MyPa$$w0rd')

    data_group = parser.add_argument_group('Data Export', 'Configuration of data export functionality')
    data_group.add_argument('-D', '--data-export', choices=['prometheus', 'influxdb'], default=Config.data_export,
                            dest='DATA_EXPORT', help='Enable exporting of data for chosen option')

    data_group.add_argument('-a', '--prometheus-addr', default=Config.prometheus_addr,
                            dest='PROMETHEUS_ADDR', help='Bind address to run Prometheus exporter on\n'
                                                         'DEFAULT: 127.0.0.1')

    data_group.add_argument('-p', '--prometheus-port', type=int, default=Config.prometheus_port,
                            dest='PROMETHEUS_PORT', help='Port to run Prometheus exporter on\n'
                                                         'DEFAULT: 8000')

    data_group.add_argument('-I', '--influx-url', default=Config.influx_url, dest='INFLUX_URL',
                            help='URL for influxdb\n'
                                  'DEFAULT: 127.0.0.1')

    data_group.add_argument('-P', '--influx-port', type=int, default=Config.influx_port, dest='INFLUX_PORT',
                            help='PORT for influxdb\n'
                                  'DEFAULT: 8086')

    data_group.add_argument('-U', '--influx-username', default=Config.influx_username, dest='INFLUX_USERNAME',
                            help='Username for influxdb\n'
                                  'DEFAULT: root')

    data_group.add_argument('-x', '--influx-password', default=Config.influx_password, dest='INFLUX_PASSWORD',
                            help='Password for influxdb\n'
                                  'DEFAULT: root')

    data_group.add_argument('-X', '--influx-database', default=Config.influx_database, dest='INFLUX_DATABASE',
                            help='Influx database name. Required if using influxdb')

    data_group.add_argument('-s', '--influx-ssl', default=Config.influx_ssl, dest='INFLUX_SSL', action='store_true',
                            help='Use SSL when connecting to influxdb')

    data_group.add_argument('-V', '--influx-verify-ssl', default=Config.influx_verify_ssl, dest='INFLUX_VERIFY_SSL',
                            action='store_true', help='Verify SSL certificate when connecting to influxdb')

    docker_group.add_argument('--skip-startup-notifications', default=Config.skip_startup_notifications,
                              dest='SKIP_STARTUP_NOTIFICATIONS', action='store_true',
                              help='Do not send ouroboros notifications when starting')

    args = parser.parse_args()

    if environ.get('LOG_LEVEL'):
        log_level = environ.get('LOG_LEVEL')
    else:
        log_level = args.LOG_LEVEL
    ol = OuroborosLogger(level=log_level)
    ol.logger.info('Version: %s-%s', VERSION, BRANCH)
    config = Config(environment_vars=environ, cli_args=args)
    config_dict = {key: value for key, value in vars(config).items() if key.upper() in config.options}
    ol.logger.debug("Ouroboros configuration: %s", config_dict)

    data_manager = DataManager(config)
    notification_manager = NotificationManager(config, data_manager)
    scheduler = BackgroundScheduler()
    scheduler.start()

    for socket in config.docker_sockets:
        try:
            docker = Docker(socket, config, data_manager, notification_manager)
            if config.swarm:
                mode = Service(docker)
            else:
                mode = Container(docker)

            if config.run_once:
                scheduler.add_job(mode.update, name=f'Run Once container update for {socket}')
            else:
                if mode.mode == 'container':
                    scheduler.add_job(mode.self_check, name=f'Self Check for {socket}')
                if config.cron:
                    scheduler.add_job(
                        mode.update,
                        name=f'Cron container update for {socket}',
                        trigger='cron',
                        minute=config.cron[0],
                        hour=config.cron[1],
                        day=config.cron[2],
                        month=config.cron[3],
                        day_of_week=config.cron[4],
                        misfire_grace_time=15
                    )
                else:
                    scheduler.add_job(
                        mode.update,
                        name=f'Initial run interval container update for {socket}'
                    )
                    scheduler.add_job(
                        mode.update,
                        name=f'Interval container update for {socket}',
                        trigger='interval', seconds=config.interval
                    )
        except ConnectionError:
            ol.logger.error("Could not connect to socket %s. Check your config", socket)

    if config.run_once:
        next_run = None
    elif config.cron:
        next_run = scheduler.get_jobs()[0].next_run_time
    else:
        now = datetime.now(timezone.utc).astimezone()
        next_run = (now + timedelta(0, config.interval)).strftime("%Y-%m-%d %H:%M:%S")

    if not config.skip_startup_notifications:
        notification_manager.send(kind='startup', next_run=next_run)

    while scheduler.get_jobs():
        sleep(1)

    scheduler.shutdown()


if __name__ == "__main__":
    main()


================================================
FILE: requirements.txt
================================================
docker>=3.7.0
prometheus_client>=0.5.0
requests>=2.21.0
influxdb>=5.2.1
apprise>=0.8.5
apscheduler>=3.5.3


================================================
FILE: setup.py
================================================
from setuptools import setup, find_packages
from pyouroboros import VERSION


def read(filename):
    with open(filename) as f:
        return f.read()


def get_requirements(filename="requirements.txt"):
    """returns a list of all requirements"""
    requirements = read(filename)
    return list(filter(None, [req.strip() for req in requirements.split() if not req.startswith('#')]))


setup(
    name='ouroboros-cli',
    version=VERSION,
    maintainer='circa10a',
    maintainer_email='caleblemoine@gmail.com',
    description='Automatically update running docker containers',
    long_description=read('README.md'),
    long_description_content_type='text/markdown',
    url='https://github.com/pyouroboros/ouroboros',
    license='MIT',
    classifiers=['Programming Language :: Python',
                 'Programming Language :: Python :: 3.6',
                 'Programming Language :: Python :: 3.7'],
    packages=find_packages(),
    scripts=['ouroboros'],
    install_requires=get_requirements(),
    python_requires='>=3.6.2'
)
Download .txt
gitextract_imqw0m8b/

├── .github/
│   ├── FUNDING.yml
│   └── dependabot.yml
├── .gitignore
├── .gitlab-ci.yml
├── CHANGELOG.md
├── Dockerfile
├── Dockerfile.arm
├── Dockerfile.arm64
├── LICENSE
├── MANIFEST.in
├── README.md
├── docker-compose.yml
├── ouroboros
├── pyouroboros/
│   ├── __init__.py
│   ├── config.py
│   ├── dataexporters.py
│   ├── dockerclient.py
│   ├── helpers.py
│   ├── logger.py
│   ├── notifiers.py
│   └── ouroboros.py
├── requirements.txt
└── setup.py
Download .txt
SYMBOL INDEX (54 symbols across 8 files)

FILE: pyouroboros/config.py
  class Config (line 6) | class Config(object):
    method __init__ (line 50) | def __init__(self, environment_vars, cli_args):
    method config_blacklist (line 58) | def config_blacklist(self):
    method parse (line 81) | def parse(self):

FILE: pyouroboros/dataexporters.py
  class DataManager (line 8) | class DataManager(object):
    method __init__ (line 9) | def __init__(self, config):
    method add (line 20) | def add(self, label, socket):
    method set (line 30) | def set(self, socket):
  class PrometheusExporter (line 35) | class PrometheusExporter(object):
    method __init__ (line 36) | def __init__(self, data_manager, config):
    method set_monitored (line 60) | def set_monitored(self, socket):
    method update (line 66) | def update(self, label, socket):
  class InfluxClient (line 76) | class InfluxClient(object):
    method __init__ (line 77) | def __init__(self, data_manger, config):
    method db_check (line 92) | def db_check(self):
    method write_points (line 102) | def write_points(self, label, socket):

FILE: pyouroboros/dockerclient.py
  class Docker (line 10) | class Docker(object):
    method __init__ (line 11) | def __init__(self, socket, config, data_manager, notification_manager):
    method connect (line 20) | def connect(self):
  class BaseImageObject (line 58) | class BaseImageObject(object):
    method __init__ (line 59) | def __init__(self, docker_client):
    method _pull (line 69) | def _pull(self, tag):
  class Container (line 104) | class Container(BaseImageObject):
    method __init__ (line 107) | def __init__(self, docker_client):
    method stop (line 112) | def stop(self, container):
    method remove (line 125) | def remove(self, container):
    method recreate (line 133) | def recreate(self, container, latest_image):
    method pull (line 175) | def pull(self, current_tag):
    method running_filter (line 186) | def running_filter(self):
    method monitor_filter (line 210) | def monitor_filter(self):
    method self_check (line 236) | def self_check(self):
    method socket_check (line 242) | def socket_check(self):
    method update (line 296) | def update(self):
    method update_self (line 350) | def update_self(self, count=None, old_container=None, me_list=None, ne...
  class Service (line 381) | class Service(BaseImageObject):
    method __init__ (line 384) | def __init__(self, docker_client):
    method monitor_filter (line 388) | def monitor_filter(self):
    method pull (line 404) | def pull(self, tag):
    method update (line 408) | def update(self):

FILE: pyouroboros/helpers.py
  function set_properties (line 1) | def set_properties(old, new, self_name=None):
  function remove_sha_prefix (line 29) | def remove_sha_prefix(digest):
  function get_digest (line 35) | def get_digest(image):

FILE: pyouroboros/logger.py
  class BlacklistFilter (line 4) | class BlacklistFilter(Filter):
    method __init__ (line 12) | def __init__(self, filteredstrings):
    method filter (line 16) | def filter(self, record):
  class OuroborosLogger (line 29) | class OuroborosLogger(object):
    method __init__ (line 30) | def __init__(self, level='INFO'):

FILE: pyouroboros/notifiers.py
  class NotificationManager (line 7) | class NotificationManager(object):
    method __init__ (line 8) | def __init__(self, config, data_manager):
    method build_apprise (line 15) | def build_apprise(self):
    method send (line 35) | def send(self, container_tuples=None, socket=None, kind='update', next...

FILE: pyouroboros/ouroboros.py
  function main (line 17) | def main():

FILE: setup.py
  function read (line 5) | def read(filename):
  function get_requirements (line 10) | def get_requirements(filename="requirements.txt"):
Condensed preview — 23 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (102K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 39,
    "preview": "custom: https://buymeacoff.ee/ouroboros"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 381,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: \"/\"\n    schedule:\n      interval: daily\n    ope"
  },
  {
    "path": ".gitignore",
    "chars": 1323,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
  },
  {
    "path": ".gitlab-ci.yml",
    "chars": 3554,
    "preview": "stages:\n  - test\n  - build and push\n  - manifests\n  - release\n\n.common_build: &common_build\n  image: docker:stable\n  sta"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 36848,
    "preview": "# Change Log\n\n## [1.4.3](https://github.com/pyouroboros/ouroboros/tree/1.4.3) (2019-12-11)\n[Full Changelog](https://gith"
  },
  {
    "path": "Dockerfile",
    "chars": 346,
    "preview": "FROM amd64/python:3.8.5-alpine\n\nLABEL maintainers=\"dirtycajunrice,circa10a\"\n\nENV TZ UTC\n\nWORKDIR /app\n\nCOPY /requirement"
  },
  {
    "path": "Dockerfile.arm",
    "chars": 348,
    "preview": "FROM arm32v6/python:3.8.5-alpine\n\nLABEL maintainers=\"dirtycajunrice,circa10a\"\n\nENV TZ UTC\n\nWORKDIR /app\n\nCOPY /requireme"
  },
  {
    "path": "Dockerfile.arm64",
    "chars": 348,
    "preview": "FROM arm64v8/python:3.8.5-alpine\n\nLABEL maintainers=\"dirtycajunrice,circa10a\"\n\nENV TZ UTC\n\nWORKDIR /app\n\nCOPY /requireme"
  },
  {
    "path": "LICENSE",
    "chars": 1070,
    "preview": "MIT License\n\nCopyright (c) 2018 Caleb Lemoine\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
  },
  {
    "path": "MANIFEST.in",
    "chars": 42,
    "preview": "include requirements.txt\ninclude README.md"
  },
  {
    "path": "README.md",
    "chars": 3332,
    "preview": "⚠️⚠️⚠️ ouroboros is no longer in development. It does its job (more or less) and the devs have succumb to real life! Ple"
  },
  {
    "path": "docker-compose.yml",
    "chars": 395,
    "preview": "version: '3'\nservices:\n  ouroboros:\n    container_name: ouroboros\n    hostname: ouroboros\n    image: pyouroboros/ourobor"
  },
  {
    "path": "ouroboros",
    "chars": 125,
    "preview": "#!/usr/bin/env python3\nfrom pyouroboros.ouroboros import main\n# stub to allow cli call\nif __name__ == \"__main__\":\n    ma"
  },
  {
    "path": "pyouroboros/__init__.py",
    "chars": 36,
    "preview": "VERSION = \"1.4.3\"\nBRANCH = \"master\"\n"
  },
  {
    "path": "pyouroboros/config.py",
    "chars": 6692,
    "preview": "from os import environ\nfrom logging import getLogger\nfrom pyouroboros.logger import BlacklistFilter\n\n\nclass Config(objec"
  },
  {
    "path": "pyouroboros/dataexporters.py",
    "chars": 5355,
    "preview": "import prometheus_client\n\nfrom logging import getLogger\nfrom influxdb import InfluxDBClient\nfrom datetime import datetim"
  },
  {
    "path": "pyouroboros/dockerclient.py",
    "chars": 19713,
    "preview": "from time import sleep\nfrom logging import getLogger\nfrom docker import DockerClient, tls\nfrom os.path import isdir, isf"
  },
  {
    "path": "pyouroboros/helpers.py",
    "chars": 1543,
    "preview": "def set_properties(old, new, self_name=None):\n    \"\"\"Store object for spawning new container in place of the one with ou"
  },
  {
    "path": "pyouroboros/logger.py",
    "chars": 1891,
    "preview": "from logging import Filter, getLogger, Formatter, StreamHandler\n\n\nclass BlacklistFilter(Filter):\n    \"\"\"\n    Log filter "
  },
  {
    "path": "pyouroboros/notifiers.py",
    "chars": 2466,
    "preview": "import apprise\n\nfrom logging import getLogger\nfrom datetime import datetime, timezone\n\n\nclass NotificationManager(object"
  },
  {
    "path": "pyouroboros/ouroboros.py",
    "chars": 11049,
    "preview": "from time import sleep\nfrom os import environ\n\nfrom requests.exceptions import ConnectionError\nfrom datetime import date"
  },
  {
    "path": "requirements.txt",
    "chars": 106,
    "preview": "docker>=3.7.0\nprometheus_client>=0.5.0\nrequests>=2.21.0\ninfluxdb>=5.2.1\napprise>=0.8.5\napscheduler>=3.5.3\n"
  },
  {
    "path": "setup.py",
    "chars": 1044,
    "preview": "from setuptools import setup, find_packages\nfrom pyouroboros import VERSION\n\n\ndef read(filename):\n    with open(filename"
  }
]

About this extraction

This page contains the full source code of the circa10a/ouroboros GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 23 files (95.7 KB), approximately 26.3k tokens, and a symbol index with 54 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!