[
  {
    "path": ".github/FUNDING.yml",
    "content": "custom: https://buymeacoff.ee/ouroboros"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: \"/\"\n    schedule:\n      interval: daily\n    open-pull-requests-limit: 10\n\n  - package-ecosystem: docker\n    directory: \"/\"\n    schedule:\n      interval: daily\n    open-pull-requests-limit: 10\n\n  - package-ecosystem: pip\n    directory: \"/\"\n    schedule:\n      interval: daily\n    open-pull-requests-limit: 10\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n#vscode\n.vscode/\n\n# JetBrains\n.idea/\n*.iml\n\n"
  },
  {
    "path": ".gitlab-ci.yml",
    "content": "stages:\n  - test\n  - build and push\n  - manifests\n  - release\n\n.common_build: &common_build\n  image: docker:stable\n  stage: build and push\n  variables:\n    DOCKER_HOST: tcp://docker:2375/\n    DOCKER_DRIVER: overlay2\n  services:\n    - docker:dind\n  except:\n    - tags\n  only:\n    changes:\n      - \"Dockerfile*\"\n      - ouroboros\n      - pyouroboros/*\n      - .gitlab-ci.yml\n  before_script:\n    - mkdir $HOME/.docker\n    - echo '{\"experimental\":\"enabled\"}' > $HOME/.docker/config.json\n    - if [[ $CI_COMMIT_REF_NAME == \"master\" ]]; then\n          export TAG=$(grep -i version pyouroboros/__init__.py | cut -d \\  -f3 | tr -d \\\");\n      else\n          export TAG=\"develop\";\n      fi\n    - echo \"$DOCKER_PASSWORD\" | docker login -u \"$DOCKER_USER\" --password-stdin\n    - export CI_PROJECT_PATH=$(echo $CI_PROJECT_PATH | tr \"[:upper:]\" \"[:lower:]\")\n\n.common_release: &common_release\n  <<: *common_build\n  tags:\n    - docker\n    - shared\n  only:\n    refs:\n      - master\n\nflake8:\n  image: python:3.7.3-alpine\n  stage: test\n  cache:\n    key: \"ouroboros-cache\"\n    paths:\n      - .cache/pip\n      - venv/\n  variables:\n    PIP_CACHE_DIR: \"$CI_PROJECT_DIR/.cache/pip\"\n  tags:\n    - shared\n  before_script:\n    - python -m venv venv\n    - source venv/bin/activate\n    - pip install -U flake8\n  script:\n    - flake8 --max-line-length 120 ouroboros *.py pyouroboros/*.py\n\namd64:\n  <<: *common_build\n  tags:\n    - docker\n    - shared\n  script:\n    - docker build --pull\n                   --cache-from \"$CI_PROJECT_PATH\":\"$TAG-amd64\"\n                   -t \"$CI_PROJECT_PATH\":\"$TAG-amd64\" .\n    - docker push \"$CI_PROJECT_PATH\":\"$TAG-amd64\"\n\narmv6:\n  <<: *common_build\n  tags:\n    - docker\n    - arm\n  script:\n    - docker build --pull\n                   --cache-from \"$CI_PROJECT_PATH\":\"$TAG-arm\"\n                   -t \"$CI_PROJECT_PATH\":\"$TAG-arm\"\n                   -f Dockerfile.arm .\n    - docker push \"$CI_PROJECT_PATH\":\"$TAG-arm\"\n\narm64v8:\n  <<: *common_build\n  tags:\n    - docker\n    - arm64\n  script:\n    - docker build --pull\n                   --cache-from \"$CI_PROJECT_PATH\":\"$TAG-arm64\"\n                   -t \"$CI_PROJECT_PATH\":\"$TAG-arm64\"\n                   -f Dockerfile.arm64 .\n    - docker push \"$CI_PROJECT_PATH\":\"$TAG-arm64\"\n\nversioned:\n  <<: *common_build\n  stage: manifests\n  tags:\n    - docker\n    - shared\n  script:\n    - docker manifest create \"$CI_PROJECT_PATH\":\"$TAG\"\n      \"$CI_PROJECT_PATH\":\"$TAG-amd64\"\n      \"$CI_PROJECT_PATH\":\"$TAG-arm64\"\n      \"$CI_PROJECT_PATH\":\"$TAG-arm\"\n    - docker manifest push -p \"$CI_PROJECT_PATH\":\"$TAG\"\n\nlatest:\n  <<: *common_release\n  stage: manifests\n  tags:\n    - docker\n  script:\n    - docker manifest create \"$CI_PROJECT_PATH\":latest\n      \"$CI_PROJECT_PATH\":\"$TAG-amd64\"\n      \"$CI_PROJECT_PATH\":\"$TAG-arm64\"\n      \"$CI_PROJECT_PATH\":\"$TAG-arm\"\n    - docker manifest push -p \"$CI_PROJECT_PATH\":latest\n\ngithub:\n  <<: *common_release\n  stage: release\n  script:\n    - apk add git\n    - git remote set-url origin \"https://$GITHUB_USER:$GITHUB_TOKEN@github.com/$CI_PROJECT_PATH.git\"\n    - git tag $TAG\n    - git push --tags\n\npypi:\n  <<: *common_release\n  image: python:3.7.3-alpine\n  stage: release\n  cache:\n    key: \"ouroboros-cache\"\n    paths:\n      - .cache/pip\n      - venv/\n  variables:\n    PIP_CACHE_DIR: \"$CI_PROJECT_DIR/.cache/pip\"\n  tags:\n    - shared\n  before_script:\n    - python -m venv venv\n    - source venv/bin/activate\n    - pip install -U twine\n  script:\n    - venv/bin/python setup.py sdist && venv/bin/python -m twine upload --skip-existing -u ${PYPI_CREDS_USR} -p ${PYPI_CREDS_PSW} dist/*"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Change Log\n\n## [1.4.3](https://github.com/pyouroboros/ouroboros/tree/1.4.3) (2019-12-11)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.4.3...1.4.2)\n\n**Fixed bugs:**\n\n- Join Notifications truncated and icon returning 404 [\\#325](https://github.com/pyouroboros/ouroboros/issues/325)\n\n**Other Pull Requests**\n\n- v1.4.3 Merge [\\#354](https://github.com/pyouroboros/ouroboros/pull/354) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- v1.4.3 to develop [\\#353](https://github.com/pyouroboros/ouroboros/pull/353) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Update requirements.txt [\\#349](https://github.com/pyouroboros/ouroboros/pull/349) ([nemchik](https://github.com/nemchik))\n\n## [1.4.2](https://github.com/pyouroboros/ouroboros/tree/1.4.2) (2019-08-01)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.4.1...1.4.2)\n\n**Other Pull Requests**\n\n- 1.4.2 Merge [\\#327](https://github.com/pyouroboros/ouroboros/pull/327) ([circa10a](https://github.com/circa10a))\n- 1.4.2 to develop [\\#326](https://github.com/pyouroboros/ouroboros/pull/326) ([circa10a](https://github.com/circa10a))\n\n## [1.4.1](https://github.com/pyouroboros/ouroboros/tree/1.4.1) (2019-06-04)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/v1.4.0...1.4.1)\n\n**Other Pull Requests**\n\n- v1.4.1 Merge [\\#315](https://github.com/pyouroboros/ouroboros/pull/315) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- v1.4.1 to develop [\\#314](https://github.com/pyouroboros/ouroboros/pull/314) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n\n## [v1.4.0](https://github.com/pyouroboros/ouroboros/tree/v1.4.0) (2019-04-25)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.3.1...v1.4.0)\n\n**Implemented enhancements:**\n\n- Make startup notification optional [\\#253](https://github.com/pyouroboros/ouroboros/issues/253)\n\n**Fixed bugs:**\n\n- Missing MANIFEST.in file causes pypi install to fail [\\#284](https://github.com/pyouroboros/ouroboros/issues/284)\n- Healthcheck section not re-applied after container update [\\#275](https://github.com/pyouroboros/ouroboros/issues/275)\n- docker-compose local and remote tls logger location [\\#273](https://github.com/pyouroboros/ouroboros/issues/273)\n- Self update errors. Not deleting old self [\\#262](https://github.com/pyouroboros/ouroboros/issues/262)\n- ouroboros sets fixed IP addresses [\\#254](https://github.com/pyouroboros/ouroboros/issues/254)\n\n**Closed issues:**\n\n- Update apprise to v0.7.4 [\\#266](https://github.com/pyouroboros/ouroboros/issues/266) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)]\n- 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)]\n\n**Other Pull Requests**\n\n- v1.4.0 Merge [\\#299](https://github.com/pyouroboros/ouroboros/pull/299) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Remove Watchtower Reference [\\#298](https://github.com/pyouroboros/ouroboros/pull/298) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- v1.4.0 to develop [\\#297](https://github.com/pyouroboros/ouroboros/pull/297) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- fixes \\#284 [\\#296](https://github.com/pyouroboros/ouroboros/pull/296) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Patches [\\#295](https://github.com/pyouroboros/ouroboros/pull/295) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Fix healthcheck attr [\\#294](https://github.com/pyouroboros/ouroboros/pull/294) ([circa10a](https://github.com/circa10a))\n- Patch/catch up [\\#271](https://github.com/pyouroboros/ouroboros/pull/271) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Arg skip startup notifications [\\#261](https://github.com/pyouroboros/ouroboros/pull/261) ([circa10a](https://github.com/circa10a))\n- Revert \"add option to skip startup notifications\" [\\#260](https://github.com/pyouroboros/ouroboros/pull/260) ([circa10a](https://github.com/circa10a))\n- add option to skip startup notifications [\\#259](https://github.com/pyouroboros/ouroboros/pull/259) ([circa10a](https://github.com/circa10a))\n\n## [1.3.1](https://github.com/pyouroboros/ouroboros/tree/1.3.1) (2019-02-28)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.3.0...1.3.1)\n\n**Fixed bugs:**\n\n- Since 1.3.0, docker login fails [\\#243](https://github.com/pyouroboros/ouroboros/issues/243)\n- Catch Failed self-updates [\\#230](https://github.com/pyouroboros/ouroboros/issues/230)\n\n**Other Pull Requests**\n\n- v1.3.1 Merge [\\#249](https://github.com/pyouroboros/ouroboros/pull/249) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- v1.3.1 to develop [\\#248](https://github.com/pyouroboros/ouroboros/pull/248) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- fix name subscript for \\#243 [\\#247](https://github.com/pyouroboros/ouroboros/pull/247) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- fixes \\#230 and \\#243 [\\#242](https://github.com/pyouroboros/ouroboros/pull/242) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n\n## [1.3.0](https://github.com/pyouroboros/ouroboros/tree/1.3.0) (2019-02-25)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.2.1...1.3.0)\n\n**Implemented enhancements:**\n\n- Start new container in detached mode [\\#222](https://github.com/pyouroboros/ouroboros/pull/222) ([nightvisi0n](https://github.com/nightvisi0n))\n- Optimise dockerfile layers [\\#218](https://github.com/pyouroboros/ouroboros/pull/218) ([nightvisi0n](https://github.com/nightvisi0n))\n\n**Fixed bugs:**\n\n- Cron scheduled missed following successful runs [\\#229](https://github.com/pyouroboros/ouroboros/issues/229)\n- Catch attribute.id error [\\#226](https://github.com/pyouroboros/ouroboros/issues/226)\n- AttachStdout and AttachStderr are not carried over properly [\\#221](https://github.com/pyouroboros/ouroboros/issues/221)\n- Exception when updating container started with --rm \\(autoremove\\)  [\\#219](https://github.com/pyouroboros/ouroboros/issues/219)\n- Issue with Swarm Mode V2 [\\#216](https://github.com/pyouroboros/ouroboros/issues/216)\n- Fix docker swarm mode [\\#227](https://github.com/pyouroboros/ouroboros/pull/227) ([mathcantin](https://github.com/mathcantin))\n\n**Other Pull Requests**\n\n- v1.3.0 Merge [\\#241](https://github.com/pyouroboros/ouroboros/pull/241) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- v1.3.0 to develop [\\#240](https://github.com/pyouroboros/ouroboros/pull/240) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Catch self update apierror [\\#238](https://github.com/pyouroboros/ouroboros/pull/238) ([circa10a](https://github.com/circa10a))\n- Catch attribute error [\\#237](https://github.com/pyouroboros/ouroboros/pull/237) ([circa10a](https://github.com/circa10a))\n- Check for autoremove [\\#236](https://github.com/pyouroboros/ouroboros/pull/236) ([circa10a](https://github.com/circa10a))\n- Add misfire\\_grace\\_time for cron scheduler [\\#234](https://github.com/pyouroboros/ouroboros/pull/234) ([circa10a](https://github.com/circa10a))\n- 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))\n- remove git in pypi + branch develop + version bump + maintainer\\_email [\\#214](https://github.com/pyouroboros/ouroboros/pull/214) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n\n## [1.2.1](https://github.com/pyouroboros/ouroboros/tree/1.2.1) (2019-02-14)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.2.0...1.2.1)\n\n**Fixed bugs:**\n\n- Broken when no :tag specified  [\\#210](https://github.com/pyouroboros/ouroboros/issues/210)\n\n**Other Pull Requests**\n\n- v1.2.1 Merge [\\#213](https://github.com/pyouroboros/ouroboros/pull/213) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- v1.2.1 to develop [\\#212](https://github.com/pyouroboros/ouroboros/pull/212) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- fixes \\#210 [\\#211](https://github.com/pyouroboros/ouroboros/pull/211) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- version bump to 1.2.1 + develop branch + twine fix + … [\\#209](https://github.com/pyouroboros/ouroboros/pull/209) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n\n## [1.2.0](https://github.com/pyouroboros/ouroboros/tree/1.2.0) (2019-02-14)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.1.2...1.2.0)\n\n**Implemented enhancements:**\n\n- Move \"Interval container update\" messages to debug log level [\\#194](https://github.com/pyouroboros/ouroboros/issues/194)\n- \\[Feature Request\\] Support for Swarm Services [\\#178](https://github.com/pyouroboros/ouroboros/issues/178)\n- Add Warning for label\\_enable not set while using labels\\_only [\\#202](https://github.com/pyouroboros/ouroboros/pull/202) ([larsderidder](https://github.com/larsderidder))\n\n**Fixed bugs:**\n\n- Change depends\\_on logic [\\#198](https://github.com/pyouroboros/ouroboros/issues/198)\n- 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)\n- Exception when trying to update container with complex compose networks [\\#196](https://github.com/pyouroboros/ouroboros/issues/196)\n- Problem with network IPv4 address carry-over [\\#193](https://github.com/pyouroboros/ouroboros/issues/193)\n- 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))\n\n**Closed issues:**\n\n- 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)]\n- Add environment variables in Wiki [\\#203](https://github.com/pyouroboros/ouroboros/issues/203) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)]\n- Slack notifications via webhook not working [\\#187](https://github.com/pyouroboros/ouroboros/issues/187)\n\n**Other Pull Requests**\n\n- v1.2.0 Merge [\\#208](https://github.com/pyouroboros/ouroboros/pull/208) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- v1.2.0 to develop [\\#207](https://github.com/pyouroboros/ouroboros/pull/207) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Patch/tag bug [\\#205](https://github.com/pyouroboros/ouroboros/pull/205) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Patch/group 5 [\\#201](https://github.com/pyouroboros/ouroboros/pull/201) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Fix bug in user defined network detection [\\#200](https://github.com/pyouroboros/ouroboros/pull/200) ([nightvisi0n](https://github.com/nightvisi0n))\n- Adjust apscheduler logger [\\#199](https://github.com/pyouroboros/ouroboros/pull/199) ([circa10a](https://github.com/circa10a))\n- Carry over network config [\\#195](https://github.com/pyouroboros/ouroboros/pull/195) ([nightvisi0n](https://github.com/nightvisi0n))\n- Jenkins tweaks [\\#192](https://github.com/pyouroboros/ouroboros/pull/192) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Swarm + Jenkins [\\#188](https://github.com/pyouroboros/ouroboros/pull/188) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n\n## [1.1.2](https://github.com/pyouroboros/ouroboros/tree/1.1.2) (2019-02-02)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.1.1...1.1.2)\n\n**Fixed bugs:**\n\n- No default timezone [\\#176](https://github.com/pyouroboros/ouroboros/issues/176)\n\n**Closed issues:**\n\n- cron documentation example update [\\#182](https://github.com/pyouroboros/ouroboros/issues/182) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)]\n\n**Other Pull Requests**\n\n- v1.1.2 Merge [\\#186](https://github.com/pyouroboros/ouroboros/pull/186) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- v1.1.2 to develop [\\#183](https://github.com/pyouroboros/ouroboros/pull/183) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Fix default timezone [\\#177](https://github.com/pyouroboros/ouroboros/pull/177) ([circa10a](https://github.com/circa10a))\n\n## [1.1.1](https://github.com/pyouroboros/ouroboros/tree/1.1.1) (2019-02-01)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.1.0...1.1.1)\n\n**Implemented enhancements:**\n\n- Support for adding an identifier \\(hostname?\\) to notifications [\\#158](https://github.com/pyouroboros/ouroboros/issues/158)\n- 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))\n- add cli arg for cron [\\#157](https://github.com/pyouroboros/ouroboros/pull/157) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n\n**Fixed bugs:**\n\n- Ouroboros does not respect MONITOR= [\\#166](https://github.com/pyouroboros/ouroboros/issues/166)\n- Docker TLS over TCP connections [\\#154](https://github.com/pyouroboros/ouroboros/issues/154) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)]\n- Patch/group 4 [\\#169](https://github.com/pyouroboros/ouroboros/pull/169) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Recheck properly for only non lists [\\#164](https://github.com/pyouroboros/ouroboros/pull/164) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- add some missing passthrough info for restart [\\#163](https://github.com/pyouroboros/ouroboros/pull/163) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n\n**Other Pull Requests**\n\n- v1.1.1 Merge [\\#173](https://github.com/pyouroboros/ouroboros/pull/173) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- v1.1.1 to develop [\\#172](https://github.com/pyouroboros/ouroboros/pull/172) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Patch/group 3 [\\#167](https://github.com/pyouroboros/ouroboros/pull/167) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Add hostname to the notifications [\\#161](https://github.com/pyouroboros/ouroboros/pull/161) ([tlkamp](https://github.com/tlkamp))\n- Patch/group 2 [\\#155](https://github.com/pyouroboros/ouroboros/pull/155) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n\n## [1.1.0](https://github.com/pyouroboros/ouroboros/tree/1.1.0) (2019-01-26)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/1.0.0...1.1.0)\n\n**Implemented enhancements:**\n\n- Notification via Telegram [\\#146](https://github.com/pyouroboros/ouroboros/issues/146)\n- Add flag to allow a labels\\_only condition [\\#142](https://github.com/pyouroboros/ouroboros/issues/142)\n- DRY\\_RUN flag [\\#140](https://github.com/pyouroboros/ouroboros/issues/140)\n- Notification on startup [\\#138](https://github.com/pyouroboros/ouroboros/issues/138)\n- Start/Stop containers in sequence [\\#106](https://github.com/pyouroboros/ouroboros/issues/106)\n- 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))\n\n**Fixed bugs:**\n\n- Catch invalid docker socket config [\\#148](https://github.com/pyouroboros/ouroboros/issues/148)\n- Explicitly Define true/false [\\#141](https://github.com/pyouroboros/ouroboros/issues/141) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)]\n\n**Other Pull Requests**\n\n- v1.1.0 Merge [\\#153](https://github.com/pyouroboros/ouroboros/pull/153) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- v1.1.0 to develop [\\#152](https://github.com/pyouroboros/ouroboros/pull/152) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Patch/group 1 [\\#150](https://github.com/pyouroboros/ouroboros/pull/150) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Add volume for docker socket path [\\#144](https://github.com/pyouroboros/ouroboros/pull/144) ([mauvehed](https://github.com/mauvehed))\n\n## [1.0.0](https://github.com/pyouroboros/ouroboros/tree/1.0.0) (2019-01-23)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.6.0...1.0.0)\n\n**Implemented enhancements:**\n\n- Stop containers with alternate signal [\\#107](https://github.com/pyouroboros/ouroboros/issues/107)\n- Docker Socket secure connections [\\#105](https://github.com/pyouroboros/ouroboros/issues/105)\n- Selectively monitor containers with label [\\#104](https://github.com/pyouroboros/ouroboros/issues/104)\n- Allow stop-signal label [\\#133](https://github.com/pyouroboros/ouroboros/pull/133) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Docker TLS Verify option [\\#132](https://github.com/pyouroboros/ouroboros/pull/132) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- add label priority feature for watch/ignore. Addresses \\#104 [\\#121](https://github.com/pyouroboros/ouroboros/pull/121) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n\n**Fixed bugs:**\n\n- Unexpected docker API causes program to quit ‘500 Server Error: Internal Server Error’ [\\#130](https://github.com/pyouroboros/ouroboros/issues/130)\n- Error tag handling under the registry with port [\\#129](https://github.com/pyouroboros/ouroboros/issues/129)\n- a fatal error when none tag image [\\#122](https://github.com/pyouroboros/ouroboros/issues/122)\n- Bug/ignore logic [\\#135](https://github.com/pyouroboros/ouroboros/pull/135) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Bug/registry logic [\\#131](https://github.com/pyouroboros/ouroboros/pull/131) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- catch no tags in get\\_running [\\#124](https://github.com/pyouroboros/ouroboros/pull/124) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- 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))\n\n**Closed issues:**\n\n- Missing docker-compose.yml from documentation [\\#120](https://github.com/pyouroboros/ouroboros/issues/120) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)]\n- Wiki usage docs reference old argument names [\\#115](https://github.com/pyouroboros/ouroboros/issues/115) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)]\n\n**Other Pull Requests**\n\n- v1.0.0 Merge [\\#137](https://github.com/pyouroboros/ouroboros/pull/137) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- v1.0.0 to develop [\\#136](https://github.com/pyouroboros/ouroboros/pull/136) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- 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))\n- Cleanup/qemu logic [\\#128](https://github.com/pyouroboros/ouroboros/pull/128) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- 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))\n\n## [0.6.0](https://github.com/pyouroboros/ouroboros/tree/0.6.0) (2019-01-17)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.5.0...0.6.0)\n\n**Implemented enhancements:**\n\n- Support multi-architecture Docker images [\\#78](https://github.com/pyouroboros/ouroboros/issues/78)\n- Mail notification [\\#59](https://github.com/pyouroboros/ouroboros/issues/59)\n- Multi architecture docker [\\#110](https://github.com/pyouroboros/ouroboros/pull/110) ([circa10a](https://github.com/circa10a))\n- added logo to readme [\\#109](https://github.com/pyouroboros/ouroboros/pull/109) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Feature/ouroboros self\\_update [\\#103](https://github.com/pyouroboros/ouroboros/pull/103) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- add version cli arg [\\#100](https://github.com/pyouroboros/ouroboros/pull/100) ([circa10a](https://github.com/circa10a))\n- added email notifications. Addresses \\#59 [\\#97](https://github.com/pyouroboros/ouroboros/pull/97) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Documentation [\\#96](https://github.com/pyouroboros/ouroboros/pull/96) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n\n**Fixed bugs:**\n\n- Ignore not working as expected [\\#98](https://github.com/pyouroboros/ouroboros/issues/98)\n- specify for specificity! [\\#114](https://github.com/pyouroboros/ouroboros/pull/114) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- manifesting failures [\\#113](https://github.com/pyouroboros/ouroboros/pull/113) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- sigh. [\\#112](https://github.com/pyouroboros/ouroboros/pull/112) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Multiarch/fine tuning [\\#111](https://github.com/pyouroboros/ouroboros/pull/111) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- catch index error and account for shared images, x [\\#102](https://github.com/pyouroboros/ouroboros/pull/102) ([circa10a](https://github.com/circa10a))\n- add monitor/ignore to list sanity check. Fixes \\#98 [\\#99](https://github.com/pyouroboros/ouroboros/pull/99) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n\n**Other Pull Requests**\n\n- v0.6.0 to develop [\\#118](https://github.com/pyouroboros/ouroboros/pull/118) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- v0.6.0 Merge [\\#117](https://github.com/pyouroboros/ouroboros/pull/117) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- 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))\n\n## [0.5.0](https://github.com/pyouroboros/ouroboros/tree/0.5.0) (2019-01-13)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.4.3...0.5.0)\n\n**Implemented enhancements:**\n\n- Auto discover slack/discord notifications in WEBHOOK\\_URLS [\\#83](https://github.com/pyouroboros/ouroboros/issues/83)\n- Add to schedule logic run now [\\#75](https://github.com/pyouroboros/ouroboros/issues/75)\n- add pushover functionality. Finishes other half of \\#80 [\\#93](https://github.com/pyouroboros/ouroboros/pull/93) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- add keep\\_alive url for healthchecks. Addresses half of \\#80 [\\#89](https://github.com/pyouroboros/ouroboros/pull/89) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- changed webhook json to auto-deciding + fixed RUN\\_ONCE no underscore [\\#86](https://github.com/pyouroboros/ouroboros/pull/86) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n- Refactor [\\#79](https://github.com/pyouroboros/ouroboros/pull/79) ([DirtyCajunRice](https://github.com/DirtyCajunRice))\n\n**Fixed bugs:**\n\n- Fix log level case sensitivity [\\#82](https://github.com/pyouroboros/ouroboros/issues/82)\n- Invalid URL 'h': No schema supplied. Perhaps you meant http://h? [\\#76](https://github.com/pyouroboros/ouroboros/issues/76)\n- Installation via pip fails [\\#73](https://github.com/pyouroboros/ouroboros/issues/73)\n- Added try except [\\#95](https://github.com/pyouroboros/ouroboros/pull/95) ([circa10a](https://github.com/circa10a))\n- Fix dockerfile [\\#92](https://github.com/pyouroboros/ouroboros/pull/92) ([circa10a](https://github.com/circa10a))\n- use ouroboros script in dockerfile [\\#91](https://github.com/pyouroboros/ouroboros/pull/91) ([circa10a](https://github.com/circa10a))\n- fix deploy script to push git tags [\\#90](https://github.com/pyouroboros/ouroboros/pull/90) ([circa10a](https://github.com/circa10a))\n- change pypi travis username [\\#88](https://github.com/pyouroboros/ouroboros/pull/88) ([circa10a](https://github.com/circa10a))\n- install flake8 for travis, run on appropriate directories [\\#87](https://github.com/pyouroboros/ouroboros/pull/87) ([circa10a](https://github.com/circa10a))\n- 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))\n- change loglevel to use upper\\(\\) [\\#84](https://github.com/pyouroboros/ouroboros/pull/84) ([circa10a](https://github.com/circa10a))\n- Prometheus bind fix, org rename [\\#81](https://github.com/pyouroboros/ouroboros/pull/81) ([circa10a](https://github.com/circa10a))\n\n## [0.4.3](https://github.com/pyouroboros/ouroboros/tree/0.4.3) (2019-01-09)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.4.2...0.4.3)\n\n**Implemented enhancements:**\n\n- grafana to metrics/prometheus endpoint [\\#74](https://github.com/pyouroboros/ouroboros/issues/74)\n- add aarch64 docker image [\\#77](https://github.com/pyouroboros/ouroboros/pull/77) ([circa10a](https://github.com/circa10a))\n\n## [0.4.2](https://github.com/pyouroboros/ouroboros/tree/0.4.2) (2019-01-08)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.4.1...0.4.2)\n\n**Implemented enhancements:**\n\n- Add autopep8 to the pre-merge checks [\\#30](https://github.com/pyouroboros/ouroboros/issues/30)\n\n## [0.4.1](https://github.com/pyouroboros/ouroboros/tree/0.4.1) (2018-12-30)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.4.0...0.4.1)\n\n**Implemented enhancements:**\n\n- Pre merge code quality checks [\\#72](https://github.com/pyouroboros/ouroboros/pull/72) ([circa10a](https://github.com/circa10a))\n\n## [0.4.0](https://github.com/pyouroboros/ouroboros/tree/0.4.0) (2018-12-30)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.7...0.4.0)\n\n**Implemented enhancements:**\n\n- Slack notification  [\\#61](https://github.com/pyouroboros/ouroboros/issues/61)\n- Webhook notifications [\\#71](https://github.com/pyouroboros/ouroboros/pull/71) ([circa10a](https://github.com/circa10a))\n\n## [0.3.7](https://github.com/pyouroboros/ouroboros/tree/0.3.7) (2018-12-26)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.6...0.3.7)\n\n**Implemented enhancements:**\n\n- Timezone Support [\\#68](https://github.com/pyouroboros/ouroboros/issues/68)\n- Add output to log at container start [\\#66](https://github.com/pyouroboros/ouroboros/issues/66)\n- Enable Timezone Configuration [\\#69](https://github.com/pyouroboros/ouroboros/pull/69) ([circa10a](https://github.com/circa10a))\n\n## [0.3.6](https://github.com/pyouroboros/ouroboros/tree/0.3.6) (2018-12-21)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.5...0.3.6)\n\n**Implemented enhancements:**\n\n- print ouroboros configuration on startup [\\#67](https://github.com/pyouroboros/ouroboros/pull/67) ([circa10a](https://github.com/circa10a))\n\n## [0.3.5](https://github.com/pyouroboros/ouroboros/tree/0.3.5) (2018-12-20)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.4...0.3.5)\n\n**Implemented enhancements:**\n\n- Raspberry Pi compatible docker image [\\#62](https://github.com/pyouroboros/ouroboros/issues/62)\n- Scheduling docs [\\#65](https://github.com/pyouroboros/ouroboros/pull/65) ([circa10a](https://github.com/circa10a))\n\n## [0.3.4](https://github.com/pyouroboros/ouroboros/tree/0.3.4) (2018-12-19)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.3...0.3.4)\n\n**Implemented enhancements:**\n\n- Rpi docker image [\\#64](https://github.com/pyouroboros/ouroboros/pull/64) ([circa10a](https://github.com/circa10a))\n\n## [0.3.3](https://github.com/pyouroboros/ouroboros/tree/0.3.3) (2018-11-29)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.2...0.3.3)\n\n**Implemented enhancements:**\n\n- add docs, bump version [\\#58](https://github.com/pyouroboros/ouroboros/pull/58) ([circa10a](https://github.com/circa10a))\n\n**Fixed bugs:**\n\n- Problem accessing private registry [\\#55](https://github.com/pyouroboros/ouroboros/issues/55)\n\n**Closed issues:**\n\n- Q: Add config file? [\\#46](https://github.com/pyouroboros/ouroboros/issues/46) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)]\n\n## [0.3.2](https://github.com/pyouroboros/ouroboros/tree/0.3.2) (2018-11-28)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.1...0.3.2)\n\n**Fixed bugs:**\n\n- unrecognized arguments [\\#52](https://github.com/pyouroboros/ouroboros/issues/52)\n- Fix config json [\\#56](https://github.com/pyouroboros/ouroboros/pull/56) ([circa10a](https://github.com/circa10a))\n\n## [0.3.1](https://github.com/pyouroboros/ouroboros/tree/0.3.1) (2018-11-16)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.3.0...0.3.1)\n\n**Implemented enhancements:**\n\n- Add Prometheus endpoint [\\#23](https://github.com/pyouroboros/ouroboros/issues/23) [[hacktoberfest](https://github.com/pyouroboros/ouroboros/labels/hacktoberfest)]\n\n**Fixed bugs:**\n\n- fix bind address bug [\\#53](https://github.com/pyouroboros/ouroboros/pull/53) ([circa10a](https://github.com/circa10a))\n\n## [0.3.0](https://github.com/pyouroboros/ouroboros/tree/0.3.0) (2018-11-15)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.2.3...0.3.0)\n\n**Implemented enhancements:**\n\n- Q: continue to update to latest or same tag [\\#43](https://github.com/pyouroboros/ouroboros/issues/43)\n- Metrics [\\#51](https://github.com/pyouroboros/ouroboros/pull/51) ([circa10a](https://github.com/circa10a))\n- Disable pip cache in Dockerfile [\\#50](https://github.com/pyouroboros/ouroboros/pull/50) ([Strayer](https://github.com/Strayer))\n\n## [0.2.3](https://github.com/pyouroboros/ouroboros/tree/0.2.3) (2018-11-08)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.2.2...0.2.3)\n\n**Implemented enhancements:**\n\n- Keep tags [\\#48](https://github.com/pyouroboros/ouroboros/pull/48) ([circa10a](https://github.com/circa10a))\n\n## [0.2.2](https://github.com/pyouroboros/ouroboros/tree/0.2.2) (2018-11-03)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.2.1...0.2.2)\n\n**Implemented enhancements:**\n\n- Add ability to ignore select containers [\\#35](https://github.com/pyouroboros/ouroboros/issues/35)\n- Ignore containers [\\#45](https://github.com/pyouroboros/ouroboros/pull/45) ([tlkamp](https://github.com/tlkamp))\n- Update setup.py, travis param [\\#42](https://github.com/pyouroboros/ouroboros/pull/42) ([circa10a](https://github.com/circa10a))\n\n## [0.2.1](https://github.com/pyouroboros/ouroboros/tree/0.2.1) (2018-10-28)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.1.3...0.2.1)\n\n**Implemented enhancements:**\n\n- Option precedence [\\#32](https://github.com/pyouroboros/ouroboros/issues/32)\n- Add ouroboros to the user's path automagically [\\#28](https://github.com/pyouroboros/ouroboros/issues/28)\n- Deploy to Pypi [\\#41](https://github.com/pyouroboros/ouroboros/pull/41) ([circa10a](https://github.com/circa10a))\n- Add setup.py [\\#40](https://github.com/pyouroboros/ouroboros/pull/40) ([tlkamp](https://github.com/tlkamp))\n- change branch to master [\\#39](https://github.com/pyouroboros/ouroboros/pull/39) ([circa10a](https://github.com/circa10a))\n- Move api client out of cli.py [\\#38](https://github.com/pyouroboros/ouroboros/pull/38) ([tlkamp](https://github.com/tlkamp))\n- Handle the exceptions better in cli.py [\\#36](https://github.com/pyouroboros/ouroboros/pull/36) ([tlkamp](https://github.com/tlkamp))\n\n**Closed issues:**\n\n- \\[question\\] network\\_mode: \"service:XXX\" ? [\\#33](https://github.com/pyouroboros/ouroboros/issues/33) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)]\n\n**Other Pull Requests**\n\n- 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))\n- update docs [\\#34](https://github.com/pyouroboros/ouroboros/pull/34) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] ([circa10a](https://github.com/circa10a))\n\n## [0.1.3](https://github.com/pyouroboros/ouroboros/tree/0.1.3) (2018-10-25)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.1.2...0.1.3)\n\n**Implemented enhancements:**\n\n- Make CLI expose fewer globals, formatting [\\#31](https://github.com/pyouroboros/ouroboros/pull/31) ([tlkamp](https://github.com/tlkamp))\n\n## [0.1.2](https://github.com/pyouroboros/ouroboros/tree/0.1.2) (2018-10-24)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.1.1...0.1.2)\n\n**Implemented enhancements:**\n\n- Rewrite script to use vendor packages if possible [\\#25](https://github.com/pyouroboros/ouroboros/pull/25) ([dannysauer](https://github.com/dannysauer))\n- Improve URL matching Regex [\\#24](https://github.com/pyouroboros/ouroboros/pull/24) ([dannysauer](https://github.com/dannysauer))\n- Add environment files to the project for those working with Conda [\\#22](https://github.com/pyouroboros/ouroboros/pull/22) ([tlkamp](https://github.com/tlkamp))\n\n**Other Pull Requests**\n\n- 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))\n- 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))\n\n## [0.1.1](https://github.com/pyouroboros/ouroboros/tree/0.1.1) (2018-10-21)\n[Full Changelog](https://github.com/pyouroboros/ouroboros/compare/0.1.0...0.1.1)\n\n## [0.1.0](https://github.com/pyouroboros/ouroboros/tree/0.1.0) (2018-10-21)\n**Implemented enhancements:**\n\n- account for environment variables [\\#19](https://github.com/pyouroboros/ouroboros/issues/19)\n- Support private repos [\\#10](https://github.com/pyouroboros/ouroboros/issues/10)\n- Deploy to pypi [\\#5](https://github.com/pyouroboros/ouroboros/issues/5)\n- Create travis build [\\#4](https://github.com/pyouroboros/ouroboros/issues/4)\n- Rewrite new container class [\\#3](https://github.com/pyouroboros/ouroboros/issues/3)\n- Write Unit Tests [\\#2](https://github.com/pyouroboros/ouroboros/issues/2)\n- Add CLI Args [\\#1](https://github.com/pyouroboros/ouroboros/issues/1)\n- added support for private registries [\\#12](https://github.com/pyouroboros/ouroboros/pull/12) ([circa10a](https://github.com/circa10a))\n- Torpus cli args [\\#11](https://github.com/pyouroboros/ouroboros/pull/11) ([Torpus](https://github.com/Torpus))\n- single client [\\#9](https://github.com/pyouroboros/ouroboros/pull/9) ([circa10a](https://github.com/circa10a))\n- the less code the better [\\#8](https://github.com/pyouroboros/ouroboros/pull/8) ([circa10a](https://github.com/circa10a))\n- Initial stuff [\\#6](https://github.com/pyouroboros/ouroboros/pull/6) ([circa10a](https://github.com/circa10a))\n\n**Closed issues:**\n\n- Create good docs [\\#7](https://github.com/pyouroboros/ouroboros/issues/7) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)]\n\n**Other Pull Requests**\n\n- Docs [\\#21](https://github.com/pyouroboros/ouroboros/pull/21) [[documentation](https://github.com/pyouroboros/ouroboros/labels/documentation)] ([circa10a](https://github.com/circa10a))\n- Tests [\\#20](https://github.com/pyouroboros/ouroboros/pull/20) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a))\n- Add travis [\\#18](https://github.com/pyouroboros/ouroboros/pull/18) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a))\n- Tests [\\#17](https://github.com/pyouroboros/ouroboros/pull/17) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a))\n- Tests [\\#16](https://github.com/pyouroboros/ouroboros/pull/16) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a))\n- Tests [\\#15](https://github.com/pyouroboros/ouroboros/pull/15) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a))\n- Tests [\\#14](https://github.com/pyouroboros/ouroboros/pull/14) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a))\n- Tests [\\#13](https://github.com/pyouroboros/ouroboros/pull/13) [[cleanup](https://github.com/pyouroboros/ouroboros/labels/cleanup)] ([circa10a](https://github.com/circa10a))\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM amd64/python:3.8.5-alpine\n\nLABEL maintainers=\"dirtycajunrice,circa10a\"\n\nENV TZ UTC\n\nWORKDIR /app\n\nCOPY /requirements.txt /setup.py /ouroboros /README.md /app/\n\nRUN apk add --no-cache tzdata && \\\n    pip install --no-cache-dir -r requirements.txt\n\nCOPY /pyouroboros /app/pyouroboros\n\nRUN pip install --no-cache-dir .\n\nENTRYPOINT [\"ouroboros\"]"
  },
  {
    "path": "Dockerfile.arm",
    "content": "FROM arm32v6/python:3.8.5-alpine\n\nLABEL maintainers=\"dirtycajunrice,circa10a\"\n\nENV TZ UTC\n\nWORKDIR /app\n\nCOPY /requirements.txt /setup.py /ouroboros /README.md /app/\n\nRUN apk add --no-cache tzdata && \\\n    pip install --no-cache-dir -r requirements.txt\n\nCOPY /pyouroboros /app/pyouroboros\n\nRUN pip install --no-cache-dir .\n\nENTRYPOINT [\"ouroboros\"]"
  },
  {
    "path": "Dockerfile.arm64",
    "content": "FROM arm64v8/python:3.8.5-alpine\n\nLABEL maintainers=\"dirtycajunrice,circa10a\"\n\nENV TZ UTC\n\nWORKDIR /app\n\nCOPY /requirements.txt /setup.py /ouroboros /README.md /app/\n\nRUN apk add --no-cache tzdata && \\\n    pip install --no-cache-dir -r requirements.txt\n\nCOPY /pyouroboros /app/pyouroboros\n\nRUN pip install --no-cache-dir .\n\nENTRYPOINT [\"ouroboros\"]"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Caleb Lemoine\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include requirements.txt\ninclude README.md"
  },
  {
    "path": "README.md",
    "content": "⚠️⚠️⚠️ 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. ⚠️⚠️⚠️\n<img width=\"800\" src=\"https://raw.githubusercontent.com/pyouroboros/ouroboros/master/assets/ouroboros_logo_primary_long_cropped.jpg\" alt=\"Ouroboros Logo\">\n\n[![Discord](https://img.shields.io/discord/532695326117593112.svg?colorB=7289DA&label=Discord&logo=Discord&logoColor=7289DA&style=flat-square)](https://discord.gg/qHNByUW)\n[![Release](https://img.shields.io/github/release/pyouroboros/ouroboros.svg?style=flat-square)](https://hub.docker.com/r/pyouroboros/ouroboros/)\n[![Python Version](https://img.shields.io/pypi/pyversions/ouroboros-cli.svg?style=flat-square)](https://pypi.org/project/ouroboros-cli/)\n[![Docker Pulls](https://img.shields.io/docker/pulls/pyouroboros/ouroboros.svg?style=flat-square)](https://hub.docker.com/r/pyouroboros/ouroboros/)\n[![Layers](https://images.microbadger.com/badges/image/pyouroboros/ouroboros.svg)](https://microbadger.com/images/pyouroboros/ouroboros)  \n\nAutomatically update your running Docker containers to the latest available image.\n\nThe de-facto standard for docker update automation\n\n## Overview\n\nOuroboros 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.\n\n- Push your image to your registry and simply wait your defined interval for ouroboros to find the new image and redeploy your container autonomously.\n- Notify you via many platforms courtesy of [Apprise](https://github.com/caronc/apprise) \n- Serve metrics for trend monitoring (Currently: Prometheus/Influxdb)\n- Limit your server ssh access\n- `ssh -i key server.domainname \"docker pull ... && docker run ...\"` is for scrubs\n- `docker-compose pull && docker-compose up -d` is for fancier scrubs\n\n## Getting Started\n\nMore detailed usage and configuration can be found on [the wiki](https://github.com/pyouroboros/ouroboros/wiki).\n\n### Docker\n\nOuroboros is deployed via docker image like so:\n\n```bash\ndocker run -d --name ouroboros \\\n  -v /var/run/docker.sock:/var/run/docker.sock \\\n  pyouroboros/ouroboros\n```\n\n> This is image is compatible for amd64, arm32, and arm64 CPU architectures\n\nor via `docker-compose`:\n\n[Official Example](https://github.com/pyouroboros/ouroboros/blob/master/docker-compose.yml)\n\n### Pip\n\nOuroboros can also be installed via `pip`:\n\n```bash\npip install ouroboros-cli\n```\n\nAnd can then be invoked using the `ouroboros` command:\n\n```bash\n$ ouroboros --interval 300 --log-level debug\n```\n\n> This can be useful if you would like to create a `systemd` service or similar daemon that doesn't run in a container\n\n## Examples\nPer-command and scenario examples can be found in the [wiki](https://github.com/pyouroboros/ouroboros/wiki/Usage)\n\n## Contributing\n\nAll contributions are welcome! Contributing guidelines are in the works\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3'\nservices:\n  ouroboros:\n    container_name: ouroboros\n    hostname: ouroboros\n    image: pyouroboros/ouroboros\n    environment:\n      - CLEANUP=true\n      - INTERVAL=300\n      - LOG_LEVEL=info\n      - SELF_UPDATE=true\n      - IGNORE=mongo influxdb postgres mariadb\n      - TZ=America/Chicago\n    restart: unless-stopped\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock\n"
  },
  {
    "path": "ouroboros",
    "content": "#!/usr/bin/env python3\nfrom pyouroboros.ouroboros import main\n# stub to allow cli call\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "pyouroboros/__init__.py",
    "content": "VERSION = \"1.4.3\"\nBRANCH = \"master\"\n"
  },
  {
    "path": "pyouroboros/config.py",
    "content": "from os import environ\nfrom logging import getLogger\nfrom pyouroboros.logger import BlacklistFilter\n\n\nclass Config(object):\n    options = ['INTERVAL', 'PROMETHEUS', 'DOCKER_SOCKETS', 'MONITOR', 'IGNORE', 'LOG_LEVEL', 'PROMETHEUS_ADDR',\n               'PROMETHEUS_PORT', 'NOTIFIERS', 'REPO_USER', 'REPO_PASS', 'CLEANUP', 'RUN_ONCE', 'CRON',\n               'INFLUX_URL', 'INFLUX_PORT', 'INFLUX_USERNAME', 'INFLUX_PASSWORD', 'INFLUX_DATABASE', 'INFLUX_SSL',\n               'INFLUX_VERIFY_SSL', 'DATA_EXPORT', 'SELF_UPDATE', 'LABEL_ENABLE', 'DOCKER_TLS', 'LABELS_ONLY',\n               'DRY_RUN', 'HOSTNAME', 'DOCKER_TLS_VERIFY', 'SWARM', 'SKIP_STARTUP_NOTIFICATIONS']\n\n    hostname = environ.get('HOSTNAME')\n    interval = 300\n    cron = None\n    docker_sockets = 'unix://var/run/docker.sock'\n    docker_tls = False\n    docker_tls_verify = True\n    swarm = False\n    monitor = []\n    ignore = []\n    data_export = None\n    log_level = 'info'\n    cleanup = False\n    run_once = False\n    dry_run = False\n    self_update = False\n    label_enable = False\n    labels_only = False\n\n    repo_user = None\n    repo_pass = None\n    auth_json = None\n\n    prometheus = False\n    prometheus_addr = '127.0.0.1'\n    prometheus_port = 8000\n\n    influx_url = '127.0.0.1'\n    influx_port = 8086\n    influx_ssl = False\n    influx_verify_ssl = False\n    influx_username = 'root'\n    influx_password = 'root'\n    influx_database = None\n\n    notifiers = []\n    skip_startup_notifications = False\n\n    def __init__(self, environment_vars, cli_args):\n        self.cli_args = cli_args\n        self.environment_vars = environment_vars\n        self.filtered_strings = None\n\n        self.logger = getLogger()\n        self.parse()\n\n    def config_blacklist(self):\n        filtered_strings = [getattr(self, key.lower()) for key in Config.options\n                            if key.lower() in BlacklistFilter.blacklisted_keys]\n        # Clear None values\n        self.filtered_strings = list(filter(None, filtered_strings))\n        # take lists inside of list and append to list\n        for index, value in enumerate(self.filtered_strings, 0):\n            if isinstance(value, list):\n                self.filtered_strings.extend(self.filtered_strings.pop(index))\n                self.filtered_strings.insert(index, self.filtered_strings[-1:][0])\n        # Added matching for ports\n        ports = [string.split(':')[0] for string in self.filtered_strings if ':' in string]\n        self.filtered_strings.extend(ports)\n        # Added matching for tcp sockets. ConnectionPool ignores the tcp://\n        tcp_sockets = [string.split('//')[1] for string in self.filtered_strings if '//' in string]\n        self.filtered_strings.extend(tcp_sockets)\n        # Get JUST hostname from tcp//unix\n        for socket in getattr(self, 'docker_sockets'):\n            self.filtered_strings.append(socket.split('//')[1].split(':')[0])\n\n        for handler in self.logger.handlers:\n            handler.addFilter(BlacklistFilter(set(self.filtered_strings)))\n\n    def parse(self):\n        for option in Config.options:\n            if self.environment_vars.get(option):\n                env_opt = self.environment_vars[option]\n                if isinstance(env_opt, str):\n                    # Clean out quotes, both single/double and whitespace\n                    env_opt = env_opt.strip(\"'\").strip('\"').strip(' ')\n                if option in ['INTERVAL', 'PROMETHEUS_PORT', 'INFLUX_PORT']:\n                    try:\n                        opt = int(env_opt)\n                        setattr(self, option.lower(), opt)\n                    except ValueError as e:\n                        print(e)\n                elif option in ['CLEANUP', 'RUN_ONCE', 'INFLUX_SSL', 'INFLUX_VERIFY_SSL', 'DRY_RUN', 'SWARM',\n                                'SELF_UPDATE', 'LABEL_ENABLE', 'DOCKER_TLS', 'LABELS_ONLY', 'DOCKER_TLS_VERIFY',\n                                'SKIP_STARTUP_NOTIFICATIONS']:\n                    if env_opt.lower() in ['true', 'yes']:\n                        setattr(self, option.lower(), True)\n                    elif env_opt.lower() in ['false', 'no']:\n                        setattr(self, option.lower(), False)\n                    else:\n                        self.logger.error('%s is not true/yes, nor false/no for %s. Assuming %s',\n                                          env_opt, option, getattr(self, option))\n                else:\n                    setattr(self, option.lower(), env_opt)\n            elif vars(self.cli_args).get(option):\n                setattr(self, option.lower(), vars(self.cli_args).get(option))\n\n        # Specific var changes\n        if self.repo_user and self.repo_pass:\n            self.auth_json = {'Username': self.repo_user, 'Password': self.repo_pass}\n\n        if self.interval < 30:\n            self.interval = 30\n\n        if self.labels_only and not self.label_enable:\n            self.logger.warning('labels_only enabled but not in use without label_enable')\n\n        for option in ['docker_sockets', 'notifiers', 'monitor', 'ignore']:\n            if isinstance(getattr(self, option), str):\n                string_list = getattr(self, option)\n                setattr(self, option, [string for string in string_list.split(' ')])\n\n        # Config sanity checks\n        if self.cron:\n            cron_times = self.cron.strip().split(' ')\n            if len(cron_times) != 5:\n                self.logger.error(\"Cron must be in cron syntax. e.g. * * * * * (5 places). Ignoring and using interval\")\n                self.cron = None\n            else:\n                self.logger.info(\"Cron configuration is valid. Using Cron schedule %s\", cron_times)\n                self.cron = cron_times\n                self.interval = None\n\n        if self.data_export == 'influxdb' and not self.influx_database:\n            self.logger.error(\"You need to specify an influx database if you want to export to influxdb. Disabling \"\n                              \"influxdb data export.\")\n\n        if self.data_export == 'prometheus' and self.self_update:\n            self.logger.warning(\"If you bind a port to ouroboros, it will be lost when it updates itself.\")\n\n        if self.dry_run and not self.run_once:\n            self.logger.warning(\"Dry run is designed to be ran with run once. Setting for you.\")\n            self.run_once = True\n\n        # Remove default config that is not used for cleaner logs\n        if self.data_export != 'prometheus':\n            self.prometheus_addr, self.prometheus_port = None, None\n\n        if self.data_export != 'influxdb':\n            self.influx_url, self.influx_port, self.influx_username, self.influx_password = None, None, None, None\n\n        self.config_blacklist()\n"
  },
  {
    "path": "pyouroboros/dataexporters.py",
    "content": "import prometheus_client\n\nfrom logging import getLogger\nfrom influxdb import InfluxDBClient\nfrom datetime import datetime, timezone\n\n\nclass DataManager(object):\n    def __init__(self, config):\n        self.config = config\n        self.logger = getLogger()\n        self.enabled = True\n\n        self.monitored_containers = {}\n        self.total_updated = {}\n\n        self.prometheus = PrometheusExporter(self, config) if self.config.data_export == \"prometheus\" else None\n        self.influx = InfluxClient(self, config) if self.config.data_export == \"influxdb\" else None\n\n    def add(self, label, socket):\n        if self.config.data_export == \"prometheus\" and self.enabled:\n            self.prometheus.update(label, socket)\n\n        elif self.config.data_export == \"influxdb\" and self.enabled:\n            if label == \"all\":\n                self.logger.debug(\"Total containers updated %s\", self.total_updated[socket])\n\n            self.influx.write_points(label, socket)\n\n    def set(self, socket):\n        if self.config.data_export == \"prometheus\" and self.enabled:\n            self.prometheus.set_monitored(socket)\n\n\nclass PrometheusExporter(object):\n    def __init__(self, data_manager, config):\n        self.config = config\n        self.data_manager = data_manager\n        self.http_server = prometheus_client.start_http_server(\n            self.config.prometheus_port,\n            addr=self.config.prometheus_addr\n        )\n        self.updated_containers_counter = prometheus_client.Counter(\n            'containers_updated',\n            'Count of containers updated',\n            ['socket', 'container']\n        )\n        self.monitored_containers_gauge = prometheus_client.Gauge(\n            'containers_being_monitored',\n            'Gauge of containers being monitored',\n            ['socket']\n        )\n        self.updated_all_containers_gauge = prometheus_client.Gauge(\n            'all_containers_updated',\n            'Count of total updated',\n            ['socket']\n        )\n        self.logger = getLogger()\n\n    def set_monitored(self, socket):\n        \"\"\"Set number of containers being monitoring with a gauge\"\"\"\n        self.monitored_containers_gauge.labels(socket=socket).set(self.data_manager.monitored_containers[socket])\n        self.logger.debug(\"Prometheus Exporter monitored containers gauge set to %s\",\n                          self.data_manager.monitored_containers[socket])\n\n    def update(self, label, socket):\n        \"\"\"Set container update count based on label\"\"\"\n        if label == \"all\":\n            self.updated_all_containers_gauge.labels(socket=socket).set(self.data_manager.total_updated[socket])\n        else:\n            self.updated_containers_counter.labels(socket=socket, container=label).inc()\n\n        self.logger.debug(\"Prometheus Exporter container update counter incremented for %s\", label)\n\n\nclass InfluxClient(object):\n    def __init__(self, data_manger, config):\n        self.data_manager = data_manger\n        self.config = config\n        self.logger = getLogger()\n        self.influx = InfluxDBClient(\n            host=self.config.influx_url,\n            port=self.config.influx_port,\n            username=self.config.influx_username,\n            password=self.config.influx_password,\n            database=self.config.influx_database,\n            ssl=self.config.influx_ssl,\n            verify_ssl=self.config.influx_verify_ssl\n        )\n        self.db_check()\n\n    def db_check(self):\n        database_dicts = self.influx.get_list_database()\n        databases = [d['name'] for d in database_dicts]\n        if self.config.influx_database in databases:\n            self.logger.debug(\"Influxdb database existence check passed for %s\", self.config.influx_database)\n        else:\n            self.logger.debug(\"Influxdb database existence failed for %s. Disabling exports.\",\n                              self.config.influx_database)\n            self.data_manager.enabled = False\n\n    def write_points(self, label, socket):\n        clean_socket = socket.split(\"//\")[1]\n        now = datetime.now(timezone.utc).astimezone().isoformat()\n        influx_payload = [\n            {\n                \"measurement\": \"Ouroboros\",\n                \"tags\": {'socket': clean_socket},\n                \"time\": now,\n                \"fields\": {}\n            },\n            {\n                \"measurement\": \"Ouroboros\",\n                \"tags\": {'configuration': self.config.hostname},\n                \"time\": now,\n                \"fields\": {key: (value if not isinstance(value, list) else ' '.join(value)) for key, value in\n                           vars(self.config).items() if key.upper() in self.config.options}\n            }\n        ]\n        if label == \"all\":\n            influx_payload[0]['tags'][\"type\"] = \"stats\"\n            influx_payload[0]['fields'] = {\n                \"monitored_containers\": self.data_manager.monitored_containers[socket],\n                \"updated_count\": self.data_manager.total_updated[socket]\n            }\n        else:\n            influx_payload[0]['tags'].update(\n                {\n                    \"type\": \"container_update\",\n                    \"container\": label\n                }\n            )\n            influx_payload[0]['fields'] = {\"count\": 1}\n\n        self.logger.debug(\"Writing data to influxdb: %s\", influx_payload)\n        self.influx.write_points(influx_payload)\n"
  },
  {
    "path": "pyouroboros/dockerclient.py",
    "content": "from time import sleep\nfrom logging import getLogger\nfrom docker import DockerClient, tls\nfrom os.path import isdir, isfile, join\nfrom docker.errors import DockerException, APIError, NotFound\n\nfrom pyouroboros.helpers import set_properties, remove_sha_prefix, get_digest\n\n\nclass Docker(object):\n    def __init__(self, socket, config, data_manager, notification_manager):\n        self.config = config\n        self.socket = socket\n        self.client = self.connect()\n        self.data_manager = data_manager\n        self.logger = getLogger()\n\n        self.notification_manager = notification_manager\n\n    def connect(self):\n        if self.config.docker_tls:\n            try:\n                cert_paths = {\n                    'cert_top_dir': '/etc/docker/certs.d/',\n                    'clean_socket': self.socket.split('//')[1]\n                }\n                cert_paths['cert_dir'] = join(cert_paths['cert_top_dir'], cert_paths['clean_socket'])\n                cert_paths['cert_files'] = {\n                    'client_cert': join(cert_paths['cert_dir'], 'client.cert'),\n                    'client_key': join(cert_paths['cert_dir'], 'client.key'),\n                    'ca_crt': join(cert_paths['cert_dir'], 'ca.crt')\n                }\n\n                if not isdir(cert_paths['cert_dir']):\n                    self.logger.error('%s is not a valid cert folder', cert_paths['cert_dir'])\n                    raise ValueError\n\n                for cert_file in cert_paths['cert_files'].values():\n                    if not isfile(cert_file):\n                        self.logger.error('%s does not exist', cert_file)\n                        raise ValueError\n\n                tls_config = tls.TLSConfig(\n                    ca_cert=cert_paths['cert_files']['ca_crt'],\n                    verify=cert_paths['cert_files']['ca_crt'] if self.config.docker_tls_verify else False,\n                    client_cert=(cert_paths['cert_files']['client_cert'], cert_paths['cert_files']['client_key'])\n                )\n                client = DockerClient(base_url=self.socket, tls=tls_config)\n            except ValueError:\n                self.logger.error('Invalid Docker TLS config for %s, reverting to unsecured', self.socket)\n                client = DockerClient(base_url=self.socket)\n        else:\n            client = DockerClient(base_url=self.socket)\n\n        return client\n\n\nclass BaseImageObject(object):\n    def __init__(self, docker_client):\n        self.docker = docker_client\n        self.logger = self.docker.logger\n        self.config = self.docker.config\n        self.client = self.docker.client\n        self.socket = self.docker.socket\n        self.data_manager = self.docker.data_manager\n        self.data_manager.total_updated[self.socket] = 0\n        self.notification_manager = self.docker.notification_manager\n\n    def _pull(self, tag):\n        \"\"\"Docker pull image tag\"\"\"\n        self.logger.debug('Checking tag: %s', tag)\n        try:\n            if self.config.dry_run:\n                # The authentication doesn't work with this call\n                # See bugs https://github.com/docker/docker-py/issues/2225\n                return self.client.images.get_registry_data(tag)\n            else:\n                if self.config.auth_json:\n                    return_image = self.client.images.pull(tag, auth_config=self.config.auth_json)\n                else:\n                    return_image = self.client.images.pull(tag)\n                return return_image\n        except APIError as e:\n            if '<html>' in str(e):\n                self.logger.debug(\"Docker api issue. Ignoring\")\n                raise ConnectionError\n            elif 'unauthorized' in str(e):\n                if self.config.dry_run:\n                    self.logger.error('dry run : Upstream authentication issue while checking %s. See: '\n                                      'https://github.com/docker/docker-py/issues/2225', tag)\n                    raise ConnectionError\n                else:\n                    self.logger.critical(\"Invalid Credentials. Exiting\")\n                    exit(1)\n            elif 'Client.Timeout' in str(e):\n                self.logger.critical(\n                    \"Couldn't find an image on docker.com for %s. Local Build?\", tag)\n                raise ConnectionError\n            elif ('pull access' or 'TLS handshake') in str(e):\n                self.logger.critical(\"Couldn't pull. Skipping. Error: %s\", e)\n                raise ConnectionError\n\n\nclass Container(BaseImageObject):\n    mode = 'container'\n\n    def __init__(self, docker_client):\n        super().__init__(docker_client)\n        self.monitored = self.monitor_filter()\n\n    # Container sub functions\n    def stop(self, container):\n        self.logger.debug('Stopping container: %s', container.name)\n        stop_signal = container.labels.get('com.ouroboros.stop_signal', False)\n        if stop_signal:\n            try:\n                container.kill(signal=stop_signal)\n            except APIError as e:\n                self.logger.error('Cannot kill container using signal %s. stopping normally. Error: %s',\n                                  stop_signal, e)\n                container.stop()\n        else:\n            container.stop()\n\n    def remove(self, container):\n        self.logger.debug('Removing container: %s', container.name)\n        try:\n            container.remove()\n        except NotFound as e:\n            self.logger.error(\"Could not remove container. Error: %s\", e)\n            return\n\n    def recreate(self, container, latest_image):\n        new_config = set_properties(old=container, new=latest_image)\n\n        self.stop(container)\n        self.remove(container)\n\n        created = self.client.api.create_container(**new_config)\n        new_container = self.client.containers.get(created.get(\"Id\"))\n\n        # connect the new container to all networks of the old container\n        for network_name, network_config in container.attrs['NetworkSettings']['Networks'].items():\n            network = self.client.networks.get(network_config['NetworkID'])\n            try:\n                network.disconnect(new_container.id, force=True)\n            except APIError:\n                pass\n            new_network_config = {\n                'container': new_container,\n                'aliases': network_config['Aliases'],\n                'links': network_config['Links']\n            }\n            if network_config['IPAMConfig']:\n                new_network_config.update(\n                    {\n                        'ipv4_address': network_config['IPAddress'],\n                        'ipv6_address': network_config['GlobalIPv6Address']\n                    }\n                )\n            try:\n                network.connect(**new_network_config)\n            except APIError as e:\n                if any(err in str(e) for err in ['user configured subnets', 'user defined networks']):\n                    if new_network_config.get('ipv4_address'):\n                        del new_network_config['ipv4_address']\n                    if new_network_config.get('ipv6_address'):\n                        del new_network_config['ipv6_address']\n                    network.connect(**new_network_config)\n                else:\n                    self.logger.error('Unable to attach updated container to network \"%s\". Error: %s', network.name, e)\n\n        new_container.start()\n\n    def pull(self, current_tag):\n        \"\"\"Docker pull image tag\"\"\"\n        tag = current_tag\n        if not tag:\n            self.logger.error('Missing tag. Skipping...')\n            raise ConnectionError\n        elif ':' not in tag:\n            tag = f'{tag}:latest'\n        return self._pull(tag)\n\n    # Filters\n    def running_filter(self):\n        \"\"\"Return running container objects list, except ouroboros itself\"\"\"\n        running_containers = []\n        try:\n            for container in self.client.containers.list(filters={'status': 'running'}):\n                if self.config.self_update:\n                    running_containers.append(container)\n                else:\n                    try:\n                        if 'ouroboros' not in container.image.tags[0]:\n                            if container.attrs['HostConfig']['AutoRemove']:\n                                self.logger.debug(\"Skipping %s due to --rm property.\", container.name)\n                            else:\n                                running_containers.append(container)\n                    except IndexError:\n                        self.logger.error(\"%s has no tags.. you should clean it up! Ignoring.\", container.id)\n                        continue\n\n        except DockerException:\n            self.logger.critical(\"Can't connect to Docker API at %s\", self.config.docker_socket)\n            exit(1)\n\n        return running_containers\n\n    def monitor_filter(self):\n        \"\"\"Return filtered running container objects list\"\"\"\n        running_containers = self.running_filter()\n        monitored_containers = []\n\n        for container in running_containers:\n            ouro_label = container.labels.get('com.ouroboros.enable', False)\n            # if labels enabled, use the label. 'true/yes' trigger monitoring.\n            if self.config.label_enable and ouro_label:\n                if ouro_label.lower() in [\"true\", \"yes\"]:\n                    monitored_containers.append(container)\n                else:\n                    continue\n            elif not self.config.labels_only:\n                if self.config.monitor:\n                    if container.name in self.config.monitor and container.name not in self.config.ignore:\n                        monitored_containers.append(container)\n                elif container.name not in self.config.ignore:\n                    monitored_containers.append(container)\n\n        self.data_manager.monitored_containers[self.socket] = len(monitored_containers)\n        self.data_manager.set(self.socket)\n\n        return monitored_containers\n\n    # Socket Functions\n    def self_check(self):\n        if self.config.self_update:\n            me_list = [container for container in self.client.containers.list() if 'ouroboros' in container.name]\n            if len(me_list) > 1:\n                self.update_self(count=2, me_list=me_list)\n\n    def socket_check(self):\n        depends_on_names = []\n        hard_depends_on_names = []\n        updateable = []\n        self.monitored = self.monitor_filter()\n\n        if not self.monitored:\n            self.logger.info('No containers are running or monitored on %s', self.socket)\n            return\n\n        for container in self.monitored:\n            current_image = container.image\n            current_tag = container.attrs['Config']['Image']\n\n            try:\n                latest_image = self.pull(current_tag)\n            except ConnectionError:\n                continue\n\n            try:\n                if current_image.id != latest_image.id:\n                    updateable.append((container, current_image, latest_image))\n                else:\n                    continue\n            except AttributeError:\n                self.logger.error(\"Issue detecting %s's image tag. Skipping...\", container.name)\n\n            # Get container list to restart after update complete\n            depends_on = container.labels.get('com.ouroboros.depends_on', False)\n            hard_depends_on = container.labels.get('com.ouroboros.hard_depends_on', False)\n            if depends_on:\n                depends_on_names.extend([name.strip() for name in depends_on.split(',')])\n            if hard_depends_on:\n                hard_depends_on_names.extend([name.strip() for name in hard_depends_on.split(',')])\n\n        hard_depends_on_containers = []\n        hard_depends_on_names = list(set(hard_depends_on_names))\n        for name in hard_depends_on_names:\n            try:\n                hard_depends_on_containers.append(self.client.containers.get(name))\n            except NotFound:\n                self.logger.error(\"Could not find dependant container %s on socket %s. Ignoring\", name, self.socket)\n\n        depends_on_containers = []\n        depends_on_names = list(set(depends_on_names))\n        depends_on_names = [name for name in depends_on_names if name not in hard_depends_on_names]\n        for name in depends_on_names:\n            try:\n                depends_on_containers.append(self.client.containers.get(name))\n            except NotFound:\n                self.logger.error(\"Could not find dependant container %s on socket %s. Ignoring\", name, self.socket)\n\n        return updateable, depends_on_containers, hard_depends_on_containers\n\n    def update(self):\n        updated_count = 0\n        try:\n            updateable, depends_on_containers, hard_depends_on_containers = self.socket_check()\n        except TypeError:\n            return\n\n        for container in depends_on_containers + hard_depends_on_containers:\n            self.stop(container)\n\n        for container, current_image, latest_image in updateable:\n            if self.config.dry_run:\n                # Ugly hack for repo digest\n                repo_digest_id = current_image.attrs['RepoDigests'][0].split('@')[1]\n                if repo_digest_id != latest_image.id:\n                    self.logger.info('dry run : %s would be updated', container.name)\n                continue\n\n            if container.name in ['ouroboros', 'ouroboros-updated']:\n                self.data_manager.total_updated[self.socket] += 1\n                self.data_manager.add(label=container.name, socket=self.socket)\n                self.data_manager.add(label='all', socket=self.socket)\n                self.notification_manager.send(container_tuples=updateable,\n                                               socket=self.socket, kind='update')\n                self.update_self(old_container=container, new_image=latest_image, count=1)\n\n            self.logger.info('%s will be updated', container.name)\n\n            self.recreate(container, latest_image)\n\n            if self.config.cleanup:\n                try:\n                    self.client.images.remove(current_image.id)\n                except APIError as e:\n                    self.logger.error(\"Could not delete old image for %s, Error: %s\", container.name, e)\n            updated_count += 1\n\n            self.logger.debug(\"Incrementing total container updated count\")\n\n            self.data_manager.total_updated[self.socket] += 1\n            self.data_manager.add(label=container.name, socket=self.socket)\n            self.data_manager.add(label='all', socket=self.socket)\n\n        for container in depends_on_containers:\n            # Reload container to ensure it isn't referencing the old image\n            container.reload()\n            container.start()\n\n        for container in hard_depends_on_containers:\n            self.recreate(container, container.image)\n\n        if updated_count > 0:\n            self.notification_manager.send(container_tuples=updateable, socket=self.socket, kind='update')\n\n    def update_self(self, count=None, old_container=None, me_list=None, new_image=None):\n        if count == 2:\n            self.logger.debug('God im messy... cleaning myself up.')\n            old_me_id = me_list[0].id if me_list[0].attrs['Created'] < me_list[1].attrs['Created'] else me_list[1].id\n            old_me = self.client.containers.get(old_me_id)\n            old_me_image_id = old_me.image.id\n\n            old_me.stop()\n            old_me.remove()\n\n            self.client.images.remove(old_me_image_id)\n            self.logger.debug('Ahhh. All better.')\n\n            self.monitored = self.monitor_filter()\n        elif count == 1:\n            self.logger.debug('I need to update! Starting the ouroboros ;)')\n            self_name = 'ouroboros-updated' if old_container.name == 'ouroboros' else 'ouroboros'\n            new_config = set_properties(old=old_container, new=new_image, self_name=self_name)\n            try:\n                me_created = self.client.api.create_container(**new_config)\n                new_me = self.client.containers.get(me_created.get(\"Id\"))\n                new_me.start()\n                self.logger.debug('If you strike me down, I shall become '\n                                  'more powerful than you could possibly imagine.')\n                self.logger.debug('https://bit.ly/2VVY7GH')\n                sleep(30)\n            except APIError as e:\n                self.logger.error(\"Self update failed.\")\n                self.logger.error(e)\n\n\nclass Service(BaseImageObject):\n    mode = 'service'\n\n    def __init__(self, docker_client):\n        super().__init__(docker_client)\n        self.monitored = self.monitor_filter()\n\n    def monitor_filter(self):\n        \"\"\"Return filtered service objects list\"\"\"\n        services = self.client.services.list(filters={'label': 'com.ouroboros.enable'})\n\n        monitored_services = []\n\n        for service in services:\n            ouro_label = service.attrs['Spec']['Labels'].get('com.ouroboros.enable')\n            if not self.config.label_enable or ouro_label.lower() in [\"true\", \"yes\"]:\n                monitored_services.append(service)\n\n        self.data_manager.monitored_containers[self.socket] = len(monitored_services)\n        self.data_manager.set(self.socket)\n\n        return monitored_services\n\n    def pull(self, tag):\n        \"\"\"Docker pull image tag\"\"\"\n        return self._pull(tag)\n\n    def update(self):\n        updated_service_tuples = []\n        self.monitored = self.monitor_filter()\n\n        if not self.monitored:\n            self.logger.info('No services monitored')\n\n        for service in self.monitored:\n            image_string = service.attrs['Spec']['TaskTemplate']['ContainerSpec']['Image']\n            if '@' in image_string:\n                tag = image_string.split('@')[0]\n                sha256 = remove_sha_prefix(image_string.split('@')[1])\n            else:\n                self.logger.error('No image SHA for %s. Skipping', image_string)\n                continue\n\n            try:\n                latest_image = self.pull(tag)\n            except ConnectionError:\n                continue\n\n            latest_image_sha256 = get_digest(latest_image)\n            self.logger.debug('Latest sha256 for %s is %s', tag, latest_image_sha256)\n\n            if sha256 != latest_image_sha256:\n                if self.config.dry_run:\n                    # Ugly hack for repo digest\n                    self.logger.info('dry run : %s would be updated', service.name)\n                    continue\n\n                updated_service_tuples.append(\n                    (service, sha256[-10:], latest_image)\n                )\n\n                if 'ouroboros' in service.name and self.config.self_update:\n                    self.data_manager.total_updated[self.socket] += 1\n                    self.data_manager.add(label=service.name, socket=self.socket)\n                    self.data_manager.add(label='all', socket=self.socket)\n                    self.notification_manager.send(container_tuples=updated_service_tuples,\n                                                   socket=self.socket, kind='update', mode='service')\n\n                self.logger.info('%s will be updated', service.name)\n                service.update(image=f\"{tag}@sha256:{latest_image_sha256}\")\n\n                self.data_manager.total_updated[self.socket] += 1\n                self.data_manager.add(label=service.name, socket=self.socket)\n                self.data_manager.add(label='all', socket=self.socket)\n\n        if updated_service_tuples:\n            self.notification_manager.send(\n                container_tuples=updated_service_tuples,\n                socket=self.socket,\n                kind='update',\n                mode='service'\n            )\n"
  },
  {
    "path": "pyouroboros/helpers.py",
    "content": "def set_properties(old, new, self_name=None):\n    \"\"\"Store object for spawning new container in place of the one with outdated image\"\"\"\n    properties = {\n        'name': self_name if self_name else old.name,\n        'hostname': old.attrs['Config']['Hostname'],\n        'user': old.attrs['Config']['User'],\n        'detach': True,\n        'domainname': old.attrs['Config']['Domainname'],\n        'tty': old.attrs['Config']['Tty'],\n        'ports': None if not old.attrs['Config'].get('ExposedPorts') else [\n            (p.split('/')[0], p.split('/')[1]) for p in old.attrs['Config']['ExposedPorts'].keys()\n        ],\n        'volumes': None if not old.attrs['Config'].get('Volumes') else [\n            v for v in old.attrs['Config']['Volumes'].keys()\n        ],\n        'working_dir': old.attrs['Config']['WorkingDir'],\n        'image': new.tags[0],\n        'command': old.attrs['Config']['Cmd'],\n        'host_config': old.attrs['HostConfig'],\n        'labels': old.attrs['Config']['Labels'],\n        'entrypoint': old.attrs['Config']['Entrypoint'],\n        'environment': old.attrs['Config']['Env'],\n        'healthcheck': old.attrs['Config'].get('Healthcheck', None)\n    }\n\n    return properties\n\n\ndef remove_sha_prefix(digest):\n    if digest.startswith(\"sha256:\"):\n        return digest[7:]\n    return digest\n\n\ndef get_digest(image):\n    digest = image.attrs.get(\n            \"Descriptor\", {}\n        ).get(\"digest\") or image.attrs.get(\n            \"RepoDigests\"\n        )[0].split('@')[1] or image.id\n    return remove_sha_prefix(digest)\n"
  },
  {
    "path": "pyouroboros/logger.py",
    "content": "from logging import Filter, getLogger, Formatter, StreamHandler\n\n\nclass BlacklistFilter(Filter):\n    \"\"\"\n    Log filter for blacklisted tokens and passwords\n    \"\"\"\n\n    blacklisted_keys = ['repo_user', 'repo_pass', 'auth_json', 'docker_sockets', 'prometheus_addr',\n                        'influx_username', 'influx_password', 'influx_url', 'notifiers']\n\n    def __init__(self, filteredstrings):\n        super().__init__()\n        self.filtered_strings = filteredstrings\n\n    def filter(self, record):\n        for item in self.filtered_strings:\n            try:\n                if item in record.msg:\n                    record.msg = record.msg.replace(item, 8 * '*' + item[-5:])\n                if any(item in str(arg) for arg in record.args):\n                    record.args = tuple(arg.replace(item, 8 * '*' + item[-5:]) if isinstance(arg, str) else arg\n                                        for arg in record.args)\n            except TypeError:\n                pass\n        return True\n\n\nclass OuroborosLogger(object):\n    def __init__(self, level='INFO'):\n        # Create the Logger\n        self.logger = getLogger()\n        try:\n            self.logger.setLevel(level.upper())\n        except ValueError:\n            level = \"INFO\"\n            self.logger.setLevel(level.upper())\n\n        # Create a Formatter for formatting the log messages\n        logger_formatter = Formatter('%(asctime)s : %(levelname)s : %(module)s : %(message)s', '%Y-%m-%d %H:%M:%S')\n\n        # Add the console logger\n        console_logger = StreamHandler()\n        console_logger.setFormatter(logger_formatter)\n\n        console_logger.setLevel(level.upper())\n\n        # Add the Handler to the Logger\n        self.logger.addHandler(console_logger)\n\n        # Less verbose apscheduler logging if info\n        if self.logger.getEffectiveLevel() == 20:\n            getLogger('apscheduler').setLevel('WARNING')\n"
  },
  {
    "path": "pyouroboros/notifiers.py",
    "content": "import apprise\n\nfrom logging import getLogger\nfrom datetime import datetime, timezone\n\n\nclass NotificationManager(object):\n    def __init__(self, config, data_manager):\n        self.config = config\n        self.data_manager = data_manager\n        self.logger = getLogger()\n\n        self.apprise = self.build_apprise()\n\n    def build_apprise(self):\n        asset = apprise.AppriseAsset(\n            image_url_mask='https://i.imgur.com/L40ksWY.png',\n            default_extension='.png'\n        )\n        asset.app_id = \"Ouroboros\"\n        asset.app_desc = \"Ouroboros\"\n        asset.app_url = \"https://github.com/pyouroboros/ouroboros\"\n        asset.html_notify_map['info'] = '#5F87C6'\n        asset.image_url_logo = 'https://bin.cajun.pro/images/ouroboros/notifications/ouroboros-logo-256x256.png'\n\n        apprise_obj = apprise.Apprise(asset=asset)\n\n        for notifier in self.config.notifiers:\n            add = apprise_obj.add(notifier)\n            if not add:\n                self.logger.error('Could not add notifier %s', notifier)\n\n        return apprise_obj\n\n    def send(self, container_tuples=None, socket=None, kind='update', next_run=None, mode='container'):\n        if kind == 'startup':\n            now = datetime.now(timezone.utc).astimezone()\n            title = f'Ouroboros has started'\n            body_fields = [\n                f'Host: {self.config.hostname}',\n                f'Time: {now.strftime(\"%Y-%m-%d %H:%M:%S\")}',\n                f'Next Run: {next_run}']\n        else:\n            title = 'Ouroboros has updated containers!'\n            body_fields = [\n                f\"Host/Socket: {self.config.hostname} / {socket.split('//')[1]}\",\n                f\"Containers Monitored: {self.data_manager.monitored_containers[socket]}\",\n                f\"Total Containers Updated: {self.data_manager.total_updated[socket]}\",\n                f\"Containers updated this pass: {len(container_tuples)}\"\n            ]\n            body_fields.extend(\n                [\n                    \"{} updated from {} to {}\".format(\n                        container.name,\n                        old_image if mode == 'service' else old_image.short_id.split(':')[1],\n                        new_image.short_id.split(':')[1]\n                    ) for container, old_image, new_image in container_tuples\n                ]\n            )\n        body = '\\r\\n'.join(body_fields)\n\n        if self.apprise.servers:\n            self.apprise.notify(title=title, body=body)\n"
  },
  {
    "path": "pyouroboros/ouroboros.py",
    "content": "from time import sleep\nfrom os import environ\n\nfrom requests.exceptions import ConnectionError\nfrom datetime import datetime, timezone, timedelta\nfrom argparse import ArgumentParser, RawTextHelpFormatter\nfrom apscheduler.schedulers.background import BackgroundScheduler\n\nfrom pyouroboros.config import Config\nfrom pyouroboros import VERSION, BRANCH\nfrom pyouroboros.logger import OuroborosLogger\nfrom pyouroboros.dataexporters import DataManager\nfrom pyouroboros.notifiers import NotificationManager\nfrom pyouroboros.dockerclient import Docker, Container, Service\n\n\ndef main():\n    \"\"\"Declare command line options\"\"\"\n    parser = ArgumentParser(description='ouroboros', formatter_class=RawTextHelpFormatter,\n                            epilog='EXAMPLE: ouroboros -d tcp://1.2.3.4:5678 -i 20 -m container1 container2 -l warn')\n\n    core_group = parser.add_argument_group(\"Core\", \"Configuration of core functionality\")\n    core_group.add_argument('-v', '--version', action='version', version=VERSION)\n\n    core_group.add_argument('-d', '--docker-sockets', nargs='+', default=Config.docker_sockets, dest='DOCKER_SOCKETS',\n                            help='Sockets for docker management\\n'\n                                 'DEFAULT: \"unix://var/run/docker.sock\"\\n'\n                                 'EXAMPLE: -d unix://var/run/docker.sock tcp://192.168.1.100:2376')\n\n    core_group.add_argument('-t', '--docker-tls', default=Config.docker_tls, dest='DOCKER_TLS',\n                            action='store_true', help='Enable docker TLS\\n'\n                                                      'REQUIRES: docker cert mount')\n\n    core_group.add_argument('-T', '--docker-tls-verify', default=Config.docker_tls_verify, dest='DOCKER_TLS_VERIFY',\n                            action='store_false', help='Verify the CA Certificate mounted for TLS\\n'\n                                                       'DEFAULT: True')\n\n    core_group.add_argument('-i', '--interval', type=int, default=Config.interval, dest='INTERVAL',\n                            help='Interval in seconds between checking for updates\\n'\n                                 'DEFAULT: 300')\n\n    core_group.add_argument('-C', '--cron', default=Config.cron, dest='CRON',\n                            help='Cron formatted string for scheduling\\n'\n                                 'EXAMPLE: \"*/5 * * * *\"')\n\n    core_group.add_argument('-l', '--log-level', choices=['debug', 'info', 'warn', 'error', 'critical'],\n                            dest='LOG_LEVEL', default=Config.log_level, help='Set logging level\\n'\n                                                                             'DEFAULT: info')\n\n    core_group.add_argument('-u', '--self-update', default=Config.self_update, dest='SELF_UPDATE', action='store_true',\n                            help='Let ouroboros update itself')\n\n    core_group.add_argument('-S', '--swarm', default=Config.swarm, dest='SWARM', action='store_true',\n                            help='Put ouroboros in swarm mode')\n\n    core_group.add_argument('-o', '--run-once', default=Config.run_once, action='store_true', dest='RUN_ONCE',\n                            help='Single run')\n\n    core_group.add_argument('-A', '--dry-run', default=Config.dry_run, action='store_true', dest='DRY_RUN',\n                            help='Run without making changes. Best used with run-once')\n\n    core_group.add_argument('-N', '--notifiers', nargs='+', default=Config.notifiers, dest='NOTIFIERS',\n                            help='Apprise formatted notifiers\\n'\n                                 'EXAMPLE: -N discord://1234123412341234/jasdfasdfasdfasddfasdf '\n                                 'mailto://user:pass@gmail.com')\n\n    docker_group = parser.add_argument_group(\"Docker\", \"Configuration of docker functionality\")\n    docker_group.add_argument('-m', '--monitor', nargs='+', default=Config.monitor, dest='MONITOR',\n                              help='Which container(s) to monitor\\n'\n                                   'DEFAULT: All')\n\n    docker_group.add_argument('-n', '--ignore', nargs='+', default=Config.ignore, dest='IGNORE',\n                              help='Container(s) to ignore\\n'\n                                   'EXAMPLE: -n container1 container2')\n\n    docker_group.add_argument('-k', '--label-enable', default=Config.label_enable, dest='LABEL_ENABLE',\n                              action='store_true', help='Enable label monitoring for ouroboros label options\\n'\n                                                        'Note: labels take precedence'\n                                                        'DEFAULT: False')\n\n    docker_group.add_argument('-M', '--labels-only', default=Config.labels_only, dest='LABELS_ONLY',\n                              action='store_true', help='Only watch containers that utilize labels\\n'\n                                                        'This allows a more strict compliance for environments'\n                                                        'DEFAULT: False')\n\n    docker_group.add_argument('-c', '--cleanup', default=Config.cleanup, dest='CLEANUP', action='store_true',\n                              help='Remove old images after updating')\n\n    docker_group.add_argument('-r', '--repo-user', default=Config.repo_user, dest='REPO_USER',\n                              help='Private docker registry username\\n'\n                                   'EXAMPLE: foo@bar.baz')\n\n    docker_group.add_argument('-R', '--repo-pass', default=Config.repo_pass, dest='REPO_PASS',\n                              help='Private docker registry password\\n'\n                                   'EXAMPLE: MyPa$$w0rd')\n\n    data_group = parser.add_argument_group('Data Export', 'Configuration of data export functionality')\n    data_group.add_argument('-D', '--data-export', choices=['prometheus', 'influxdb'], default=Config.data_export,\n                            dest='DATA_EXPORT', help='Enable exporting of data for chosen option')\n\n    data_group.add_argument('-a', '--prometheus-addr', default=Config.prometheus_addr,\n                            dest='PROMETHEUS_ADDR', help='Bind address to run Prometheus exporter on\\n'\n                                                         'DEFAULT: 127.0.0.1')\n\n    data_group.add_argument('-p', '--prometheus-port', type=int, default=Config.prometheus_port,\n                            dest='PROMETHEUS_PORT', help='Port to run Prometheus exporter on\\n'\n                                                         'DEFAULT: 8000')\n\n    data_group.add_argument('-I', '--influx-url', default=Config.influx_url, dest='INFLUX_URL',\n                            help='URL for influxdb\\n'\n                                  'DEFAULT: 127.0.0.1')\n\n    data_group.add_argument('-P', '--influx-port', type=int, default=Config.influx_port, dest='INFLUX_PORT',\n                            help='PORT for influxdb\\n'\n                                  'DEFAULT: 8086')\n\n    data_group.add_argument('-U', '--influx-username', default=Config.influx_username, dest='INFLUX_USERNAME',\n                            help='Username for influxdb\\n'\n                                  'DEFAULT: root')\n\n    data_group.add_argument('-x', '--influx-password', default=Config.influx_password, dest='INFLUX_PASSWORD',\n                            help='Password for influxdb\\n'\n                                  'DEFAULT: root')\n\n    data_group.add_argument('-X', '--influx-database', default=Config.influx_database, dest='INFLUX_DATABASE',\n                            help='Influx database name. Required if using influxdb')\n\n    data_group.add_argument('-s', '--influx-ssl', default=Config.influx_ssl, dest='INFLUX_SSL', action='store_true',\n                            help='Use SSL when connecting to influxdb')\n\n    data_group.add_argument('-V', '--influx-verify-ssl', default=Config.influx_verify_ssl, dest='INFLUX_VERIFY_SSL',\n                            action='store_true', help='Verify SSL certificate when connecting to influxdb')\n\n    docker_group.add_argument('--skip-startup-notifications', default=Config.skip_startup_notifications,\n                              dest='SKIP_STARTUP_NOTIFICATIONS', action='store_true',\n                              help='Do not send ouroboros notifications when starting')\n\n    args = parser.parse_args()\n\n    if environ.get('LOG_LEVEL'):\n        log_level = environ.get('LOG_LEVEL')\n    else:\n        log_level = args.LOG_LEVEL\n    ol = OuroborosLogger(level=log_level)\n    ol.logger.info('Version: %s-%s', VERSION, BRANCH)\n    config = Config(environment_vars=environ, cli_args=args)\n    config_dict = {key: value for key, value in vars(config).items() if key.upper() in config.options}\n    ol.logger.debug(\"Ouroboros configuration: %s\", config_dict)\n\n    data_manager = DataManager(config)\n    notification_manager = NotificationManager(config, data_manager)\n    scheduler = BackgroundScheduler()\n    scheduler.start()\n\n    for socket in config.docker_sockets:\n        try:\n            docker = Docker(socket, config, data_manager, notification_manager)\n            if config.swarm:\n                mode = Service(docker)\n            else:\n                mode = Container(docker)\n\n            if config.run_once:\n                scheduler.add_job(mode.update, name=f'Run Once container update for {socket}')\n            else:\n                if mode.mode == 'container':\n                    scheduler.add_job(mode.self_check, name=f'Self Check for {socket}')\n                if config.cron:\n                    scheduler.add_job(\n                        mode.update,\n                        name=f'Cron container update for {socket}',\n                        trigger='cron',\n                        minute=config.cron[0],\n                        hour=config.cron[1],\n                        day=config.cron[2],\n                        month=config.cron[3],\n                        day_of_week=config.cron[4],\n                        misfire_grace_time=15\n                    )\n                else:\n                    scheduler.add_job(\n                        mode.update,\n                        name=f'Initial run interval container update for {socket}'\n                    )\n                    scheduler.add_job(\n                        mode.update,\n                        name=f'Interval container update for {socket}',\n                        trigger='interval', seconds=config.interval\n                    )\n        except ConnectionError:\n            ol.logger.error(\"Could not connect to socket %s. Check your config\", socket)\n\n    if config.run_once:\n        next_run = None\n    elif config.cron:\n        next_run = scheduler.get_jobs()[0].next_run_time\n    else:\n        now = datetime.now(timezone.utc).astimezone()\n        next_run = (now + timedelta(0, config.interval)).strftime(\"%Y-%m-%d %H:%M:%S\")\n\n    if not config.skip_startup_notifications:\n        notification_manager.send(kind='startup', next_run=next_run)\n\n    while scheduler.get_jobs():\n        sleep(1)\n\n    scheduler.shutdown()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "requirements.txt",
    "content": "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",
    "content": "from setuptools import setup, find_packages\nfrom pyouroboros import VERSION\n\n\ndef read(filename):\n    with open(filename) as f:\n        return f.read()\n\n\ndef get_requirements(filename=\"requirements.txt\"):\n    \"\"\"returns a list of all requirements\"\"\"\n    requirements = read(filename)\n    return list(filter(None, [req.strip() for req in requirements.split() if not req.startswith('#')]))\n\n\nsetup(\n    name='ouroboros-cli',\n    version=VERSION,\n    maintainer='circa10a',\n    maintainer_email='caleblemoine@gmail.com',\n    description='Automatically update running docker containers',\n    long_description=read('README.md'),\n    long_description_content_type='text/markdown',\n    url='https://github.com/pyouroboros/ouroboros',\n    license='MIT',\n    classifiers=['Programming Language :: Python',\n                 'Programming Language :: Python :: 3.6',\n                 'Programming Language :: Python :: 3.7'],\n    packages=find_packages(),\n    scripts=['ouroboros'],\n    install_requires=get_requirements(),\n    python_requires='>=3.6.2'\n)\n"
  }
]