├── etcd.egg-info ├── not-zip-safe ├── top_level.txt ├── dependency_links.txt └── SOURCES.txt ├── etcd ├── __init__.py ├── resources │ └── requirements.txt ├── modules │ ├── __init__.py │ ├── leader.py │ └── lock.py ├── compat.py ├── config.py ├── log.py ├── exceptions.py ├── server_ops.py ├── inorder_ops.py ├── stat_ops.py ├── common_ops.py ├── directory_ops.py ├── response.py ├── node_ops.py ├── docs │ └── conf.py └── client.py ├── tests ├── ssl │ ├── demoCA │ │ ├── serial │ │ ├── serial.old │ │ ├── index.txt.attr │ │ ├── index.txt.attr.old │ │ ├── index.txt.old │ │ ├── index.txt │ │ ├── careq.pem │ │ ├── private │ │ │ └── cakey.pem │ │ ├── cacert.pem │ │ └── newcerts │ │ │ └── FC4EE3437CF69DDA.pem │ ├── build_ssl_ca.sh │ ├── build_ssl.sh │ ├── build_ssl_client.sh │ ├── cert_2_newca │ │ ├── build_2.sh │ │ ├── alien_client.csr │ │ ├── alien_client.crt │ │ └── alien_client.key │ ├── etcd.local.csr │ ├── client.csr │ ├── client.key │ ├── etcd.local.key │ ├── client.crt │ ├── etcd.local.crt │ ├── FC4EE3437CF69DDB.pem │ └── FC4EE3437CF69DDC.pem ├── test.sh ├── wait.sh ├── ssl.sh ├── wait.py └── test.py ├── .gitignore ├── dev ├── start_etcd.sh ├── start_etcd_ssl_example.sh ├── connect_etcd_ssl.sh ├── start_etcd_ssl.sh ├── ssl │ ├── ca.public.pem │ ├── client.public.pem │ ├── server.public.pem │ ├── client.etcd.public.pem │ ├── server.csr.pem │ ├── client.csr.pem │ ├── client.etcd.csr.pem │ ├── ca.csr.pem │ ├── server.crt.pem │ ├── ca.crt.pem │ ├── client.crt.pem │ ├── client.key.pem │ ├── server.key.pem │ ├── client.etcd.key.pem │ └── ca.key.pem └── test.py ├── docs ├── etcd.client.rst ├── etcd.config.rst ├── etcd.node_ops.rst ├── etcd.response.rst ├── etcd.common_ops.rst ├── etcd.exceptions.rst ├── etcd.server_ops.rst ├── etcd.inorder_ops.rst ├── etcd.modules.lock.rst ├── etcd.directory_ops.rst ├── etcd.modules.leader.rst ├── etcd.modules.rst ├── etcd.rst ├── index.rst ├── Makefile ├── make.bat └── conf.py ├── setup.py └── README.md /etcd.egg-info/not-zip-safe: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /etcd.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | etcd 2 | -------------------------------------------------------------------------------- /etcd.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /etcd/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '2.0.8' 2 | -------------------------------------------------------------------------------- /tests/ssl/demoCA/serial: -------------------------------------------------------------------------------- 1 | FC4EE3437CF69DDD 2 | -------------------------------------------------------------------------------- /tests/ssl/demoCA/serial.old: -------------------------------------------------------------------------------- 1 | FC4EE3437CF69DDC 2 | -------------------------------------------------------------------------------- /tests/ssl/demoCA/index.txt.attr: -------------------------------------------------------------------------------- 1 | unique_subject = yes 2 | -------------------------------------------------------------------------------- /tests/ssl/demoCA/index.txt.attr.old: -------------------------------------------------------------------------------- 1 | unique_subject = yes 2 | -------------------------------------------------------------------------------- /etcd/resources/requirements.txt: -------------------------------------------------------------------------------- 1 | pytz==2013.8 2 | requests==2.1.0 3 | -------------------------------------------------------------------------------- /tests/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PYTHONPATH=.. python test.py 4 | -------------------------------------------------------------------------------- /tests/wait.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PYTHONPATH=.. python wait.py 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /include 3 | /lib 4 | /.Python 5 | /dist 6 | /local 7 | *.pyc 8 | /etcd.egg-info 9 | /build 10 | -------------------------------------------------------------------------------- /dev/start_etcd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | etcd -f -data-dir=data -addr=server.etcd:4001 -peer-addr=server.etcd:7001 -name machine0 4 | -------------------------------------------------------------------------------- /etcd/modules/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. module:: Modules Package 3 | .. moduleauthor:: Dustin Oprea 4 | """ 5 | -------------------------------------------------------------------------------- /tests/ssl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PEC_DEBUG=1 PEC_SCHEME=https PEC_HOSTNAME=etcd.local PEC_SSL_CA_BUNDLE_FILEPATH=ssl/demoCA/cacert.pem ./test.sh 4 | 5 | -------------------------------------------------------------------------------- /docs/etcd.client.rst: -------------------------------------------------------------------------------- 1 | etcd.client module 2 | ================== 3 | 4 | .. automodule:: etcd.client 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/etcd.config.rst: -------------------------------------------------------------------------------- 1 | etcd.config module 2 | ================== 3 | 4 | .. automodule:: etcd.config 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /dev/start_etcd_ssl_example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | etcd -f -name machine0 -data-dir machine0 -ca-file=ssl/ca.crt.pem -cert-file=ssl/server.crt.pem -key-file=ssl/server.key.pem 4 | -------------------------------------------------------------------------------- /etcd/compat.py: -------------------------------------------------------------------------------- 1 | try: 2 | from urlparse import parse_qsl 3 | from urllib import urlencode 4 | except ImportError: 5 | from urllib.parse import parse_qsl, urlencode 6 | -------------------------------------------------------------------------------- /docs/etcd.node_ops.rst: -------------------------------------------------------------------------------- 1 | etcd.node_ops module 2 | ==================== 3 | 4 | .. automodule:: etcd.node_ops 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/etcd.response.rst: -------------------------------------------------------------------------------- 1 | etcd.response module 2 | ==================== 3 | 4 | .. automodule:: etcd.response 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /tests/ssl/build_ssl_ca.sh: -------------------------------------------------------------------------------- 1 | @!/bin/sh 2 | 3 | # Create CA. 4 | openssl genrsa -out rootCA.key 2048 5 | openssl req -x509 -new -nodes -key rootCA.key -days 1024 -out rootCA.pem 6 | 7 | -------------------------------------------------------------------------------- /docs/etcd.common_ops.rst: -------------------------------------------------------------------------------- 1 | etcd.common_ops module 2 | ====================== 3 | 4 | .. automodule:: etcd.common_ops 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/etcd.exceptions.rst: -------------------------------------------------------------------------------- 1 | etcd.exceptions module 2 | ====================== 3 | 4 | .. automodule:: etcd.exceptions 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/etcd.server_ops.rst: -------------------------------------------------------------------------------- 1 | etcd.server_ops module 2 | ====================== 3 | 4 | .. automodule:: etcd.server_ops 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/etcd.inorder_ops.rst: -------------------------------------------------------------------------------- 1 | etcd.inorder_ops module 2 | ======================= 3 | 4 | .. automodule:: etcd.inorder_ops 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/etcd.modules.lock.rst: -------------------------------------------------------------------------------- 1 | etcd.modules.lock module 2 | ======================== 3 | 4 | .. automodule:: etcd.modules.lock 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /dev/connect_etcd_ssl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | curl --cacert ssl/ca.crt.pem --key ssl/client.key.pem --cert ssl/client.crt.pem https://server.etcd:4001/v2/keys/_etcd/machines -w "%{http_code}" -k && echo 4 | -------------------------------------------------------------------------------- /docs/etcd.directory_ops.rst: -------------------------------------------------------------------------------- 1 | etcd.directory_ops module 2 | ========================= 3 | 4 | .. automodule:: etcd.directory_ops 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/etcd.modules.leader.rst: -------------------------------------------------------------------------------- 1 | etcd.modules.leader module 2 | ========================== 3 | 4 | .. automodule:: etcd.modules.leader 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /dev/start_etcd_ssl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | etcd -f -ca-file=ssl/ca.crt.pem -cert-file=ssl/server.crt.pem -key-file=ssl/server.key.pem -data-dir=data -addr=server.etcd:4001 -peer-addr=server.etcd:7001 -name machine0 4 | -------------------------------------------------------------------------------- /etcd/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | HOST_FAIL_WAIT_S = 5 4 | "Number of seconds that must elapse before we're allowed to retry a host." 5 | 6 | ATOMIC_MAX_ATTEMPTS = int(os.environ.get('ETCD_ATOMIC_MAX_ATTEMPTS', '5')) 7 | -------------------------------------------------------------------------------- /tests/ssl/demoCA/index.txt.old: -------------------------------------------------------------------------------- 1 | V 170115053440Z FC4EE3437CF69DDA unknown /C=US/ST=Florida/O=Internet Widgits Pty Ltd/CN=ca.local 2 | V 150116053623Z FC4EE3437CF69DDB unknown /C=US/ST=Florida/O=Internet Widgits Pty Ltd/CN=etcd.local 3 | -------------------------------------------------------------------------------- /tests/ssl/build_ssl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Create server certs. 4 | openssl genrsa -out etcd.local.key 2048 5 | openssl req -new -key etcd.local.key -out etcd.local.csr 6 | 7 | # Sign server certs. 8 | openssl x509 -req -in etcd.local.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out etcd.local.crt -days 500 9 | -------------------------------------------------------------------------------- /tests/ssl/build_ssl_client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Create server certs. 4 | openssl genrsa -out client.key 2048 5 | openssl req -new -key client.key -out client.csr 6 | 7 | # Sign server certs. 8 | openssl x509 -extensions ssl_client -req -in client.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out client.crt -days 500 9 | -------------------------------------------------------------------------------- /tests/ssl/demoCA/index.txt: -------------------------------------------------------------------------------- 1 | V 170115053440Z FC4EE3437CF69DDA unknown /C=US/ST=Florida/O=Internet Widgits Pty Ltd/CN=ca.local 2 | V 150116053623Z FC4EE3437CF69DDB unknown /C=US/ST=Florida/O=Internet Widgits Pty Ltd/CN=etcd.local 3 | V 150116053653Z FC4EE3437CF69DDC unknown /C=US/ST=Florida/O=Internet Widgits Pty Ltd/CN=client.local 4 | -------------------------------------------------------------------------------- /docs/etcd.modules.rst: -------------------------------------------------------------------------------- 1 | etcd.modules package 2 | ==================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | 9 | etcd.modules.leader 10 | etcd.modules.lock 11 | 12 | Module contents 13 | --------------- 14 | 15 | .. automodule:: etcd.modules 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /tests/ssl/cert_2_newca/build_2.sh: -------------------------------------------------------------------------------- 1 | openssl genrsa -des3 -passout pass:x -out alien_client.pass.key 2048 2 | openssl rsa -passin pass:x -in alien_client.pass.key -out alien_client.key 3 | rm alien_client.pass.key 4 | openssl req -new -key alien_client.key -out alien_client.csr 5 | openssl x509 -req -days 365 -in alien_client.csr -signkey alien_client.key -out alien_client.crt 6 | 7 | -------------------------------------------------------------------------------- /dev/ssl/ca.public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1xVnV3s97fZwY+gwUYGv 3 | 9ymPgcoObI5DIVysJofFNgmw6De3VMn4Z9RD78F4/u9BJrWOi9w1tZ6EjTq4byb7 4 | 68+FdrihQ0Ks7dsxlIp8M0tKnefXjgi9r0QdFp4cFULSzd/0k1mHhZAuMxfN6+Kp 5 | /2y0ktcAIhrXId0+SNbryxaV1DyI6jwMgV58bhFcbWfcKCG8lQtHh+n8vZBGp3Iz 6 | /HHAwWiEOzCTq8dnH9KaxFKJoRtUf+IpgQ10dFwEf19kO7tnHar7XXCsvue9TMve 7 | itXV3HLH+6Fjza3itgj2rNemR4beUK1nRBVGgM9m2iov0vv2uIxAmzvFNyFhqjTN 8 | 6wIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /dev/ssl/client.public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu7+sfPCbPBH77syuBtPa 3 | USPXaNcxw3bzi9id4z4ObVME6wsABYnelhfuxBDZoXv9iiGLl8tJQqRgumNBH/uM 4 | 3MWO73q+zciYkpPulWglJIpNq7w/n6olQPthoSv3LFnBdm08HDyU6LjCr57iL6QD 5 | sXrsREPzBjY6YyMp66Jh5OpcoqykhiIYsXd1hLscrJ8aP1CZAHyywmje2F1cnCmN 6 | ITZR/zMZrzV1h7H9qqQlVUHT5oiR65s8vKcHt+ZHD2A8lX2WSVHV3yktUCkgpNIY 7 | /3Kl9gtLE0ktL/SJq4xExBjk8LmME4NjhtR5j6Dv6FXdpwgNIHgkQxk2UqUX3gVr 8 | mwIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /dev/ssl/server.public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA95RtcYHgnNqg4/HwAX3q 3 | gTzqr+WhpTy8RlNc4b4e2hxBe6KohTP+UbRq59rybTkP0cDQOgFGXDWzHdDeIl6F 4 | GFG6C6dG/ADEMEl+uC71ucmuyyQBEmx6g8gDtjDiW4TyuLJwdcQ0HvpF45pDMkPA 5 | +mKFFwZIA0aM2IJhj9PZWsjvOAD91tYunrWUJYzVnQNKBSivCsKlI67NQTDb7ZPj 6 | 3SgXh4iZWnl2SyEz+/y5BDcKB62p9ypSMpjRLywM6o5mDF8Zcf39+8XeZe746Xb+ 7 | /WgonAKZl5DpnhzyvBcmqTYmS7Zs2G9gf95aHrFakbiOEwt+9oU0RKbPGjYDk4zs 8 | fwIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /dev/ssl/client.etcd.public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvKzAY6ZutFPqdSQRtWYh 3 | wU3HRFCDs7TMOr5KXcgfFw0kciL3OvfW/Nl3rfDumbvYE5wIdStmAMa+8uHTCdBG 4 | SNR/y0qmRWXY9VLZNYaq1scgT9ESO0lZSKa3oCq0v1e1nmlHIyuUyiG4h2SN+p4Q 5 | hkswWQilz6tm6c1ANmIi7e/IhQGQ0u7YNZg01OAKVYYIvGoRrNb+pObLYSwayN+V 6 | ne8T3ySzr6UtmyCI/BhdVROsRTmX7MJBz20CLEJ3mv9dspEYllpskUVkud26JyBO 7 | T0BtPNlD3XP4AMtJvPb5rIjRmkfJKhp0Mz4bXioaWU6hTCN+q5aGaBozBbxDXXro 8 | 4wIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /docs/etcd.rst: -------------------------------------------------------------------------------- 1 | etcd package 2 | ============ 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | etcd.modules 10 | 11 | Submodules 12 | ---------- 13 | 14 | .. toctree:: 15 | 16 | etcd.client 17 | etcd.common_ops 18 | etcd.config 19 | etcd.directory_ops 20 | etcd.exceptions 21 | etcd.inorder_ops 22 | etcd.node_ops 23 | etcd.response 24 | etcd.server_ops 25 | 26 | Module contents 27 | --------------- 28 | 29 | .. automodule:: etcd 30 | :members: 31 | :undoc-members: 32 | :show-inheritance: 33 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Python etcd Client documentation master file, created by 2 | sphinx-quickstart on Mon Jan 6 09:05:18 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Python etcd Client's documentation! 7 | ============================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 4 13 | 14 | etcd 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | 24 | -------------------------------------------------------------------------------- /etcd.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | setup.py 2 | etcd/__init__.py 3 | etcd/client.py 4 | etcd/common_ops.py 5 | etcd/compat.py 6 | etcd/config.py 7 | etcd/directory_ops.py 8 | etcd/exceptions.py 9 | etcd/inorder_ops.py 10 | etcd/log.py 11 | etcd/node_ops.py 12 | etcd/response.py 13 | etcd/server_ops.py 14 | etcd/stat_ops.py 15 | etcd.egg-info/PKG-INFO 16 | etcd.egg-info/SOURCES.txt 17 | etcd.egg-info/dependency_links.txt 18 | etcd.egg-info/not-zip-safe 19 | etcd.egg-info/requires.txt 20 | etcd.egg-info/top_level.txt 21 | etcd/modules/__init__.py 22 | etcd/modules/leader.py 23 | etcd/modules/lock.py -------------------------------------------------------------------------------- /dev/ssl/server.csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBgzCCAXcCADBLMQswCQYDVQQGEwJVUzEUMBIGA1UEAxMLc2VydmVyLmV0Y2Qx 3 | FDASBgNVBAoTC1NvbWUgRW50aXR5MRAwDgYDVQQIEwdGbG9yaWRhMIIBIjANBgkq 4 | hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA95RtcYHgnNqg4/HwAX3qgTzqr+WhpTy8 5 | RlNc4b4e2hxBe6KohTP+UbRq59rybTkP0cDQOgFGXDWzHdDeIl6FGFG6C6dG/ADE 6 | MEl+uC71ucmuyyQBEmx6g8gDtjDiW4TyuLJwdcQ0HvpF45pDMkPA+mKFFwZIA0aM 7 | 2IJhj9PZWsjvOAD91tYunrWUJYzVnQNKBSivCsKlI67NQTDb7ZPj3SgXh4iZWnl2 8 | SyEz+/y5BDcKB62p9ypSMpjRLywM6o5mDF8Zcf39+8XeZe746Xb+/WgonAKZl5Dp 9 | nhzyvBcmqTYmS7Zs2G9gf95aHrFakbiOEwt+9oU0RKbPGjYDk4zsfwIDAQABoAAw 10 | AwYBAAMBAA== 11 | -----END CERTIFICATE REQUEST----- 12 | -------------------------------------------------------------------------------- /tests/ssl/demoCA/careq.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBlDCB/gIBADBVMQswCQYDVQQGEwJVUzEQMA4GA1UECBMHRmxvcmlkYTEhMB8G 3 | A1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMREwDwYDVQQDEwhjYS5sb2Nh 4 | bDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzZcVnI+Laf4y835fduPvt9EX 5 | CE58OVnyjJj8yHnzSeCf6Pt0h7Fo1BOmPHdkk1vjP0iZ0gGUUJQhno057Llootih 6 | Shqxz8pnNw3/Z4SpQDlDTiQXl9E6JRf/Mj4eGr8A9ysc7yybNZbuyNaCVHf2iA+e 7 | Yo+sfZqBh3yMmgeN8FsCAwEAAaAAMA0GCSqGSIb3DQEBBQUAA4GBAJ4w/N4yFfGw 8 | VfqjpDq9Vo9pmoHzo832j1hS6n5ECyz1iW4EWMa7U8/Q75FcxXVdeFXaHBRkEvb/ 9 | 7hmUQ+0FYwk7b0qkoHXza3fh8Jvf+Fh0rneYEM4ieto/MZ+f+ScYN5f2YwPLtCVL 10 | /JSDp18BjIdQXHrsvlp3Kg2ddeL6CFJl 11 | -----END CERTIFICATE REQUEST----- 12 | -------------------------------------------------------------------------------- /dev/ssl/client.csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBrDCCAaACADBLMQswCQYDVQQGEwJVUzEUMBIGA1UEAxMLY2xpZW50LmV0Y2Qx 3 | FDASBgNVBAoTC1NvbWUgRW50aXR5MRAwDgYDVQQIEwdGbG9yaWRhMIIBIjANBgkq 4 | hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu7+sfPCbPBH77syuBtPaUSPXaNcxw3bz 5 | i9id4z4ObVME6wsABYnelhfuxBDZoXv9iiGLl8tJQqRgumNBH/uM3MWO73q+zciY 6 | kpPulWglJIpNq7w/n6olQPthoSv3LFnBdm08HDyU6LjCr57iL6QDsXrsREPzBjY6 7 | YyMp66Jh5OpcoqykhiIYsXd1hLscrJ8aP1CZAHyywmje2F1cnCmNITZR/zMZrzV1 8 | h7H9qqQlVUHT5oiR65s8vKcHt+ZHD2A8lX2WSVHV3yktUCkgpNIY/3Kl9gtLE0kt 9 | L/SJq4xExBjk8LmME4NjhtR5j6Dv6FXdpwgNIHgkQxk2UqUX3gVrmwIDAQABoCkw 10 | JwYJKoZIhvcNAQkOMRowGDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAjADBgEAAwEA 11 | -----END CERTIFICATE REQUEST----- 12 | -------------------------------------------------------------------------------- /dev/ssl/client.etcd.csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBsjCCAaYCADBRMQswCQYDVQQGEwJVUzEaMBgGA1UEAxMRY2xpZW50LmV0Y2Qu 3 | bG9jYWwxFDASBgNVBAoTC1NvbWUgRW50aXR5MRAwDgYDVQQIEwdGbG9yaWRhMIIB 4 | IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvKzAY6ZutFPqdSQRtWYhwU3H 5 | RFCDs7TMOr5KXcgfFw0kciL3OvfW/Nl3rfDumbvYE5wIdStmAMa+8uHTCdBGSNR/ 6 | y0qmRWXY9VLZNYaq1scgT9ESO0lZSKa3oCq0v1e1nmlHIyuUyiG4h2SN+p4Qhksw 7 | WQilz6tm6c1ANmIi7e/IhQGQ0u7YNZg01OAKVYYIvGoRrNb+pObLYSwayN+Vne8T 8 | 3ySzr6UtmyCI/BhdVROsRTmX7MJBz20CLEJ3mv9dspEYllpskUVkud26JyBOT0Bt 9 | PNlD3XP4AMtJvPb5rIjRmkfJKhp0Mz4bXioaWU6hTCN+q5aGaBozBbxDXXro4wID 10 | AQABoCkwJwYJKoZIhvcNAQkOMRowGDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAjAD 11 | BgEAAwEA 12 | -----END CERTIFICATE REQUEST----- 13 | -------------------------------------------------------------------------------- /etcd/log.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import os.path 4 | import logging 5 | import logging.handlers 6 | 7 | logger = logging.getLogger() 8 | 9 | is_debug = bool(int(os.environ.get('DEBUG', '0'))) 10 | 11 | if is_debug is True: 12 | logger.setLevel(logging.DEBUG) 13 | else: 14 | logger.setLevel(logging.INFO) 15 | 16 | format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 17 | formatter = logging.Formatter(format) 18 | 19 | ch = logging.StreamHandler() 20 | ch.setFormatter(formatter) 21 | logger.addHandler(ch) 22 | 23 | # TODO(dustin): This all does not work for a Linux system. Just omit the "address" parameter completely. 24 | if sys.platform == 'darwin': 25 | address = '/var/run/syslog' 26 | elif os.path.exists('/dev/log'): 27 | address = '/dev/log' 28 | else: 29 | address = ('localhost', 514) 30 | 31 | ch = logging.handlers.SysLogHandler( 32 | address, 33 | facility=logging.handlers.SysLogHandler.LOG_LOCAL0) 34 | 35 | ch.setFormatter(formatter) 36 | logger.addHandler(ch) 37 | -------------------------------------------------------------------------------- /dev/ssl/ca.csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIClDCCAXwCADBQMQswCQYDVQQGEwJVUzEQMA4GA1UECBMHRmxvcmlkYTEWMBQG 3 | A1UEBxMNQm95bnRvbiBCZWFjaDEXMBUGA1UEAxMOc2VydmVyLmNhLmV0Y2QwggEi 4 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDXFWdXez3t9nBj6DBRga/3KY+B 5 | yg5sjkMhXKwmh8U2CbDoN7dUyfhn1EPvwXj+70EmtY6L3DW1noSNOrhvJvvrz4V2 6 | uKFDQqzt2zGUinwzS0qd59eOCL2vRB0WnhwVQtLN3/STWYeFkC4zF83r4qn/bLSS 7 | 1wAiGtch3T5I1uvLFpXUPIjqPAyBXnxuEVxtZ9woIbyVC0eH6fy9kEancjP8ccDB 8 | aIQ7MJOrx2cf0prEUomhG1R/4imBDXR0XAR/X2Q7u2cdqvtdcKy+571My96K1dXc 9 | csf7oWPNreK2CPas16ZHht5QrWdEFUaAz2baKi/S+/a4jECbO8U3IWGqNM3rAgMB 10 | AAGgADANBgkqhkiG9w0BAQUFAAOCAQEAEDdXYjUxkwioQwv9D7nq0oc8pPE+SEkF 11 | HqUVECEO2bX+H/St4rw/rQzTSVuTdIstthmuQ2UcWsp4NX2u2IjOQhqGktvDim2D 12 | f4/OSgWJ05yTgNxd0mGdnRvphkv4h/ZdNqgE6o9Y1e1FZ4VFPoT6AG3BESXrTrIx 13 | if/V/ZWrfniTxWikwDSscnJK+M8CQNQVt1TH6MTO/EuF6OSWeY55tlE/4//VP/Jd 14 | gFXElIUZ7r771UiEn8MXz3DIJaH4SlOS4hQrzknJvxp4stuV9yUVy9uGN0pMAOYf 15 | 5YWn7CGGfoUH91b7f1t+qcOBWBv7YZD3JBpF85/enLqlLgkgE+NdIg== 16 | -----END CERTIFICATE REQUEST----- 17 | -------------------------------------------------------------------------------- /tests/ssl/cert_2_newca/alien_client.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIChzCCAW8CAQAwQjELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0Zsb3JpZGExITAf 3 | BgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEB 4 | BQADggEPADCCAQoCggEBAMw+EhZjML2WYSTZnTCEMuvu5dxWgGJeRg+Gr8WriplF 5 | /FHcJS23W5pc84om6PUJukqrVn3ncbFpouSmw98vathocbYFgJJ5FFMbA2Emp9Ax 6 | Mykd0xu2DVF6PM309p+OFgmeEi2MYiilIehGviRsZ/8zW2Lu9a0Ms5W2rS++7YLI 7 | M8c9Qyecrt9cUJzrE5ikov2QJTqi3prPEl6dVU9UvImbsJC1/Cglyr0KvNXAqStu 8 | VRMKX4KngqgcdZWuuD78lEsrnDXkIK9HG/k7FpxnfBk2ivWQrggrcDyWpUqM9+gS 9 | ohsAuz7bGFVADpx9oxVoT3XZh3RFYrWD3Ik7XXtVMakCAwEAAaAAMA0GCSqGSIb3 10 | DQEBBQUAA4IBAQCaSPTt+F7TapouxSAtQaizKCElRaFlmB6e+IV7d3kEIk8rxLKh 11 | +ZVm747CtiIkuGu7yKXmCd6JZe6QD3+Wsk0KNT6SyDyB16ek0zNE55W2l7mliIai 12 | fx97Nxitjt1hjI6HyoW+SGbyXrxdrm6tvS+M56J7Usy0I30wh6uqm1P5KZbzlsr0 13 | F7778r2LjWeGU11cJWmuX6guNH7Emogxk1YlRupGyrm4H8kG0DECI1HxqUQ+WQBn 14 | tVFiZ53qD7pWHffR0ttVyCxAQngWDtuOBrR8mMjnpKIpxlm1AEXjkN+c+ZPEyd1p 15 | UpTCwOSOHdzxQF4QJuCF3dyIVYcDRuln78mu 16 | -----END CERTIFICATE REQUEST----- 17 | -------------------------------------------------------------------------------- /tests/ssl/demoCA/private/cakey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,8CD101652D8C2295 4 | 5 | YwP9hRPZsAa0rL+6h+ns/siO0q8md12NGBH8cR3B71CcLMXnNHNZjjITLbuHtGBp 6 | RefIGl7le/BMUzN5VmwuuaHH2rYSrgp9Ty6CDhbkTMKoce69WU6NZkMwfcnoFOgp 7 | wSm3Mde0HRMznKOc8m7pZ7n3zL5VSuxv5e1A2Fy5uxzpjC72/JJW72sVuI6EQTVh 8 | NTttQhYUi+CdBfvs9K5MaFyA6ztHXHEvfEw+boFYcHfTEyaUf2nLjluYv6Emijzh 9 | e4TdraC9cTH/0D/YpEp/6nczTzppzw9wu8h8I2A+JNOE3ptBYSy/BGS+UdEVfJnG 10 | dRvknkGFkJ8kqxC0BVkXNElzgemfR5G6ydh3aMNZOs+dnDHFNt6uvO+R9DxpclVV 11 | nip4/xd2+sgdbZHQdl5n+gt1fbfjIhQKEwAiXbRpdEP8lQYfk/8+t7sPCnA2rJWa 12 | q7UGiwmDZezK0kYr9AX3p/VwNQKS+aLzpcH+g9sHofh/1IXbWRvyPcdVvkbUmlWX 13 | +K0BdESQoBNJNh8Koh2wolpGvu9Z8ABHJ6EeB0vL9yCS6UbKey6NCSxAQsalSCas 14 | ldtpT+LMhS0ZdSx4J6t+EG93U+Fp1WEGDOiKcKlYjAj5b5SCNhB6beTA2B6OaS09 15 | mOeRF11qVaeAYCQ7klW5e0EFvvSBcFrm78JVvMqsgQHHykmNu3ytZdH3FFBefV/e 16 | +GCykRMx1FWh0h8gbJbGqIrqszkjnIx38NA872Fu9TG44gx3s+aorg0YUJqTVNUH 17 | upuhTAbIk/qElydjH3KgmKno1vMl7cXapiU3GX8zfY4OokETVjwreg== 18 | -----END RSA PRIVATE KEY----- 19 | -------------------------------------------------------------------------------- /tests/ssl/etcd.local.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICnDCCAYQCAQAwVzELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0Zsb3JpZGExITAf 3 | BgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDETMBEGA1UEAxMKZXRjZC5s 4 | b2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL60hFzMzdpJ8mHk 5 | 61zFk5PdW+e5Vm0YmP5KRueoEjO7oX0R0iOQoPi/7MALFW3R1+UbW8/RYFE8MADI 6 | ng2I0+cHEWPe95sySMnoU0KPJykR+nHjhFvf8wpaZ0L2RIROmwpBL3q9wBk0nVAo 7 | pxVhzM1a4pnufoelNvCYVuRPCds8KfzdsOmyAl5O+X3n/NxLy5spOC4zcSJ8SxnM 8 | 7+y+UuIiak/qJKwTmkxPEUXtBeLqnmwMXm5fO2L5MYb1V025rgNyblyBWIbJs3Qn 9 | S+NPf1sNxKLSZT1Zr/jGXbCnMt4Y3F7Fne99WgjIMb217oTbSpGr/YKzW5ZBVG17 10 | RwnJm8kCAwEAAaAAMA0GCSqGSIb3DQEBBQUAA4IBAQCCLBrOPw4Uol4TXJVfELeD 11 | bFSNXyl4sTdBbUpLxgIxIZd9aorcRVUCEc5iI1dUvx+a87p2AYYJKF6RdL98yBE4 12 | 6gQS0wNSgXC/pfwackmX4RBqhkGZzQMczWUQPS6t5rkU0G9Vrrqfl9hJVY9vLbje 13 | 1d/hLFanoKP1JTx3jD0oLR68UVX7mzGfNfVbsZntTCerWWqH1tDMYEHO1kiQ8M2q 14 | LCsZFaCgRIhJxU4QCeLfn8N4cQ9al6bj+DQqvzHiyzGVJC7e6sUJBtFUau1Iddkc 15 | Edaaq/BO/MMbvMk4oePkQuT7dbBGCWg3ppQ3+PODL0eA+cRE2PwOLVR8LBZNX0v7 16 | -----END CERTIFICATE REQUEST----- 17 | -------------------------------------------------------------------------------- /tests/ssl/client.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICnjCCAYYCAQAwWTELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0Zsb3JpZGExITAf 3 | BgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEVMBMGA1UEAxMMY2xpZW50 4 | LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7tmoSrzLVOYg 5 | n4DlH1P4+RJWts233ADBQKAM1RB0MUkn7SCXQReZU9Ha78Ml8vfb1XfSAIjTZIqt 6 | zvn6rcqN2NOb4lKlWx/w3BNt+qyJH0L3KR8zKN3Azs0OknmaDMzTd96IVqWJLFuJ 7 | vx2YYAT22Unc5Net4KAho2RhVUzpbdvpqoIxjgyPx/z6Gdcwz3xKTU2GFI+rjaIO 8 | nGzpLShyN5/n2MCkRTAWQdhgyqOe+9tEyWhjWJI5VFKCmVVZIBpHg4YhDSflCeD0 9 | rG7QHQMJ8wfqsQCwLLMs4IZmMUXrGLQ+zjazEkFIvkNhmOR5QPT1qARWSUcHIq33 10 | uAbMJ/XsZwIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBALmkzTi6dtsxvzG/3lYm 11 | g3JxzXxe8FrMt7gbt4LpfNq0ttfhlHL2POruGInLWIfZC86moGbOmBmjwkFU2LK0 12 | H1ZITyVRT1fUpG11Rl7szhj+PHXZE60HUYxDslFFbL5wMUsCcEJcCZvEy56YXmav 13 | avSD8rYa0dho6f5DtYNg4NO4u9l8AVdzTrLB31FvE0qfzzLzCQYwt/xBZ6cpqg5j 14 | /rLYCXDHLAUsT8yWKSNnYAxk4ka/hGa5Urky5tX/u3pAzVR0Ud7txiMLzo/QKGwJ 15 | 5gSmtBQdfrU67CRCP9iYHsSH30ze4p4n1+pomiDi6zOEt2hJNltrdt2zLMXsdPOT 16 | WBw= 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /tests/ssl/cert_2_newca/alien_client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDADCCAegCCQCQdWR+/PO6YjANBgkqhkiG9w0BAQUFADBCMQswCQYDVQQGEwJV 3 | UzEQMA4GA1UECBMHRmxvcmlkYTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ 4 | dHkgTHRkMB4XDTE0MDEwOTIzNDMzMVoXDTE1MDEwOTIzNDMzMVowQjELMAkGA1UE 5 | BhMCVVMxEDAOBgNVBAgTB0Zsb3JpZGExITAfBgNVBAoTGEludGVybmV0IFdpZGdp 6 | dHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMw+EhZj 7 | ML2WYSTZnTCEMuvu5dxWgGJeRg+Gr8WriplF/FHcJS23W5pc84om6PUJukqrVn3n 8 | cbFpouSmw98vathocbYFgJJ5FFMbA2Emp9AxMykd0xu2DVF6PM309p+OFgmeEi2M 9 | YiilIehGviRsZ/8zW2Lu9a0Ms5W2rS++7YLIM8c9Qyecrt9cUJzrE5ikov2QJTqi 10 | 3prPEl6dVU9UvImbsJC1/Cglyr0KvNXAqStuVRMKX4KngqgcdZWuuD78lEsrnDXk 11 | IK9HG/k7FpxnfBk2ivWQrggrcDyWpUqM9+gSohsAuz7bGFVADpx9oxVoT3XZh3RF 12 | YrWD3Ik7XXtVMakCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAqRXf2btqn7gVF7Pa 13 | ftEV5uimUAAUgvxn3ZDe6W9XETo3ei8d5Sl6OUagtJyUy/RmsDF94560ISt02bVD 14 | qfYNQxvORr7DlZl1rVphRHp5Ofz9jawHFlU8Nd8BA96fQgw+imVTGLAqaGy/ld/E 15 | K6h7jKUJk0KnOmU7QFQA5xd7H6/cx6Co379QMA900oF8FCwcLTGrfByhiXIw05Py 16 | hwb963b3zynWlhZYBL+eYHNbzJ2gBvZP2Y6SA7wJ94uySNMd10N1oEmMYnI1IGJ7 17 | nFIUr4A+xDbScp3L13LmpM/zlcUUFG8h4y5zIJTKeKSEd680BAqOS/a8QlQ7Ysd5 18 | dQFn9w== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /dev/ssl/server.crt.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDMTCCAhkCFET2JexyjAgRqu6FkSiOZ+D27l/JMA0GCSqGSIb3DQEBBQUAMFAx 3 | CzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdGbG9yaWRhMRYwFAYDVQQHEw1Cb3ludG9u 4 | IEJlYWNoMRcwFQYDVQQDEw5zZXJ2ZXIuY2EuZXRjZDAeFw0xNDA3MTAwNDM2NDda 5 | Fw0xNTA4MTQwNDM2NDdaMEsxCzAJBgNVBAYTAlVTMRQwEgYDVQQDEwtzZXJ2ZXIu 6 | ZXRjZDEUMBIGA1UEChMLU29tZSBFbnRpdHkxEDAOBgNVBAgTB0Zsb3JpZGEwggEi 7 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD3lG1xgeCc2qDj8fABfeqBPOqv 8 | 5aGlPLxGU1zhvh7aHEF7oqiFM/5RtGrn2vJtOQ/RwNA6AUZcNbMd0N4iXoUYUboL 9 | p0b8AMQwSX64LvW5ya7LJAESbHqDyAO2MOJbhPK4snB1xDQe+kXjmkMyQ8D6YoUX 10 | BkgDRozYgmGP09layO84AP3W1i6etZQljNWdA0oFKK8KwqUjrs1BMNvtk+PdKBeH 11 | iJlaeXZLITP7/LkENwoHran3KlIymNEvLAzqjmYMXxlx/f37xd5l7vjpdv79aCic 12 | ApmXkOmeHPK8FyapNiZLtmzYb2B/3loesVqRuI4TC372hTREps8aNgOTjOx/AgMB 13 | AAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQEFBQADggEBALhFjq8KyBIVirh/ 14 | 94ZzzHrT0byPKNOR7BX+sYfGZuWzJrES0UpNyPbvw7qEkttl7Ye//622x/BtSoh8 15 | ASsvT4wSFsfPWMGUydw70HjbGX5gcnvn1LhZLXuZ+6mv7PHfhPzQfY+1d444s4W6 16 | 4na5QyGvqBhEeJGSdUcMZ5B2ZpOzXnq3819+sG2nxoJkDic7i3T0EULg00Z7UYJD 17 | rxNnp9CYUgLOJyj2LcVrepw6u6U0+wVHGhrd4W2W80YaSgMxUtJv4vrHH3kXujbQ 18 | 0DE5UUV+NzYKt2IzxHzz/Z4G+TZYoJ2bdUrdGNuCXNmiJ2oN2yz4raCH2KAZ0dBs 19 | Iv9FLEA= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /dev/ssl/ca.crt.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDOjCCAiICFQCtnC1GqvABqDhRPD+OvNFgB7mh2zANBgkqhkiG9w0BAQUFADBQ 3 | MQswCQYDVQQGEwJVUzEQMA4GA1UECBMHRmxvcmlkYTEWMBQGA1UEBxMNQm95bnRv 4 | biBCZWFjaDEXMBUGA1UEAxMOc2VydmVyLmNhLmV0Y2QwHhcNMTQwNzEwMDQzNDA2 5 | WhcNMTcwNDA1MDQzNDA2WjBQMQswCQYDVQQGEwJVUzEQMA4GA1UECBMHRmxvcmlk 6 | YTEWMBQGA1UEBxMNQm95bnRvbiBCZWFjaDEXMBUGA1UEAxMOc2VydmVyLmNhLmV0 7 | Y2QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDXFWdXez3t9nBj6DBR 8 | ga/3KY+Byg5sjkMhXKwmh8U2CbDoN7dUyfhn1EPvwXj+70EmtY6L3DW1noSNOrhv 9 | Jvvrz4V2uKFDQqzt2zGUinwzS0qd59eOCL2vRB0WnhwVQtLN3/STWYeFkC4zF83r 10 | 4qn/bLSS1wAiGtch3T5I1uvLFpXUPIjqPAyBXnxuEVxtZ9woIbyVC0eH6fy9kEan 11 | cjP8ccDBaIQ7MJOrx2cf0prEUomhG1R/4imBDXR0XAR/X2Q7u2cdqvtdcKy+571M 12 | y96K1dXccsf7oWPNreK2CPas16ZHht5QrWdEFUaAz2baKi/S+/a4jECbO8U3IWGq 13 | NM3rAgMBAAGjEDAOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAD0o 14 | h29sU9YWNtq+mwOXVVAF5IfJqnAp3Tu1V566B3n8E/714qlXH5QdfRkUs4HSBWZW 15 | gdEcp1SjVoAI31oe8KkxxXcXfkPjBMXiKsHRKw/lod/24c4nqn+vez/WPcXeUfOo 16 | I4mGVQEiJRssd9pHO6lm40q1jIaUU5nEbv+8ZiPjAelHR6vHltFzi/J+Q27lj/jn 17 | OEmyTAQIqF6z9TSP0w6IgqbqqpeVdrwXwnEnw4tdOFmIroyuMtdGlBmp8NEI95h4 18 | IwqWpnhI1cUnduW3Ymkj/TDGBWWM7fUGEDjd5BhPi/tee/g50ie5A4BlpakyGky1 19 | vJSrXFLpd1w2FaV4sts= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /dev/ssl/client.crt.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDSTCCAjECFBIaBTYiTiYGNdPKd1wxxiAjhSTJMA0GCSqGSIb3DQEBBQUAMFAx 3 | CzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdGbG9yaWRhMRYwFAYDVQQHEw1Cb3ludG9u 4 | IEJlYWNoMRcwFQYDVQQDEw5zZXJ2ZXIuY2EuZXRjZDAeFw0xNDA3MTAxNDE0MDFa 5 | Fw0xNTA4MTQxNDE0MDFaMEsxCzAJBgNVBAYTAlVTMRQwEgYDVQQDEwtjbGllbnQu 6 | ZXRjZDEUMBIGA1UEChMLU29tZSBFbnRpdHkxEDAOBgNVBAgTB0Zsb3JpZGEwggEi 7 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7v6x88Js8EfvuzK4G09pRI9do 8 | 1zHDdvOL2J3jPg5tUwTrCwAFid6WF+7EENmhe/2KIYuXy0lCpGC6Y0Ef+4zcxY7v 9 | er7NyJiSk+6VaCUkik2rvD+fqiVA+2GhK/csWcF2bTwcPJTouMKvnuIvpAOxeuxE 10 | Q/MGNjpjIynromHk6lyirKSGIhixd3WEuxysnxo/UJkAfLLCaN7YXVycKY0hNlH/ 11 | MxmvNXWHsf2qpCVVQdPmiJHrmzy8pwe35kcPYDyVfZZJUdXfKS1QKSCk0hj/cqX2 12 | C0sTSS0v9ImrjETEGOTwuYwTg2OG1HmPoO/oVd2nCA0geCRDGTZSpRfeBWubAgMB 13 | AAGjJTAjMAkGA1UdEwQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwIwDQYJKoZI 14 | hvcNAQEFBQADggEBADOdzjBcmg4WtUZhKqXnT2jv9IXg9QWAeokKbBm0HkLG0xmx 15 | 3jr0S1ljkvUvBLcQDs5Dn56MUYMZy2fsNsC6zrki3wq9m7IfZWGXeKlLRu4vpw8+ 16 | eS8BZJlhhtbgJjGSLHeAjkEboYndBZ0N1lkrIV7Ru6peMyAQHdnioFbq0YedfijO 17 | 8oHDbXIwvjDKrCjNyhmgRXmv843K3mnRv0D43VL+azPzZ2OKT1N1Fv8BpEt5OCf4 18 | CXW7mBNf+VN9Q3DXaMkqe6cCuWJLF+/OfM/2SR/j4nuVbDJU/I5uHUFI0x3/5J5u 19 | krAh6gf7DsIQALQBK8ZwojffG/2g7mEE03Xs++0= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | 3 | import setuptools 4 | import sys 5 | import os.path 6 | 7 | import etcd 8 | 9 | app_path = os.path.dirname(etcd.__file__) 10 | 11 | long_description = "A Python etcd client that just works." 12 | 13 | with open(os.path.join(app_path, 'resources', 'requirements.txt')) as f: 14 | install_requires = list(map(lambda s: s.strip(), f.readlines())) 15 | 16 | setuptools.setup( 17 | name='etcd', 18 | version=etcd.__version__, 19 | description="A Python etcd client that just works.", 20 | long_description=long_description, 21 | classifiers=[ 22 | 'Intended Audience :: Developers', 23 | 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 24 | 'Operating System :: POSIX :: Linux', 25 | 'Topic :: Database :: Front-Ends', 26 | 'Topic :: Software Development :: Libraries :: Python Modules', 27 | 'Topic :: System :: Distributed Computing', 28 | ], 29 | keywords='etcd kv', 30 | author='Dustin Oprea', 31 | author_email='myselfasunder@gmail.com', 32 | url='https://github.com/dsoprea/PythonEtcdClient', 33 | license='GPL 2', 34 | packages=setuptools.find_packages(exclude=[]), 35 | include_package_data=True, 36 | zip_safe=False, 37 | install_requires=install_requires, 38 | ) 39 | -------------------------------------------------------------------------------- /etcd/exceptions.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import requests.status_codes 3 | 4 | 5 | class EtcdException(Exception): 6 | """The base exception for the client.""" 7 | 8 | pass 9 | 10 | 11 | class EtcdError(EtcdException): 12 | """The base error for the client.""" 13 | 14 | pass 15 | 16 | 17 | class EtcdPreconditionException(EtcdException): 18 | """Raised when a CAS condition fails.""" 19 | 20 | pass 21 | 22 | 23 | class EtcdAlreadyExistsException(EtcdException): 24 | """Raised when a directory can't be created because it already exists.""" 25 | 26 | pass 27 | 28 | 29 | class EtcdEmptyResponseError(EtcdError): 30 | pass 31 | 32 | 33 | class EtcdWaitFaultException(EtcdException): 34 | pass 35 | 36 | 37 | class EtcdAtomicWriteError(EtcdError): 38 | pass 39 | 40 | 41 | def translate_exceptions(method): 42 | def op_wrapper(self, path, *args, **kwargs): 43 | try: 44 | return method(self, path, *args, **kwargs) 45 | except requests.HTTPError as e: 46 | # We're only concerned with generating KeyError's when appropriate. 47 | 48 | if e.response.status_code == \ 49 | requests.status_codes.codes.precondition_failed: 50 | r = EtcdPreconditionException() 51 | elif e.response.status_code == \ 52 | requests.status_codes.codes.not_found: 53 | try: 54 | j = e.response.json() 55 | except ValueError: 56 | raise 57 | 58 | if j['errorCode'] != 100: 59 | raise 60 | 61 | r = KeyError(path) 62 | else: 63 | raise 64 | 65 | raise r 66 | 67 | return op_wrapper 68 | -------------------------------------------------------------------------------- /dev/ssl/client.key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAu7+sfPCbPBH77syuBtPaUSPXaNcxw3bzi9id4z4ObVME6wsA 3 | BYnelhfuxBDZoXv9iiGLl8tJQqRgumNBH/uM3MWO73q+zciYkpPulWglJIpNq7w/ 4 | n6olQPthoSv3LFnBdm08HDyU6LjCr57iL6QDsXrsREPzBjY6YyMp66Jh5Opcoqyk 5 | hiIYsXd1hLscrJ8aP1CZAHyywmje2F1cnCmNITZR/zMZrzV1h7H9qqQlVUHT5oiR 6 | 65s8vKcHt+ZHD2A8lX2WSVHV3yktUCkgpNIY/3Kl9gtLE0ktL/SJq4xExBjk8LmM 7 | E4NjhtR5j6Dv6FXdpwgNIHgkQxk2UqUX3gVrmwIDAQABAoIBADPV0ZuVGmbVWEcv 8 | /4QnDYmFab2qXDTpLIiZv82znmwUlbEHEnR1Ah1Scjtzo4JwJynx5QZ9u7ZIh+Qt 9 | 7xcTbmqtqK75pqBOF8GwUhix7j9OZ+9YRACjCzILtYK4DsAR79Xwhr4NA9fU2vt9 10 | 1Fju4Y4or/6Q/BHk+u3QEYzbAkge6mHoRnxCikaF6ziNuJRbDj7ASroRrx2Cs6IN 11 | /OsD4Bi0sa2TJoJsk7VK1quJYvlFEXJdpNE8MBeNbguvdqz+mkMFbeaN1EBYrODd 12 | BJHTLzEvlV8wyYj5WE828qFM3R4YixE2vRvVJ3nGyIwogYP1jLwN0FbZKN2rFKZQ 13 | 5FujP4ECgYEA+kGaV7aCirlyNxxUfQp6MeAZ5e1GofYR0PxD6vXqYnxFM6x8EtIa 14 | i3tE7W8npmgtDcTQuZmQUxlYZwRNx6fdljvyriFI8yxT6Lmmiwx6Lyy2QS8OzsMB 15 | cT8uLae2oev09RDGBi8LD8cXbJNFrGGAwN0toBGQwtllSsTIF/8o17sCgYEAwA7N 16 | wovA2igfQIX8Yb9Z+9e/+VvfCV4ZS2wxAFXYPGDK097pyjOe+RNvlRR24saY2kl1 17 | pHCztjn+YlHk7AAf4Gt8sFsHdZWIbNUTVHG40KmGZU3RUqoNmDjioUgfHd1aDVXI 18 | BsaVw/XdyQ94W3zFpxR2uzRKc4LUvWHszsvvzaECgYBZlUpOw8KPLjs4hyenayy6 19 | 1eHvcLSsHY3bJbKaVVWF075h9QilJBjkfUf7RqMG6pvWJfIhvlrvpE8E8YF2Sx0D 20 | Pops0vVuNxwCXxLDC8BhSrwjMVk3oo7NnzREm5jA9dpGL67zX7ux9i7oav+x6WVR 21 | pNtERYVweqnLzg1iqbBRKQKBgQCGmhl07pNTkekNRJo0O3KqQXQnJE81doCNzp6t 22 | GHz7G+8q/ECn2PPips9+ESCdIQjBe9h6Tg51eUGi0Lh6+j9c/pgd1T5uF0XM2Ky6 23 | cxVPEHDDkP1STSJH/B5BYJWGTbsuO197+DPAahFtN21n2ROBXKM14pkHSbslm3rn 24 | GCDR4QKBgQDO6Zj03CX50OpSZV9Myk/cPUnd32d4BfNwJN1IlVjltx2ifjE3zitL 25 | jlChLrZuPoW5psQGSyzZ47uX+ILms0AkX4Uz44GN77xIBV1SKfVCyWMw/HJ4sfIA 26 | UEjX2Qrk8j0LOw24YPdXkDpxrQrRrI+a/TOA6IdsHmpXpMxoSWPdRw== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /dev/ssl/server.key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA95RtcYHgnNqg4/HwAX3qgTzqr+WhpTy8RlNc4b4e2hxBe6Ko 3 | hTP+UbRq59rybTkP0cDQOgFGXDWzHdDeIl6FGFG6C6dG/ADEMEl+uC71ucmuyyQB 4 | Emx6g8gDtjDiW4TyuLJwdcQ0HvpF45pDMkPA+mKFFwZIA0aM2IJhj9PZWsjvOAD9 5 | 1tYunrWUJYzVnQNKBSivCsKlI67NQTDb7ZPj3SgXh4iZWnl2SyEz+/y5BDcKB62p 6 | 9ypSMpjRLywM6o5mDF8Zcf39+8XeZe746Xb+/WgonAKZl5DpnhzyvBcmqTYmS7Zs 7 | 2G9gf95aHrFakbiOEwt+9oU0RKbPGjYDk4zsfwIDAQABAoIBAQDUugUbrYPIYpYx 8 | pbUaIw32w9fHEWDF8Jy5NHGvScOKlxKw3786zMTtgT8GPvtkW8465RhFbsj8++sO 9 | aGQlVd+4WKip0K5OY34vG0c1jKv4WdHFnAcLxIe0aYy+49vq1R4VEsVMeyxpnZOl 10 | j+vC6bKLsVE3yl7f6aOleMnJYjb1J9a1Z7Hqhm9EJtpQe9QKBhLToJSFq52wPB4V 11 | IwymUyakqnBZckbKQvvNXk4oUkyYRxaywyQA6/dptMCHBY2QuBDroQoxiwMb/C3z 12 | FaZD7L7i2F11yytLoQHuEKwJreG1tdItimH7ZimX00T/wuARh+KMxzIt+8NwGyNU 13 | uBTgKXoRAoGBAP3uyDxksDxsuDkTzaBTvFrOjCyc8zHj6gl2wNMWSsFsDyUslcHt 14 | y+875zYzVEHU6rYK73+qcge08HaYU/Ps29TT7zTKx+QWNoKnq+eOBV7YKJ1wPGhZ 15 | /XpwwH5/CjDqwXluiKB/PRUDECl5HQxY6PgyLei/TwN7c7RKuFTOaW0nAoGBAPmY 16 | Z75XAFrKiBy2XjaHLD6WIkVmXhy9kR5mSN/scvWlAV/rM1wdsrSUK4XjmxKpWdW7 17 | PgAXuHwlTnA9Orzs0I9gR62v677StdCXriyaugUSlJjElQU8AroPwhsvsJRr0mws 18 | ut95pqeHkR4P1niwxEGKBoxN3r9WKjzZMfad50zpAoGBAPrpF7f9Mdry/nF/rN3v 19 | /5ymnQF0MCWq89VYHx4017cZxDlSPcP66VjBkywOOgI2lZk9Yvt9+uC8nCJDUCkl 20 | cO5yX9mFSVCJ1mDxtr29qnbWFgPDJB1kVj/G653Sf8poHLaVTrFwKHX7qduhDd/m 21 | doTQaGajqjphoKmUb2F0991fAoGADB1eo7l7VezQsHwVlJX5CsykuzOdy81TtuMG 22 | FIBYkB3DNFGpO4ZhfyxV9Hi4tS4tIxekicKc/MGezgnayQLBmMP2lrKcQbXeh1Jl 23 | bNrRvp9JQSUBirB//WH4bPNiocGeBAwjwecYtLb6zze7lD4YOIoniTXaYvUetbdh 24 | GnRNnskCgYBd+FvtquQQ9E5ubEg1ANSrZUW5SS5B6ZpXFd5MIRKmgKF4P3p0teUX 25 | xsLW0zVmeUUer6fS5nQJXnZt+uK6kDDgR0WOMOvXp7hgKwXLnvyZol+c+ncU+nvP 26 | atCBqvK+vSvT9b3SXrmJZNp25TjhggJ0w03yA8ZFBY0aFd4WEzCXfA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /tests/ssl/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA7tmoSrzLVOYgn4DlH1P4+RJWts233ADBQKAM1RB0MUkn7SCX 3 | QReZU9Ha78Ml8vfb1XfSAIjTZIqtzvn6rcqN2NOb4lKlWx/w3BNt+qyJH0L3KR8z 4 | KN3Azs0OknmaDMzTd96IVqWJLFuJvx2YYAT22Unc5Net4KAho2RhVUzpbdvpqoIx 5 | jgyPx/z6Gdcwz3xKTU2GFI+rjaIOnGzpLShyN5/n2MCkRTAWQdhgyqOe+9tEyWhj 6 | WJI5VFKCmVVZIBpHg4YhDSflCeD0rG7QHQMJ8wfqsQCwLLMs4IZmMUXrGLQ+zjaz 7 | EkFIvkNhmOR5QPT1qARWSUcHIq33uAbMJ/XsZwIDAQABAoIBAAoOIbabwhed5Iuy 8 | enLaLyYVLo9dk7rwUTJuSnwByFJ22PZzo8p6utq7f/MODR/wbVHMTIv3NqcJ641G 9 | GtaMK3KQXgSmJA31B0fMjZm6/77QK+WQSbATvdU27sEd221khhaKyMSVDXRPmjnw 10 | fgS00MuO/3ym08wcEa52QF698SnNvnOWeVc4WpMRfBZTnHFKspE5cZAWVlw/rGYI 11 | /qC2e50r63yrqAyxrMTnae2gAPGC15fSUu2QI7I5RodN250J4CXbFqXiqwcHIuPz 12 | KRxyK2G4NrnX7x0Wb+Xpl4YGk6aYWi9hGThVymeu69039ZHiebX9No1v2LyyWN+K 13 | mbjcqRECgYEA912s1JaUnqz587vLr4+QkfHLraTF3Lk0i1Fk8MPKsxloDjcExIZM 14 | HGdk0NzUxG6ia1GZ/KpiJvEh55No33/hpnXQ4pvkU2iF+fTz2MR/5YKfBFi1PfOl 15 | 7+zOGbTLYbh10KYIoe5WsPstLbPBeG+eBaf7JZpu91LvtZhMhrKMU28CgYEA9y/k 16 | B7k8k7juIiDVVqta/DEx+bKyeg+Htirj6EszODzJ1g0TTjkGK+ecN/aJTachLdfB 17 | huNZPsyBif1CN9ls8Z4n4dfbeDwb/AH7145lR+TlMX9MAcNA7AJVt+Bmtc4aisKk 18 | 9u8ek+nvLmhasfaLB1bkTapCqH0nnJoR7kncGokCgYEA3Qv0mbRF8tWYNtmmVqfw 19 | BLsRy4Xm2isWbTi6/vu3cvxNJAa+V++6q9cpHAt6hl5m/YfaFxMayrnIFmx/1tks 20 | B9hYZWfxTSemMYtXLGnL8IgdGYE71LSZoCD4u7f31sdDYMQ+CpY1GcoawTBUvw7f 21 | IybNIvkyGAkg9CG4oNWE2DkCgYAwP3DK8SUZ0Gbq0m3R1PCVeSTAEFk16gIbVJPX 22 | q5X7zCG5XiP+iLLjrvmXEtAKEC0xvYNQyN+KmnBOdtiN/ip9ntk6A+Aav+DWd6yo 23 | jmrGda8m0ioLzgjGCE6i7XIe2nPj0cpRSKs3Q9ojvMq3IeBRNmypnCoWqRcqQght 24 | 8oy7wQKBgQDxJdWURMxmGupAGxl8/5BH70KGFDuqQcYOXDDEMP3n3ig/mI2SGnJc 25 | ADK4rWESJUqi61EzjlS9DaVyqwGgZGYdu55O8v7h3O2v21PO63pUVuugpeAdSheE 26 | PZBJ8Muv6l3dUP9IjGFDzyhjlt7Sh3sVz7E9xMN9JDt9ZOxNtNoCXw== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /dev/ssl/client.etcd.key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpgIBAAKCAQEAvKzAY6ZutFPqdSQRtWYhwU3HRFCDs7TMOr5KXcgfFw0kciL3 3 | OvfW/Nl3rfDumbvYE5wIdStmAMa+8uHTCdBGSNR/y0qmRWXY9VLZNYaq1scgT9ES 4 | O0lZSKa3oCq0v1e1nmlHIyuUyiG4h2SN+p4QhkswWQilz6tm6c1ANmIi7e/IhQGQ 5 | 0u7YNZg01OAKVYYIvGoRrNb+pObLYSwayN+Vne8T3ySzr6UtmyCI/BhdVROsRTmX 6 | 7MJBz20CLEJ3mv9dspEYllpskUVkud26JyBOT0BtPNlD3XP4AMtJvPb5rIjRmkfJ 7 | Khp0Mz4bXioaWU6hTCN+q5aGaBozBbxDXXro4wIDAQABAoIBAQCrKlGzVabzA3od 8 | YWaLWadf1a7kQ+LnYisxU7blk4rY/7xDq4L8xSEN7EAN1hiv8WEakSB/I6r7I1LR 9 | x5sMOR26LsXs6MElDjuge9PAInbUOHwQzQwsKuPL7CuEK5okSG7k0n+N4ZivM55T 10 | fh7r8P/uxUGMVDMQzl+j1efeLnOfxKzNWIFFBPQlKYYqCexo2Ua5cqWHCXILuxhw 11 | TCgZNMSCqX1hJvffIZym4xNB6cL8QtYytFAGHsz/ecHoJfnGXQMgV91kFEjBp3Ij 12 | 9ybSGUiyJJnI54Kjdz55Xvw//yb4mSOxy8koLTpdVbH6cJz3YnhwJpPWDZci+OWG 13 | 2igx1OjRAoGBAOXKhtxVdRt+1FgbbrKrcPnp7lpQPra7tQYwVL2+M+IzkKeVDEWg 14 | /ph/CCyBnnRohWrOFF6PuUSQITzJc5burlXvqFswcuWhMr++S7T4CAyHPr4U/bxh 15 | B0l+OO8zav7LURWijlxp65JwELz4f11mcpzTk9dykXd3XyhElb5+bvh5AoGBANIx 16 | tHkW9VRZmRzwNjwvlqr+cq20iCfI5WtkYPzAATpL42O49GzGD9Ra7APyh6H/ylUP 17 | BVl9w5r+FgZRR6ESLeFRXsD0ctUnCh2YKEemsz+kzjnmCcRDHpRWPeJx5uwmmKJv 18 | R+CaJOyyWH56mV3oQbmZctfjYnZ4aOuJ54Z6NY07AoGBANvh4xA8+dU4lIpGnbN8 19 | F5wce8aR2KDUltD4OG5j7ZO60eV52wdZmaVkkPa9hVlQlrHvxlAB6T8xR4y6G9BG 20 | Ti3PKOMxRhBzInOV7S2a1WDfnWsX/QBgPC/YBzTkl7J97PXt2ZmWhSNX9rxMcNt3 21 | 554nWuWjLI0LfdOzZTKmGxvJAoGBALjmBG2oZnKmxmuQWwUij5Unc1WeQ09MB5Qq 22 | nq53cB0J+KMWZdzxdadFu+vVAr7cte2Q4xHP8at763/vK+Viin10CLgP0Jv0VY+m 23 | 83fkCjXoEYkY9Gpy6IlwO8vgilQdt5Lb9Qx8TBLy56mKyzKyRm6vrzh9y3WPgiwA 24 | CWxgQI9VAoGBAJJwH1AHAWKruLLl6rqzvRrAQW30JCg7kshwHbF/Ta3Gjnr89Ptg 25 | 6DAySnT2BociXi6BgFYgYL64QlDEoZtrZ/idLlCw0oRKWhmexvSYsj4lqSKjNAn7 26 | l79atTT3ywxRJVp4x9TGObpie2PsP4HgHzfAJ5RRHCMIaRL55OIBuCSL 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /tests/ssl/etcd.local.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAvrSEXMzN2knyYeTrXMWTk91b57lWbRiY/kpG56gSM7uhfRHS 3 | I5Cg+L/swAsVbdHX5Rtbz9FgUTwwAMieDYjT5wcRY973mzJIyehTQo8nKRH6ceOE 4 | W9/zClpnQvZEhE6bCkEver3AGTSdUCinFWHMzVrime5+h6U28JhW5E8J2zwp/N2w 5 | 6bICXk75fef83EvLmyk4LjNxInxLGczv7L5S4iJqT+okrBOaTE8RRe0F4uqebAxe 6 | bl87YvkxhvVXTbmuA3JuXIFYhsmzdCdL409/Ww3EotJlPVmv+MZdsKcy3hjcXsWd 7 | 731aCMgxvbXuhNtKkav9grNblkFUbXtHCcmbyQIDAQABAoIBAQC25GDcrv3tD2YJ 8 | RIYEnud9s5lkjna4ghKYLZv6p1Ftk9C4JdMmkJoQdeAT/Y07mTg4XRicIe0Be3t+ 9 | BOivij3PHxQqd7d3e64bFVhkSO4BkWBiwRQ7GHf5weGucogba1d9ai6ixD/q7G7C 10 | ZXp4IydK+pK/ld57MJOUAkWzB6ooOc4ZJTGicYAZLlw4A0Lo50DM+JexOgeegU58 11 | APcgy986kzqNT0zz2CQijvp2YZ5uuJ1WKrpe+ybGKZx3/wRkeuCgmlqGI4DrUdBy 12 | irxee3l96bVm+Olrmcm9XwDrATTvloNxV1SVJvZeCUTv6obYSNXISnrLnYg0MWvD 13 | oknHHIdBAoGBAO093SYj7JL7yZ/J/kGY7uXbnIeuyu96+9Ho8miFaVzlvF1L/XZW 14 | cxvhh75qB4afKWNHIvF6wNoSpVluUZxeDl0JXYFn3ef8Hkq0DzPYYpy4MVUoWK1l 15 | G8LRhdoxSobxL7hyY7B58QxNs1AIFqIBYitYOKshuA1DAUdKAFkiPv7FAoGBAM3I 16 | rr5jScRBqm0EBip0/xzEJ5O8Bk9GyY5uOo8WHn7ttZXBATdfPCI5f4kwJLEA5TQ9 17 | wdl7zKBssNRb2N+55Dh5uCUUlEqqwRABTIsHQp18etTYke1Dznc3mzxfqjkP1/CN 18 | 9StFHPK335w33VhD3pkRy0mOYwXqWqgfVWcMQTk1AoGBAMmOWJexx6ksr6TI2PHk 19 | O3sftIH198W0R8OT1HcGeXiKWhktmV5zngIe/tPAgzpVeLU217IvJy9ezgoH1uAx 20 | bKSQc1eg5f/Uy+uNxKi4ezdIqej+iTBviUFf/wdb/0RcHr3muVlSkH9yeBhTdt8W 21 | q8/FeQezcwoZVjm53kfb94c9AoGAaWVlioGT4H65aQhsmAZAO12D37iniqb4yIhQ 22 | WcS8bcoV/NNTaibOx8CnP3527GlG+1C+HkO39Fp23u0D0OKGrx44YFV/9hYqt6XS 23 | rEpVpg+BYc5iPyFBd/H1AiEFMCCbOsuaTEUjpHTkhEBgj7qDIebpNY1FzlvPftg9 24 | h3/RUBkCgYEAk2sPdEwQdO1bfYZzNzS3zhHBChVbEazb/PFj5uklqRUqqD5x4lzh 25 | zP7BMmPhfLceU2SN9bEyB+YY1XfCEGm/NwM+v8rh9dxHqKwOxtGicyQTsjiloThH 26 | k0snIrmv/NlmlSEJglc1ExqyLwDykb5xookxUL3m/VjUK78gOp1TtjU= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /tests/ssl/cert_2_newca/alien_client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAzD4SFmMwvZZhJNmdMIQy6+7l3FaAYl5GD4avxauKmUX8Udwl 3 | Lbdbmlzziibo9Qm6SqtWfedxsWmi5KbD3y9q2GhxtgWAknkUUxsDYSan0DEzKR3T 4 | G7YNUXo8zfT2n44WCZ4SLYxiKKUh6Ea+JGxn/zNbYu71rQyzlbatL77tgsgzxz1D 5 | J5yu31xQnOsTmKSi/ZAlOqLems8SXp1VT1S8iZuwkLX8KCXKvQq81cCpK25VEwpf 6 | gqeCqBx1la64PvyUSyucNeQgr0cb+TsWnGd8GTaK9ZCuCCtwPJalSoz36BKiGwC7 7 | PtsYVUAOnH2jFWhPddmHdEVitYPciTtde1UxqQIDAQABAoIBAFfpa72pKkSgouXK 8 | qWdFr+lv5g7GSPKOY5exQJ1T7g91WVse5/3ai5pQ4h8YCnPWoRIc19jPs1dLAetr 9 | gKnjbhkCM9fGXIYvibkPyo+ZpOfvFXdgOAsYfHkSpXN9Ol6JOSEgofBrGaEnGwYw 10 | 1Q02gDkPsQlPk/3NlssdW0RO6Uq9/pqxRz0AORb8x5KfMcEs/qH7JiG3mly8hN/h 11 | i8OHWFvFR6S/O3fQln+QNfVS+kOWo5+OjWBzqstJTCarp7OsyTOagz5I+6lQUA5s 12 | NpEDRohszItri4nCkbNEsygkrMv38rkYrqyWZP5/+V42wcdlymVvoTjA/SaH0/ro 13 | Vj2EA5ECgYEA8zNSg+xV1wdilYgIfGt55hLegtjtGUXW9pQguBtvxjC6jpnzFQSi 14 | 8W0knBnpoSnJE3OG0w6FOZck9mCo5XQUwWb0UcHntpIFoZNhVzVcnzf2j7Ac5uX1 15 | kwXy7AKLDHMw3hQW3G1EKIQgBzsIQRNAMyOskAA57h4cgeiGY6NxUu0CgYEA1v3c 16 | aLBOJdVTpqsCAVMhZG9BkIzfq4BEg/g9XR0RyQsLoLBI/F1v5a4cQZcDgx1FZNnW 17 | iow/AYS6aIdYS3F751M5tMm+ae4aCMefmn3wvVsLSFgjGbblawgikBToM4/GFuQ5 18 | iv1JIWnSxgdw+O9wSXMSnZaHPaxUxcoF6BEGVi0CgYBM3RaiPUqwCFR/IBxWfn1r 19 | WuFah4cAKtOS4SQqK/m6sOH1efCa3vlExpMAhywQ1K0JacTd5cPzfLKg79HRm0uI 20 | CKsFP/yk9iezdmoYbvQUGlLh/O5Vh66CximNsAwUZj+2oLDM//e/umqI/n5QWu46 21 | e3bpmDi26mtmH4CB80E4zQKBgDF+4G95p4QuSMPmOt/zM4zb0ExnIldpgFyhmf8F 22 | 4kJWuKaV7zeqi0hlfTvceIKHCqLdQ/dBsn+2/vNxtXIo/k2Ta4WjrKLWnkfHLYdz 23 | 1yjA/Sf6opoY8Vhi1xI0vgCg6Qn8IQNOrLd0lYHh1LB67275fx+ouBoKwrmGeTWk 24 | NcFBAoGAN/rBuIUJXnGK7KWB2vPwA1rcoMiDxC8RXw8WjQd9tnoJZhQqq9UVe/Hh 25 | j30ma96HCY6c2pa/hHXHW9iPQakr1gJYJQ9pjNSduijYMMPZ88lcjiiHQhcWGtuW 26 | 2wLzpVKM7qM2tNwCExFSeBEPw45t0MOGLinWBpVMkKENLnZqU70= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /dev/ssl/ca.key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: AES-128-CBC,7D12DC8A0F2AE15308FE39333749024B 4 | 5 | H7LS90SqhPgafjdwQsG/BIwGCIqfnAeP94R0jkRvDa/eMBEPRv1DPtXSb5b7Ihjm 6 | /oLZX1a9kzTfloUxGD5SHdOfaF6W9NWJl91B63U4BlCJgPSvN10K1TyxJcwf7ld9 7 | Gtg67XR7ooIalCYNOHWHjGhDzNf80RCINnp+fJl7S+a9VIK0CKr5SzyMXPba8DSf 8 | VTsVVYWScDDgi8BEsRixa4Iac2m5CpD9LquXixHe79PGWOyIJe7c5jMSx+UQ/Mjx 9 | 8NNna5/3HLLEvKIoAx9OkQ4gQFWMcP6CI6bWQJLDUaC7TSSYz7lPrmbDndsAS366 10 | 8Q+2Nv/7jrOHgiv3HLFKn8sQP6Pev2TYiej53jQ0fs101pX9clNs86KJsVX6WS8L 11 | C40nzlene3B8EcGVl5DjxGMdpBApBd5VMjniEKZxlO5a7t/pDZefEjl6dINKM0me 12 | /OuHVHsZbTWaYh0I459xi7S8At98fOIdx41QRmIizuRt5V7hPZZ+yjP5420/Jt8P 13 | 4CgnEXW7XCntYjYLk+SY4nyi/zCn7urum/oJ/4H+XOWJiwcUtKOqHkfBhSg6EQoK 14 | dgDob7MJ7+xy+Jg/DHsxG4D/OtvQnfWUrpO/RFPRAV3yPL8+JW7rLR0dNP6/ASZr 15 | emo07R4+6Irh5iYVO9eFqJwJCncWsAczUkOMdYjr65FhyIpsOrQZX03Pc3DDWOfi 16 | tJzOjPxw/UvfkHPxKGGOPdmxbmomsrkVPAi80bbnSnAiNO+P7A1KgllNCG8G+NEI 17 | xRyFe55GK8PXo/qfx7+7L8YAjJ/qmx7cBy0r3UA+2GYBo8ElIaAfN/YSV2LyIpNN 18 | eVDLXE3zLmkYpdXxiJT1YLqawwOweEd/v9BdGL918bT1LTgTiyz2oLgZNnP3xhdh 19 | OMec8dZd0NBh5kPDUgV5SvDBUjlQcpHVdtp9aRm179lbvmLxgopM63PyT5qC3uVH 20 | 4y8YVeCkmVYM6CSTTcHeLL78BiVFqmd80Cv8Q/8LmluwoAq+5pwgxvf1f65uPVcn 21 | jvwbuuGdZ9BFF8uiEbuKdEomqc2LDUJxVWmBgnmKU04/jvvFL+jFWBS/ISUysR7G 22 | aapMuTzwXcbDn40GA6xKCeBOaNr41P9juVG10pxZRodGQ8rLHHwAjuiMBpjyebYm 23 | LQFCoBDlK6J9QMKvgxgexW60xOctEp/lHakV3cte/0jZE8aApB48pj7HGQ/0+F5r 24 | oYIpy1i6N+ORktLN/H8zUWSKc5dgNWu8JIgQv86u2kUhmGL4iWhz5gsN7VBvr1No 25 | ozWvidKVZVmqahpm1x77VUVbkLG35c2kpIemolLQxqiMgNk3zjMn3PsM3uHI8MEe 26 | vAHsB7uMMyWsWmgjEhmtK9zVQnHC5A3QDYH5X4MiHwPFVASfsP1m92BHGGO3tfKT 27 | CfO+OZkYkKDRo6l6oNsxmtj5tUjpoqAIZTxnxy2j7JybssoNO7b0gg9TGyFl1pbu 28 | g/xjx9fjzO95V14zgI1X4aTwzIvZKaViIn1U0qk/cLyLzdgWj3BDsRlgovU/Dz/P 29 | OAlICK9bBaNYOKx7/m4XNckDXG/wJHwXPoh03UWwUcrAPPc+3edr433PcaAy/rfu 30 | -----END RSA PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /etcd/server_ops.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from etcd.common_ops import CommonOps 4 | from etcd.response import ResponseV2 5 | from etcd.compat import parse_qsl 6 | 7 | 8 | class ServerOps(CommonOps): 9 | """Functions that query the server for cluster-level information.""" 10 | 11 | def get_version(self): 12 | """Return a string representing the version of the server that we're 13 | connected to. 14 | 15 | :returns: Version 16 | :rtype: string 17 | """ 18 | 19 | version_string = self.get_text('version', '/version', version=None) 20 | 21 | # Version should look like "etcd v0.2.0". 22 | prefix = 'etcd v' 23 | 24 | if version_string.startswith(prefix) is False: 25 | raise ValueError("Could not parse server version: %s" % (r.text)) 26 | 27 | return version_string[len(prefix):] 28 | 29 | def get_leader_url_prefix(self): 30 | """Return the URL prefix of the leader host. 31 | 32 | :returns: URL prefix 33 | :rtype: string 34 | """ 35 | 36 | return self.get_text('leader', '/leader') 37 | 38 | def get_machines(self): 39 | """Return the list of servers in the cluster represented as nodes. 40 | 41 | :returns: Response object 42 | :rtype: :class:`etcd.response.ResponseV2` 43 | """ 44 | 45 | fq_path = self.get_fq_node_path('/_etcd/machines') 46 | response = self.client.send(2, 'get', fq_path, allow_reconnect=False) 47 | 48 | for machine in response.node.children: 49 | yield parse_qsl(machine.value) 50 | 51 | def get_dashboard_url(self): 52 | """Return the URL for the dashboard on the server currently connected- 53 | to. 54 | 55 | :returns: URL 56 | :rtype: string 57 | """ 58 | 59 | return (self.client.prefix + '/mod/dashboard') 60 | 61 | -------------------------------------------------------------------------------- /etcd/modules/leader.py: -------------------------------------------------------------------------------- 1 | from requests.exceptions import HTTPError 2 | 3 | from etcd.common_ops import CommonOps 4 | 5 | 6 | class LeaderMod(CommonOps): 7 | """'Leader' functionality for consensus-based assignment. If multiple 8 | processes try to assign different simple strings to the given key, the 9 | first will succeed and block the others until the TTL expires. The same 10 | process repeats for all subsequent assignments. 11 | """ 12 | 13 | def __get_path(self, leader_key): 14 | return ('/' + leader_key) 15 | 16 | def set_or_renew(self, key, value, ttl): 17 | self.client.debug("LEADER: Setting key [%s] with value [%s]." % 18 | (key, value)) 19 | 20 | fq_path = self.__get_path(key) 21 | 22 | # TODO: Why is it called "name"? 23 | data = { 'name': value } 24 | parameters = { 'ttl': ttl } 25 | 26 | self.client.send(2, 'put', fq_path, data=data, 27 | parameters=parameters, module='leader', 28 | return_raw=True) 29 | 30 | def get(self, key): 31 | self.client.debug("LEADER: Getting value for key [%s]." % (key)) 32 | 33 | fq_path = self.__get_path(key) 34 | 35 | # TODO: 36 | # 37 | # If this fails, we get a text response on a 200: 38 | # 39 | # get leader error: read lock error: Cannot reach servers after 3 time\n 40 | # 41 | # Raise a KeyError when this is fixed. 42 | 43 | r = self.client.send(2, 'get', fq_path, module='leader', 44 | return_raw=True) 45 | 46 | if r.text == '': 47 | return None 48 | 49 | result = r.text 50 | if result.startswith('get leader error:') is True: 51 | raise KeyError(key) 52 | 53 | return result 54 | 55 | def delete(self, key, value): 56 | self.client.debug("LEADER: Deleting key [%s] with value [%s]." % 57 | (key, value)) 58 | 59 | # TODO: 60 | # 61 | # If this fails, we get a text response with a 500: 62 | # 63 | # delete leader error: release lock error: cannot find: test value 64 | # 65 | # Raise a KeyError when this is fixed. 66 | 67 | fq_path = self.__get_path(key) 68 | parameters = { 'name': value } 69 | 70 | try: 71 | self.client.send(2, 'delete', fq_path, module='leader', 72 | parameters=parameters, return_raw=True) 73 | except HTTPError as e: 74 | if e.response.status_code == 500: 75 | raise KeyError(key) 76 | 77 | raise 78 | -------------------------------------------------------------------------------- /etcd/inorder_ops.py: -------------------------------------------------------------------------------- 1 | from etcd.common_ops import CommonOps 2 | 3 | 4 | class _InOrder(CommonOps): 5 | """Represents an in-order directory at a specific path. 6 | 7 | :param client: Instance of client 8 | :param fq_path: Full path of the directory. 9 | 10 | :type client: :class:`etcd.client.Client` 11 | :type fq_path: string 12 | """ 13 | 14 | def __init__(self, path, *args, **kwargs): 15 | super(_InOrder, self).__init__(*args, **kwargs) 16 | 17 | self.__path = path 18 | 19 | def create(self): 20 | """Explicitly create the directory. Not usually necessary. 21 | :returns: Response object 22 | :rtype: :class:`etcd.response.ResponseV2` 23 | """ 24 | 25 | return self.client.directory.create(self.__path) 26 | 27 | def delete(self): 28 | """Delete the directory. 29 | :returns: Response object 30 | :rtype: :class:`etcd.response.ResponseV2` 31 | """ 32 | 33 | return self.client.directory.delete_recursive(self.__path) 34 | 35 | def pop(self, name): 36 | self.client.node.delete(self.__path + '/' + name) 37 | 38 | def add(self, value): 39 | """Add an in-order value. 40 | 41 | :param value: Value to be automatically-assigned a key. 42 | :type value: string 43 | 44 | :returns: Response object 45 | :rtype: :class:`etcd.response.ResponseV2` 46 | """ 47 | 48 | # TODO: Can we send a TTL? 49 | 50 | fq_path = self.get_fq_node_path(self.__path) 51 | return self.client.send(2, 'post', fq_path, value=value) 52 | 53 | def list(self, sorted=False): 54 | """Return a list of the inserted nodes. 55 | 56 | :param sorted: Return nodes in the proper, chronological order. 57 | :type sorted: bool 58 | 59 | :returns: Response object 60 | :rtype: :class:`etcd.response.ResponseV2` 61 | """ 62 | 63 | parameters = {} 64 | if sorted is True: 65 | parameters['sorted'] = 'true' 66 | 67 | return self.client.send(2, 'get', self.__path, parameters=parameters) 68 | 69 | 70 | class InOrderOps(CommonOps): 71 | """The functions having to do with in-order keys.""" 72 | 73 | def get_inorder(self, path): 74 | """Get an instance of the in-order directory class for a specific key. 75 | 76 | :param path: Key 77 | :type path: string 78 | 79 | :returns: A _InOrder instance for the given path. 80 | :rtype: :class:`etcd.inorder_ops._InOrder` 81 | """ 82 | 83 | return _InOrder(path, self.client) 84 | -------------------------------------------------------------------------------- /tests/ssl/demoCA/cacert.pem: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | fc:4e:e3:43:7c:f6:9d:da 6 | Signature Algorithm: sha1WithRSAEncryption 7 | Issuer: C=US, ST=Florida, O=Internet Widgits Pty Ltd, CN=ca.local 8 | Validity 9 | Not Before: Jan 16 05:34:40 2014 GMT 10 | Not After : Jan 15 05:34:40 2017 GMT 11 | Subject: C=US, ST=Florida, O=Internet Widgits Pty Ltd, CN=ca.local 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | RSA Public Key: (1024 bit) 15 | Modulus (1024 bit): 16 | 00:cd:97:15:9c:8f:8b:69:fe:32:f3:7e:5f:76:e3: 17 | ef:b7:d1:17:08:4e:7c:39:59:f2:8c:98:fc:c8:79: 18 | f3:49:e0:9f:e8:fb:74:87:b1:68:d4:13:a6:3c:77: 19 | 64:93:5b:e3:3f:48:99:d2:01:94:50:94:21:9e:8d: 20 | 39:ec:b9:68:a2:d8:a1:4a:1a:b1:cf:ca:67:37:0d: 21 | ff:67:84:a9:40:39:43:4e:24:17:97:d1:3a:25:17: 22 | ff:32:3e:1e:1a:bf:00:f7:2b:1c:ef:2c:9b:35:96: 23 | ee:c8:d6:82:54:77:f6:88:0f:9e:62:8f:ac:7d:9a: 24 | 81:87:7c:8c:9a:07:8d:f0:5b 25 | Exponent: 65537 (0x10001) 26 | X509v3 extensions: 27 | X509v3 Subject Key Identifier: 28 | 78:81:94:51:E7:72:21:2E:F2:9B:0D:73:02:39:31:FE:9C:E3:FF:3D 29 | X509v3 Authority Key Identifier: 30 | keyid:78:81:94:51:E7:72:21:2E:F2:9B:0D:73:02:39:31:FE:9C:E3:FF:3D 31 | DirName:/C=US/ST=Florida/O=Internet Widgits Pty Ltd/CN=ca.local 32 | serial:FC:4E:E3:43:7C:F6:9D:DA 33 | 34 | X509v3 Basic Constraints: 35 | CA:TRUE 36 | Signature Algorithm: sha1WithRSAEncryption 37 | bd:79:9f:ce:0f:f9:fe:88:4c:4b:9b:15:3e:1f:ff:d3:36:eb: 38 | 32:12:e0:b9:d8:03:4a:bd:a9:c6:75:5e:90:19:b2:6d:b6:93: 39 | 97:4c:27:ce:99:fb:79:72:a5:b4:d3:b9:e2:73:c9:ad:90:99: 40 | 84:82:fd:6c:4f:e8:54:60:a8:be:38:9f:17:62:f2:1c:73:fe: 41 | 6d:6a:ab:08:81:b2:d6:a5:f4:f0:51:f3:9f:bf:1c:3e:cc:77: 42 | ab:60:91:05:81:80:2a:6c:12:95:15:4d:6a:8b:49:16:e3:55: 43 | 9c:36:90:59:bc:f1:43:06:99:69:a3:52:8a:1f:b0:7c:53:02: 44 | 27:18 45 | -----BEGIN CERTIFICATE----- 46 | MIIC4TCCAkqgAwIBAgIJAPxO40N89p3aMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV 47 | BAYTAlVTMRAwDgYDVQQIEwdGbG9yaWRhMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRn 48 | aXRzIFB0eSBMdGQxETAPBgNVBAMTCGNhLmxvY2FsMB4XDTE0MDExNjA1MzQ0MFoX 49 | DTE3MDExNTA1MzQ0MFowVTELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0Zsb3JpZGEx 50 | ITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDERMA8GA1UEAxMIY2Eu 51 | bG9jYWwwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM2XFZyPi2n+MvN+X3bj 52 | 77fRFwhOfDlZ8oyY/Mh580ngn+j7dIexaNQTpjx3ZJNb4z9ImdIBlFCUIZ6NOey5 53 | aKLYoUoasc/KZzcN/2eEqUA5Q04kF5fROiUX/zI+Hhq/APcrHO8smzWW7sjWglR3 54 | 9ogPnmKPrH2agYd8jJoHjfBbAgMBAAGjgbgwgbUwHQYDVR0OBBYEFHiBlFHnciEu 55 | 8psNcwI5Mf6c4/89MIGFBgNVHSMEfjB8gBR4gZRR53IhLvKbDXMCOTH+nOP/PaFZ 56 | pFcwVTELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0Zsb3JpZGExITAfBgNVBAoTGElu 57 | dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDERMA8GA1UEAxMIY2EubG9jYWyCCQD8TuND 58 | fPad2jAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBAL15n84P+f6ITEub 59 | FT4f/9M26zIS4LnYA0q9qcZ1XpAZsm22k5dMJ86Z+3lypbTTueJzya2QmYSC/WxP 60 | 6FRgqL44nxdi8hxz/m1qqwiBstal9PBR85+/HD7Md6tgkQWBgCpsEpUVTWqLSRbj 61 | VZw2kFm88UMGmWmjUoofsHxTAicY 62 | -----END CERTIFICATE----- 63 | -------------------------------------------------------------------------------- /tests/ssl/demoCA/newcerts/FC4EE3437CF69DDA.pem: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | fc:4e:e3:43:7c:f6:9d:da 6 | Signature Algorithm: sha1WithRSAEncryption 7 | Issuer: C=US, ST=Florida, O=Internet Widgits Pty Ltd, CN=ca.local 8 | Validity 9 | Not Before: Jan 16 05:34:40 2014 GMT 10 | Not After : Jan 15 05:34:40 2017 GMT 11 | Subject: C=US, ST=Florida, O=Internet Widgits Pty Ltd, CN=ca.local 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | RSA Public Key: (1024 bit) 15 | Modulus (1024 bit): 16 | 00:cd:97:15:9c:8f:8b:69:fe:32:f3:7e:5f:76:e3: 17 | ef:b7:d1:17:08:4e:7c:39:59:f2:8c:98:fc:c8:79: 18 | f3:49:e0:9f:e8:fb:74:87:b1:68:d4:13:a6:3c:77: 19 | 64:93:5b:e3:3f:48:99:d2:01:94:50:94:21:9e:8d: 20 | 39:ec:b9:68:a2:d8:a1:4a:1a:b1:cf:ca:67:37:0d: 21 | ff:67:84:a9:40:39:43:4e:24:17:97:d1:3a:25:17: 22 | ff:32:3e:1e:1a:bf:00:f7:2b:1c:ef:2c:9b:35:96: 23 | ee:c8:d6:82:54:77:f6:88:0f:9e:62:8f:ac:7d:9a: 24 | 81:87:7c:8c:9a:07:8d:f0:5b 25 | Exponent: 65537 (0x10001) 26 | X509v3 extensions: 27 | X509v3 Subject Key Identifier: 28 | 78:81:94:51:E7:72:21:2E:F2:9B:0D:73:02:39:31:FE:9C:E3:FF:3D 29 | X509v3 Authority Key Identifier: 30 | keyid:78:81:94:51:E7:72:21:2E:F2:9B:0D:73:02:39:31:FE:9C:E3:FF:3D 31 | DirName:/C=US/ST=Florida/O=Internet Widgits Pty Ltd/CN=ca.local 32 | serial:FC:4E:E3:43:7C:F6:9D:DA 33 | 34 | X509v3 Basic Constraints: 35 | CA:TRUE 36 | Signature Algorithm: sha1WithRSAEncryption 37 | bd:79:9f:ce:0f:f9:fe:88:4c:4b:9b:15:3e:1f:ff:d3:36:eb: 38 | 32:12:e0:b9:d8:03:4a:bd:a9:c6:75:5e:90:19:b2:6d:b6:93: 39 | 97:4c:27:ce:99:fb:79:72:a5:b4:d3:b9:e2:73:c9:ad:90:99: 40 | 84:82:fd:6c:4f:e8:54:60:a8:be:38:9f:17:62:f2:1c:73:fe: 41 | 6d:6a:ab:08:81:b2:d6:a5:f4:f0:51:f3:9f:bf:1c:3e:cc:77: 42 | ab:60:91:05:81:80:2a:6c:12:95:15:4d:6a:8b:49:16:e3:55: 43 | 9c:36:90:59:bc:f1:43:06:99:69:a3:52:8a:1f:b0:7c:53:02: 44 | 27:18 45 | -----BEGIN CERTIFICATE----- 46 | MIIC4TCCAkqgAwIBAgIJAPxO40N89p3aMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV 47 | BAYTAlVTMRAwDgYDVQQIEwdGbG9yaWRhMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRn 48 | aXRzIFB0eSBMdGQxETAPBgNVBAMTCGNhLmxvY2FsMB4XDTE0MDExNjA1MzQ0MFoX 49 | DTE3MDExNTA1MzQ0MFowVTELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0Zsb3JpZGEx 50 | ITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDERMA8GA1UEAxMIY2Eu 51 | bG9jYWwwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM2XFZyPi2n+MvN+X3bj 52 | 77fRFwhOfDlZ8oyY/Mh580ngn+j7dIexaNQTpjx3ZJNb4z9ImdIBlFCUIZ6NOey5 53 | aKLYoUoasc/KZzcN/2eEqUA5Q04kF5fROiUX/zI+Hhq/APcrHO8smzWW7sjWglR3 54 | 9ogPnmKPrH2agYd8jJoHjfBbAgMBAAGjgbgwgbUwHQYDVR0OBBYEFHiBlFHnciEu 55 | 8psNcwI5Mf6c4/89MIGFBgNVHSMEfjB8gBR4gZRR53IhLvKbDXMCOTH+nOP/PaFZ 56 | pFcwVTELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0Zsb3JpZGExITAfBgNVBAoTGElu 57 | dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDERMA8GA1UEAxMIY2EubG9jYWyCCQD8TuND 58 | fPad2jAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBAL15n84P+f6ITEub 59 | FT4f/9M26zIS4LnYA0q9qcZ1XpAZsm22k5dMJ86Z+3lypbTTueJzya2QmYSC/WxP 60 | 6FRgqL44nxdi8hxz/m1qqwiBstal9PBR85+/HD7Md6tgkQWBgCpsEpUVTWqLSRbj 61 | VZw2kFm88UMGmWmjUoofsHxTAicY 62 | -----END CERTIFICATE----- 63 | -------------------------------------------------------------------------------- /tests/ssl/client.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | fc:4e:e3:43:7c:f6:9d:dc 6 | Signature Algorithm: sha1WithRSAEncryption 7 | Issuer: C=US, ST=Florida, O=Internet Widgits Pty Ltd, CN=ca.local 8 | Validity 9 | Not Before: Jan 16 05:36:53 2014 GMT 10 | Not After : Jan 16 05:36:53 2015 GMT 11 | Subject: C=US, ST=Florida, O=Internet Widgits Pty Ltd, CN=client.local 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | RSA Public Key: (2048 bit) 15 | Modulus (2048 bit): 16 | 00:ee:d9:a8:4a:bc:cb:54:e6:20:9f:80:e5:1f:53: 17 | f8:f9:12:56:b6:cd:b7:dc:00:c1:40:a0:0c:d5:10: 18 | 74:31:49:27:ed:20:97:41:17:99:53:d1:da:ef:c3: 19 | 25:f2:f7:db:d5:77:d2:00:88:d3:64:8a:ad:ce:f9: 20 | fa:ad:ca:8d:d8:d3:9b:e2:52:a5:5b:1f:f0:dc:13: 21 | 6d:fa:ac:89:1f:42:f7:29:1f:33:28:dd:c0:ce:cd: 22 | 0e:92:79:9a:0c:cc:d3:77:de:88:56:a5:89:2c:5b: 23 | 89:bf:1d:98:60:04:f6:d9:49:dc:e4:d7:ad:e0:a0: 24 | 21:a3:64:61:55:4c:e9:6d:db:e9:aa:82:31:8e:0c: 25 | 8f:c7:fc:fa:19:d7:30:cf:7c:4a:4d:4d:86:14:8f: 26 | ab:8d:a2:0e:9c:6c:e9:2d:28:72:37:9f:e7:d8:c0: 27 | a4:45:30:16:41:d8:60:ca:a3:9e:fb:db:44:c9:68: 28 | 63:58:92:39:54:52:82:99:55:59:20:1a:47:83:86: 29 | 21:0d:27:e5:09:e0:f4:ac:6e:d0:1d:03:09:f3:07: 30 | ea:b1:00:b0:2c:b3:2c:e0:86:66:31:45:eb:18:b4: 31 | 3e:ce:36:b3:12:41:48:be:43:61:98:e4:79:40:f4: 32 | f5:a8:04:56:49:47:07:22:ad:f7:b8:06:cc:27:f5: 33 | ec:67 34 | Exponent: 65537 (0x10001) 35 | X509v3 extensions: 36 | X509v3 Extended Key Usage: 37 | TLS Web Client Authentication 38 | Signature Algorithm: sha1WithRSAEncryption 39 | 6c:01:60:60:c8:e9:6d:30:84:84:19:c0:c1:74:07:c7:1a:a4: 40 | 41:05:5a:8b:ce:31:16:23:c2:f8:0d:05:b4:92:27:42:5b:4f: 41 | 84:b6:93:16:98:66:0c:68:2c:4f:5f:50:4b:8f:02:9d:df:7a: 42 | 7f:59:55:b0:98:e3:08:f0:f8:ef:13:cc:12:56:2d:39:0d:22: 43 | 8c:44:87:46:58:32:3f:d8:ce:3b:03:79:b4:59:06:45:8e:cd: 44 | 6f:06:ef:f9:75:b8:d6:5f:45:10:d1:a6:e4:4a:d3:a4:14:db: 45 | 2e:d3:0a:72:2f:3c:3f:6e:8a:30:09:6b:9a:69:f1:74:31:2a: 46 | 87:d9 47 | -----BEGIN CERTIFICATE----- 48 | MIICxzCCAjCgAwIBAgIJAPxO40N89p3cMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV 49 | BAYTAlVTMRAwDgYDVQQIEwdGbG9yaWRhMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRn 50 | aXRzIFB0eSBMdGQxETAPBgNVBAMTCGNhLmxvY2FsMB4XDTE0MDExNjA1MzY1M1oX 51 | DTE1MDExNjA1MzY1M1owWTELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0Zsb3JpZGEx 52 | ITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEVMBMGA1UEAxMMY2xp 53 | ZW50LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7tmoSrzL 54 | VOYgn4DlH1P4+RJWts233ADBQKAM1RB0MUkn7SCXQReZU9Ha78Ml8vfb1XfSAIjT 55 | ZIqtzvn6rcqN2NOb4lKlWx/w3BNt+qyJH0L3KR8zKN3Azs0OknmaDMzTd96IVqWJ 56 | LFuJvx2YYAT22Unc5Net4KAho2RhVUzpbdvpqoIxjgyPx/z6Gdcwz3xKTU2GFI+r 57 | jaIOnGzpLShyN5/n2MCkRTAWQdhgyqOe+9tEyWhjWJI5VFKCmVVZIBpHg4YhDSfl 58 | CeD0rG7QHQMJ8wfqsQCwLLMs4IZmMUXrGLQ+zjazEkFIvkNhmOR5QPT1qARWSUcH 59 | Iq33uAbMJ/XsZwIDAQABoxcwFTATBgNVHSUEDDAKBggrBgEFBQcDAjANBgkqhkiG 60 | 9w0BAQUFAAOBgQBsAWBgyOltMISEGcDBdAfHGqRBBVqLzjEWI8L4DQW0kidCW0+E 61 | tpMWmGYMaCxPX1BLjwKd33p/WVWwmOMI8PjvE8wSVi05DSKMRIdGWDI/2M47A3m0 62 | WQZFjs1vBu/5dbjWX0UQ0abkStOkFNsu0wpyLzw/boowCWuaafF0MSqH2Q== 63 | -----END CERTIFICATE----- 64 | -------------------------------------------------------------------------------- /tests/ssl/etcd.local.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | fc:4e:e3:43:7c:f6:9d:db 6 | Signature Algorithm: sha1WithRSAEncryption 7 | Issuer: C=US, ST=Florida, O=Internet Widgits Pty Ltd, CN=ca.local 8 | Validity 9 | Not Before: Jan 16 05:36:23 2014 GMT 10 | Not After : Jan 16 05:36:23 2015 GMT 11 | Subject: C=US, ST=Florida, O=Internet Widgits Pty Ltd, CN=etcd.local 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | RSA Public Key: (2048 bit) 15 | Modulus (2048 bit): 16 | 00:be:b4:84:5c:cc:cd:da:49:f2:61:e4:eb:5c:c5: 17 | 93:93:dd:5b:e7:b9:56:6d:18:98:fe:4a:46:e7:a8: 18 | 12:33:bb:a1:7d:11:d2:23:90:a0:f8:bf:ec:c0:0b: 19 | 15:6d:d1:d7:e5:1b:5b:cf:d1:60:51:3c:30:00:c8: 20 | 9e:0d:88:d3:e7:07:11:63:de:f7:9b:32:48:c9:e8: 21 | 53:42:8f:27:29:11:fa:71:e3:84:5b:df:f3:0a:5a: 22 | 67:42:f6:44:84:4e:9b:0a:41:2f:7a:bd:c0:19:34: 23 | 9d:50:28:a7:15:61:cc:cd:5a:e2:99:ee:7e:87:a5: 24 | 36:f0:98:56:e4:4f:09:db:3c:29:fc:dd:b0:e9:b2: 25 | 02:5e:4e:f9:7d:e7:fc:dc:4b:cb:9b:29:38:2e:33: 26 | 71:22:7c:4b:19:cc:ef:ec:be:52:e2:22:6a:4f:ea: 27 | 24:ac:13:9a:4c:4f:11:45:ed:05:e2:ea:9e:6c:0c: 28 | 5e:6e:5f:3b:62:f9:31:86:f5:57:4d:b9:ae:03:72: 29 | 6e:5c:81:58:86:c9:b3:74:27:4b:e3:4f:7f:5b:0d: 30 | c4:a2:d2:65:3d:59:af:f8:c6:5d:b0:a7:32:de:18: 31 | dc:5e:c5:9d:ef:7d:5a:08:c8:31:bd:b5:ee:84:db: 32 | 4a:91:ab:fd:82:b3:5b:96:41:54:6d:7b:47:09:c9: 33 | 9b:c9 34 | Exponent: 65537 (0x10001) 35 | X509v3 extensions: 36 | X509v3 Extended Key Usage: 37 | TLS Web Client Authentication 38 | Signature Algorithm: sha1WithRSAEncryption 39 | 7e:e1:db:11:e3:de:8e:7b:a1:f1:ed:28:02:81:f7:50:1e:0e: 40 | 9b:a2:a2:8d:ba:fe:58:47:4e:5c:45:f4:e8:27:ab:38:2d:5d: 41 | 8a:f7:2e:fd:b9:b4:00:5e:61:48:fe:cc:62:5f:87:8c:a8:6f: 42 | c2:b6:54:61:53:db:75:e1:08:44:fd:8a:df:24:f7:7f:62:2f: 43 | e5:33:6b:4e:fc:06:3e:6d:49:95:18:b0:82:fc:82:75:6f:69: 44 | 8f:d4:18:c8:11:e3:5f:9b:d3:19:a5:a2:d5:c4:ca:c6:79:15: 45 | c8:ce:4b:89:d2:4c:56:70:ba:16:cb:a8:a8:8c:3c:a7:f3:4a: 46 | 03:63 47 | -----BEGIN CERTIFICATE----- 48 | MIICxTCCAi6gAwIBAgIJAPxO40N89p3bMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV 49 | BAYTAlVTMRAwDgYDVQQIEwdGbG9yaWRhMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRn 50 | aXRzIFB0eSBMdGQxETAPBgNVBAMTCGNhLmxvY2FsMB4XDTE0MDExNjA1MzYyM1oX 51 | DTE1MDExNjA1MzYyM1owVzELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0Zsb3JpZGEx 52 | ITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDETMBEGA1UEAxMKZXRj 53 | ZC5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL60hFzMzdpJ 54 | 8mHk61zFk5PdW+e5Vm0YmP5KRueoEjO7oX0R0iOQoPi/7MALFW3R1+UbW8/RYFE8 55 | MADIng2I0+cHEWPe95sySMnoU0KPJykR+nHjhFvf8wpaZ0L2RIROmwpBL3q9wBk0 56 | nVAopxVhzM1a4pnufoelNvCYVuRPCds8KfzdsOmyAl5O+X3n/NxLy5spOC4zcSJ8 57 | SxnM7+y+UuIiak/qJKwTmkxPEUXtBeLqnmwMXm5fO2L5MYb1V025rgNyblyBWIbJ 58 | s3QnS+NPf1sNxKLSZT1Zr/jGXbCnMt4Y3F7Fne99WgjIMb217oTbSpGr/YKzW5ZB 59 | VG17RwnJm8kCAwEAAaMXMBUwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcN 60 | AQEFBQADgYEAfuHbEePejnuh8e0oAoH3UB4Om6Kijbr+WEdOXEX06CerOC1divcu 61 | /bm0AF5hSP7MYl+HjKhvwrZUYVPbdeEIRP2K3yT3f2Iv5TNrTvwGPm1JlRiwgvyC 62 | dW9pj9QYyBHjX5vTGaWi1cTKxnkVyM5LidJMVnC6FsuoqIw8p/NKA2M= 63 | -----END CERTIFICATE----- 64 | -------------------------------------------------------------------------------- /tests/ssl/FC4EE3437CF69DDB.pem: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | fc:4e:e3:43:7c:f6:9d:db 6 | Signature Algorithm: sha1WithRSAEncryption 7 | Issuer: C=US, ST=Florida, O=Internet Widgits Pty Ltd, CN=ca.local 8 | Validity 9 | Not Before: Jan 16 05:36:23 2014 GMT 10 | Not After : Jan 16 05:36:23 2015 GMT 11 | Subject: C=US, ST=Florida, O=Internet Widgits Pty Ltd, CN=etcd.local 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | RSA Public Key: (2048 bit) 15 | Modulus (2048 bit): 16 | 00:be:b4:84:5c:cc:cd:da:49:f2:61:e4:eb:5c:c5: 17 | 93:93:dd:5b:e7:b9:56:6d:18:98:fe:4a:46:e7:a8: 18 | 12:33:bb:a1:7d:11:d2:23:90:a0:f8:bf:ec:c0:0b: 19 | 15:6d:d1:d7:e5:1b:5b:cf:d1:60:51:3c:30:00:c8: 20 | 9e:0d:88:d3:e7:07:11:63:de:f7:9b:32:48:c9:e8: 21 | 53:42:8f:27:29:11:fa:71:e3:84:5b:df:f3:0a:5a: 22 | 67:42:f6:44:84:4e:9b:0a:41:2f:7a:bd:c0:19:34: 23 | 9d:50:28:a7:15:61:cc:cd:5a:e2:99:ee:7e:87:a5: 24 | 36:f0:98:56:e4:4f:09:db:3c:29:fc:dd:b0:e9:b2: 25 | 02:5e:4e:f9:7d:e7:fc:dc:4b:cb:9b:29:38:2e:33: 26 | 71:22:7c:4b:19:cc:ef:ec:be:52:e2:22:6a:4f:ea: 27 | 24:ac:13:9a:4c:4f:11:45:ed:05:e2:ea:9e:6c:0c: 28 | 5e:6e:5f:3b:62:f9:31:86:f5:57:4d:b9:ae:03:72: 29 | 6e:5c:81:58:86:c9:b3:74:27:4b:e3:4f:7f:5b:0d: 30 | c4:a2:d2:65:3d:59:af:f8:c6:5d:b0:a7:32:de:18: 31 | dc:5e:c5:9d:ef:7d:5a:08:c8:31:bd:b5:ee:84:db: 32 | 4a:91:ab:fd:82:b3:5b:96:41:54:6d:7b:47:09:c9: 33 | 9b:c9 34 | Exponent: 65537 (0x10001) 35 | X509v3 extensions: 36 | X509v3 Extended Key Usage: 37 | TLS Web Client Authentication 38 | Signature Algorithm: sha1WithRSAEncryption 39 | 7e:e1:db:11:e3:de:8e:7b:a1:f1:ed:28:02:81:f7:50:1e:0e: 40 | 9b:a2:a2:8d:ba:fe:58:47:4e:5c:45:f4:e8:27:ab:38:2d:5d: 41 | 8a:f7:2e:fd:b9:b4:00:5e:61:48:fe:cc:62:5f:87:8c:a8:6f: 42 | c2:b6:54:61:53:db:75:e1:08:44:fd:8a:df:24:f7:7f:62:2f: 43 | e5:33:6b:4e:fc:06:3e:6d:49:95:18:b0:82:fc:82:75:6f:69: 44 | 8f:d4:18:c8:11:e3:5f:9b:d3:19:a5:a2:d5:c4:ca:c6:79:15: 45 | c8:ce:4b:89:d2:4c:56:70:ba:16:cb:a8:a8:8c:3c:a7:f3:4a: 46 | 03:63 47 | -----BEGIN CERTIFICATE----- 48 | MIICxTCCAi6gAwIBAgIJAPxO40N89p3bMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV 49 | BAYTAlVTMRAwDgYDVQQIEwdGbG9yaWRhMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRn 50 | aXRzIFB0eSBMdGQxETAPBgNVBAMTCGNhLmxvY2FsMB4XDTE0MDExNjA1MzYyM1oX 51 | DTE1MDExNjA1MzYyM1owVzELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0Zsb3JpZGEx 52 | ITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDETMBEGA1UEAxMKZXRj 53 | ZC5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL60hFzMzdpJ 54 | 8mHk61zFk5PdW+e5Vm0YmP5KRueoEjO7oX0R0iOQoPi/7MALFW3R1+UbW8/RYFE8 55 | MADIng2I0+cHEWPe95sySMnoU0KPJykR+nHjhFvf8wpaZ0L2RIROmwpBL3q9wBk0 56 | nVAopxVhzM1a4pnufoelNvCYVuRPCds8KfzdsOmyAl5O+X3n/NxLy5spOC4zcSJ8 57 | SxnM7+y+UuIiak/qJKwTmkxPEUXtBeLqnmwMXm5fO2L5MYb1V025rgNyblyBWIbJ 58 | s3QnS+NPf1sNxKLSZT1Zr/jGXbCnMt4Y3F7Fne99WgjIMb217oTbSpGr/YKzW5ZB 59 | VG17RwnJm8kCAwEAAaMXMBUwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcN 60 | AQEFBQADgYEAfuHbEePejnuh8e0oAoH3UB4Om6Kijbr+WEdOXEX06CerOC1divcu 61 | /bm0AF5hSP7MYl+HjKhvwrZUYVPbdeEIRP2K3yT3f2Iv5TNrTvwGPm1JlRiwgvyC 62 | dW9pj9QYyBHjX5vTGaWi1cTKxnkVyM5LidJMVnC6FsuoqIw8p/NKA2M= 63 | -----END CERTIFICATE----- 64 | -------------------------------------------------------------------------------- /tests/ssl/FC4EE3437CF69DDC.pem: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | fc:4e:e3:43:7c:f6:9d:dc 6 | Signature Algorithm: sha1WithRSAEncryption 7 | Issuer: C=US, ST=Florida, O=Internet Widgits Pty Ltd, CN=ca.local 8 | Validity 9 | Not Before: Jan 16 05:36:53 2014 GMT 10 | Not After : Jan 16 05:36:53 2015 GMT 11 | Subject: C=US, ST=Florida, O=Internet Widgits Pty Ltd, CN=client.local 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | RSA Public Key: (2048 bit) 15 | Modulus (2048 bit): 16 | 00:ee:d9:a8:4a:bc:cb:54:e6:20:9f:80:e5:1f:53: 17 | f8:f9:12:56:b6:cd:b7:dc:00:c1:40:a0:0c:d5:10: 18 | 74:31:49:27:ed:20:97:41:17:99:53:d1:da:ef:c3: 19 | 25:f2:f7:db:d5:77:d2:00:88:d3:64:8a:ad:ce:f9: 20 | fa:ad:ca:8d:d8:d3:9b:e2:52:a5:5b:1f:f0:dc:13: 21 | 6d:fa:ac:89:1f:42:f7:29:1f:33:28:dd:c0:ce:cd: 22 | 0e:92:79:9a:0c:cc:d3:77:de:88:56:a5:89:2c:5b: 23 | 89:bf:1d:98:60:04:f6:d9:49:dc:e4:d7:ad:e0:a0: 24 | 21:a3:64:61:55:4c:e9:6d:db:e9:aa:82:31:8e:0c: 25 | 8f:c7:fc:fa:19:d7:30:cf:7c:4a:4d:4d:86:14:8f: 26 | ab:8d:a2:0e:9c:6c:e9:2d:28:72:37:9f:e7:d8:c0: 27 | a4:45:30:16:41:d8:60:ca:a3:9e:fb:db:44:c9:68: 28 | 63:58:92:39:54:52:82:99:55:59:20:1a:47:83:86: 29 | 21:0d:27:e5:09:e0:f4:ac:6e:d0:1d:03:09:f3:07: 30 | ea:b1:00:b0:2c:b3:2c:e0:86:66:31:45:eb:18:b4: 31 | 3e:ce:36:b3:12:41:48:be:43:61:98:e4:79:40:f4: 32 | f5:a8:04:56:49:47:07:22:ad:f7:b8:06:cc:27:f5: 33 | ec:67 34 | Exponent: 65537 (0x10001) 35 | X509v3 extensions: 36 | X509v3 Extended Key Usage: 37 | TLS Web Client Authentication 38 | Signature Algorithm: sha1WithRSAEncryption 39 | 6c:01:60:60:c8:e9:6d:30:84:84:19:c0:c1:74:07:c7:1a:a4: 40 | 41:05:5a:8b:ce:31:16:23:c2:f8:0d:05:b4:92:27:42:5b:4f: 41 | 84:b6:93:16:98:66:0c:68:2c:4f:5f:50:4b:8f:02:9d:df:7a: 42 | 7f:59:55:b0:98:e3:08:f0:f8:ef:13:cc:12:56:2d:39:0d:22: 43 | 8c:44:87:46:58:32:3f:d8:ce:3b:03:79:b4:59:06:45:8e:cd: 44 | 6f:06:ef:f9:75:b8:d6:5f:45:10:d1:a6:e4:4a:d3:a4:14:db: 45 | 2e:d3:0a:72:2f:3c:3f:6e:8a:30:09:6b:9a:69:f1:74:31:2a: 46 | 87:d9 47 | -----BEGIN CERTIFICATE----- 48 | MIICxzCCAjCgAwIBAgIJAPxO40N89p3cMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV 49 | BAYTAlVTMRAwDgYDVQQIEwdGbG9yaWRhMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRn 50 | aXRzIFB0eSBMdGQxETAPBgNVBAMTCGNhLmxvY2FsMB4XDTE0MDExNjA1MzY1M1oX 51 | DTE1MDExNjA1MzY1M1owWTELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0Zsb3JpZGEx 52 | ITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEVMBMGA1UEAxMMY2xp 53 | ZW50LmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7tmoSrzL 54 | VOYgn4DlH1P4+RJWts233ADBQKAM1RB0MUkn7SCXQReZU9Ha78Ml8vfb1XfSAIjT 55 | ZIqtzvn6rcqN2NOb4lKlWx/w3BNt+qyJH0L3KR8zKN3Azs0OknmaDMzTd96IVqWJ 56 | LFuJvx2YYAT22Unc5Net4KAho2RhVUzpbdvpqoIxjgyPx/z6Gdcwz3xKTU2GFI+r 57 | jaIOnGzpLShyN5/n2MCkRTAWQdhgyqOe+9tEyWhjWJI5VFKCmVVZIBpHg4YhDSfl 58 | CeD0rG7QHQMJ8wfqsQCwLLMs4IZmMUXrGLQ+zjazEkFIvkNhmOR5QPT1qARWSUcH 59 | Iq33uAbMJ/XsZwIDAQABoxcwFTATBgNVHSUEDDAKBggrBgEFBQcDAjANBgkqhkiG 60 | 9w0BAQUFAAOBgQBsAWBgyOltMISEGcDBdAfHGqRBBVqLzjEWI8L4DQW0kidCW0+E 61 | tpMWmGYMaCxPX1BLjwKd33p/WVWwmOMI8PjvE8wSVi05DSKMRIdGWDI/2M47A3m0 62 | WQZFjs1vBu/5dbjWX0UQ0abkStOkFNsu0wpyLzw/boowCWuaafF0MSqH2Q== 63 | -----END CERTIFICATE----- 64 | -------------------------------------------------------------------------------- /tests/wait.py: -------------------------------------------------------------------------------- 1 | from sys import exit 2 | 3 | from etcd.client import Client 4 | 5 | # ssl_ca_bundle_filepath='/home/dustin/development/python/etcd/tests/ssl/rootCA.pem') 6 | 7 | c = Client() 8 | 9 | #c = Client(host='etcd.local', 10 | # is_ssl=True, 11 | # ssl_client_cert_filepath='/home/dustin/development/python/etcd/tests/ssl/client.crt', 12 | # ssl_client_key_filepath='/home/dustin/development/python/etcd/tests/ssl/client.key') 13 | # ssl_client_cert_filepath='/home/dustin/development/python/etcd/tests/ssl/cert_2_newca/alien_client.crt', 14 | # ssl_client_key_filepath='/home/dustin/development/python/etcd/tests/ssl/cert_2_newca/alien_client.key') 15 | 16 | #machines = c.server.get_machines() 17 | #for machine in machines: 18 | # print(machine) 19 | # 20 | #print 21 | # 22 | #exit(0) 23 | 24 | r = c.node.get('/node_test/subkey1') 25 | print(r) 26 | 27 | r = c.node.wait('/node_test/subkey1') 28 | print(r) 29 | 30 | print(r.node.value) 31 | 32 | exit(0) 33 | 34 | print(c.server.get_version()) 35 | 36 | exit(0) 37 | #print(c.server.get_leader_url_prefix()) 38 | #for machine in c.server.get_machines().node.children: 39 | # print(machine.value) 40 | # 41 | #print(c.server.get_dashboard_url()) 42 | # 43 | #exit(0) 44 | 45 | #l = c.module.lock.get_lock('test_lock_2') 46 | #l.acquire(10) 47 | #l.renew(150) 48 | #l.release() 49 | 50 | #with c.module.lock.get_lock('test_lock_2', 10): 51 | # print("In lock.") 52 | 53 | #with c.module.lock.get_rlock('test_lock_2', 'host1', 10): 54 | # print("In lock 2.") 55 | 56 | #exit(0) 57 | 58 | #key = 'abc' 59 | #value = 'some_value' 60 | #c.module.leader.set_or_renew(key, value, 10) 61 | #c.module.leader.get(key) 62 | #c.module.leader.delete(key, value) 63 | # 64 | #exit(0) 65 | 66 | # TODO: Is a lock deleted implicitly after expiration, or is it just somehow deactivated? I tried one key with the index lock, and I subsequently used the same key for a value lock, and I got a 500. 67 | 68 | #r = c.lock.get_rlock('test_lock_3', 'proc3') 69 | #r.acquire(30) 70 | #r.release() 71 | # 72 | #r = c.lock.get_rlock('test_lock_3', 'proc3') 73 | #r.acquire(60) 74 | #r.release() 75 | # 76 | #r = c.lock.get_rlock('test_lock_3', 'proc4') 77 | #r.acquire(30) 78 | #r.release() 79 | 80 | #print("Active") 81 | #print(r.get_active_value()) 82 | 83 | #exit(0) 84 | 85 | #q = c.queue.get_queue('/queue_2302') 86 | # 87 | #value = 'value9999' 88 | #r = q.add(value) 89 | #print("Add " + value) 90 | #print(r) 91 | # 92 | #value = 'value1111' 93 | #r = q.add(value) 94 | #print("Add " + value) 95 | #print(r) 96 | # 97 | #print 98 | # 99 | #r = q.list() 100 | #print(r) 101 | # 102 | #print 103 | # 104 | #for child in r.node.children: 105 | # print(child) 106 | # print(child.value) 107 | # 108 | #print 109 | 110 | #r = c.directory.delete_recursive('/queue_2302') 111 | #print(r) 112 | 113 | #exit(0) 114 | 115 | r = c.node.set('/test_2056/val1', 5, ttl=60) 116 | print(r) 117 | 118 | exit(0) 119 | 120 | print 121 | 122 | r = c.node.set('/test_2056/val2', 10) 123 | print(r) 124 | 125 | print 126 | 127 | r = c.node.set('/test_2056/dir1/val11', 20) 128 | print(r) 129 | 130 | print 131 | 132 | r = c.node.get('/test_2056/val1') 133 | print(r) 134 | #print(r.node.value) 135 | 136 | print 137 | 138 | #r = c.node.get('/test_2056', recursive=False) 139 | #print(r) 140 | 141 | r = c.node.get('/test_2056', recursive=True) 142 | print(r) 143 | 144 | for node in r.node.children: 145 | print("CHILD: %s" % (node)) 146 | 147 | exit(0) 148 | 149 | #print("Collection:") 150 | #for node in r.node.children: 151 | # print(node) 152 | 153 | #r = c.node.delete('/test_2056/val1') 154 | #print(r) 155 | 156 | #r = c.node.delete('/test_2056/val2') 157 | #print(r) 158 | 159 | #r = c.directory.delete('/test_2056') 160 | #print(r) 161 | 162 | #print 163 | 164 | #c.node.create_only('/test_2056/val3', 5) 165 | #c.node.update_only('/test_2056/val1', 10) 166 | #c.node.update_if_index('/test_2056/val1', 15, r.node.created_index) 167 | #c.node.update_if_value('/test_2056/val1', 20, 5) 168 | 169 | #r = c.node.compare_and_swap('/test_2056/val1', 30, current_value=5, prev_exists=True) 170 | #print(r) 171 | 172 | #r = c.node.get('/test_2056/val1') 173 | #print(r.node.value) 174 | 175 | r = c.directory.create('/test_2056/new_dir', ttl=60) 176 | print(r) 177 | 178 | print 179 | 180 | r = c.directory.delete_recursive('/test_2056') 181 | print(r) 182 | -------------------------------------------------------------------------------- /etcd/stat_ops.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import re 3 | 4 | from collections import namedtuple 5 | from datetime import timedelta, datetime 6 | 7 | from etcd.common_ops import CommonOps 8 | from etcd.response import ResponseV2 9 | 10 | 11 | class StatOps(CommonOps): 12 | """Functions that query the server for statistics information.""" 13 | 14 | def get_leader_stats(self): 15 | """Returns leader and follower information. 16 | 17 | :returns: Tuple of leader name and follower dictionary 18 | :rtype: namedtuple 19 | """ 20 | 21 | r = self.client.send(2, 'get', '/stats/leader', return_raw=True) 22 | data = r.json() 23 | 24 | F = namedtuple('LStatFollower', ['counts', 'latency']) 25 | C = namedtuple('LStatCounts', ['fail', 'success']) 26 | L = namedtuple('LStatLatency', 27 | ['average', 'current', 'maximum', 'minimum', 28 | 'standard_deviation']) 29 | 30 | followers = {} 31 | for name, block in data['followers'].iteritems(): 32 | counts_raw = block['counts'] 33 | counts = C(fail=counts_raw['fail'], 34 | success=counts_raw['success']) 35 | 36 | latency_raw = block['latency'] 37 | latency = L(average=latency_raw['average'], 38 | current=latency_raw['current'], 39 | maximum=latency_raw['maximum'], 40 | minimum=latency_raw['minimum'], 41 | standard_deviation=latency_raw['standardDeviation']) 42 | 43 | followers[name] = F(counts=counts, latency=latency) 44 | 45 | return (data['leader'], followers) 46 | 47 | def get_self_stats(self): 48 | """Returns stats regarding the current node. 49 | 50 | :returns: Statistics data for host 51 | :rtype: namedtuple 52 | """ 53 | 54 | r = self.client.send(2, 'get', '/stats/self', return_raw=True) 55 | data = r.json() 56 | 57 | S = namedtuple('SStat', ['leader_info', 'name', 58 | 'recv_append_request_cnt', 59 | 'send_append_request_cnt', 60 | 'send_bandwidth_rate', 61 | 'send_pkg_rate', 'start_time', 'state']) 62 | 63 | L = namedtuple('SStatLeader', ['leader', 'uptime']) 64 | 65 | leader_info_raw = data['leaderInfo'] 66 | 67 | hours = 0 68 | minutes = 0 69 | seconds = 0 70 | 71 | match = re.match('([0-9]+)h([0-9]+)m([0-9]+\.[0-9]+)s', 72 | leader_info_raw['uptime']) 73 | 74 | if match is not None: 75 | hours = int(match.group(1)) 76 | minutes = int(match.group(2)) 77 | seconds = float(match.group(3)) 78 | else: 79 | match = re.match('([0-9]+)m([0-9]+\.[0-9]+)s', 80 | leader_info_raw['uptime']) 81 | 82 | if match is not None: 83 | minutes = int(match.group(1)) 84 | seconds = float(match.group(2)) 85 | else: 86 | match = re.match('([0-9]+\.[0-9]+)s', 87 | leader_info_raw['uptime']) 88 | 89 | if match is None: 90 | raise ValueError("Could not understand leader-uptime value: %s" % 91 | (leader_info_raw['uptime'])) 92 | 93 | seconds = float(match.group(1)) 94 | 95 | uptime = hours * 3600 + minutes * 60 + seconds 96 | leader_info = L(leader=leader_info_raw['leader'], 97 | uptime=timedelta(seconds=uptime)) 98 | 99 | start_time_raw = data['startTime'] 100 | pivot = start_time_raw.rfind('.') 101 | 102 | # TODO(dustin): At this time, we won't worry about the timezone component, and 103 | # will assume that the client is operating in the same zone as 104 | # the cluster. 105 | start_time = datetime.strptime(start_time_raw[:pivot], 106 | '%Y-%m-%dT%H:%M:%S') 107 | 108 | return S(leader_info=leader_info, name=data['name'], 109 | recv_append_request_cnt=data['recvAppendRequestCnt'], 110 | send_append_request_cnt=data['sendAppendRequestCnt'], 111 | send_bandwidth_rate=data.get('sendBandwidthRate'), 112 | send_pkg_rate=data.get('sendPkgRate'), 113 | start_time=start_time, state=data['state']) 114 | 115 | -------------------------------------------------------------------------------- /etcd/common_ops.py: -------------------------------------------------------------------------------- 1 | from requests.exceptions import HTTPError, ChunkedEncodingError 2 | from requests.status_codes import codes 3 | 4 | from etcd.exceptions import EtcdPreconditionException, EtcdEmptyResponseError,\ 5 | EtcdWaitFaultException, translate_exceptions 6 | 7 | 8 | class CommonOps(object): 9 | """Base-class of 'ops' modules. 10 | 11 | :param client: Client instance. 12 | :type client: :class:`etcd.client.Client` 13 | """ 14 | 15 | def __init__(self, client): 16 | self.__client = client 17 | 18 | def get_text(self, reason, path, version=2): 19 | """Execute a request that will return flat text. 20 | 21 | :param reason: Brief phrase describing the request 22 | :param path: URL path 23 | :param version: API version 24 | 25 | :type reason: string 26 | :type path: string 27 | :type version: int 28 | 29 | :returns: Response text 30 | :rtype: string 31 | """ 32 | 33 | if version is not None: 34 | url = ('%s/v%d%s' % (self.client.prefix, version, path)) 35 | else: 36 | url = ('%s%s' % (self.client.prefix, path)) 37 | 38 | self.client.debug("TEXT URL (%s) = [%s]" % (reason, url)) 39 | 40 | r = self.client.session.get(url) 41 | r.raise_for_status() 42 | 43 | return r.text 44 | 45 | def validate_path(self, path): 46 | """Validate the key that we were given. 47 | 48 | :param path: Key 49 | :type path: string 50 | 51 | :raises: ValueError 52 | """ 53 | 54 | if path[0] != '/': 55 | raise ValueError("Path [%s] should've been absolute." % (path,)) 56 | 57 | def get_fq_node_path(self, path): 58 | """Return the full path of the given key. 59 | 60 | :param path: Key 61 | :type path: string 62 | """ 63 | 64 | self.validate_path(path) 65 | 66 | return ('/keys' + path) 67 | 68 | def compare_and_delete(self, path, is_dir, current_value=None, 69 | current_index=None, is_recursive=None): 70 | """The base compare-and-delete function for atomic deletes. A 71 | combination of criteria may be used if necessary. 72 | 73 | :param path: Key 74 | :type path: string 75 | 76 | :param is_dir: If the node is a directory 77 | :type is_dir: bool 78 | 79 | :param current_value: Current value to check 80 | :type current_value: string or None 81 | 82 | :param current_index: Current index to check 83 | :type current_index: int or None 84 | 85 | :returns: Response object 86 | :rtype: :class:`etcd.response.ResponseV2` 87 | """ 88 | 89 | fq_path = self.get_fq_node_path(path) 90 | 91 | parameters = {} 92 | data = { } 93 | 94 | if current_value is not None: 95 | data['prevValue'] = current_value 96 | 97 | if current_index is not None: 98 | data['prevIndex'] = current_index 99 | 100 | if not data: 101 | raise ValueError("CAD requires a comparison argument.") 102 | 103 | if is_recursive is not None: 104 | # Per the discussion, "-r will also imply -d". 105 | is_dir = True 106 | 107 | if is_recursive is True: 108 | parameters['recursive'] = 'true' 109 | 110 | parameters['dir'] = 'true' if is_dir is True else 'false' 111 | 112 | 113 | try: 114 | return self.client.send(2, 'delete', fq_path, 115 | parameters=parameters, 116 | data=data) 117 | except HTTPError as e: 118 | if e.response.status_code == codes.precondition_failed: 119 | raise EtcdPreconditionException() 120 | 121 | raise 122 | 123 | @translate_exceptions 124 | def wait(self, path, recursive=False, force_consistent=False): 125 | """Long-poll on the given path until it changes. 126 | 127 | :param path: Node key 128 | :type path: string 129 | 130 | :param recursive: Wait on any change in the given directory or any of 131 | its descendants. 132 | :type recursive: bool 133 | 134 | :returns: Response object 135 | :rtype: :class:`etcd.response.ResponseV2` or None 136 | 137 | :raises: KeyError 138 | """ 139 | 140 | fq_path = self.get_fq_node_path(path) 141 | 142 | parameters = { 'wait': 'true' } 143 | 144 | if recursive is True: 145 | parameters['recursive'] = 'true' 146 | 147 | if force_consistent is True: 148 | parameters['consistent'] = 'true' 149 | 150 | try: 151 | return self.client.send(2, 'get', fq_path, parameters=parameters) 152 | except ChunkedEncodingError: 153 | # TODO(dustin): We need to document why we would get this. We don't remember 154 | # the context. 155 | pass 156 | except EtcdEmptyResponseError: 157 | # TODO(dustin): This will happen when we timeout, and should be considered a 158 | # bug as it does not constitute valid JSON. 159 | # 160 | # https://github.com/coreos/etcd/issues/1120 161 | pass 162 | 163 | raise EtcdWaitFaultException() 164 | 165 | @property 166 | def client(self): 167 | return self.__client 168 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | from sys import exit 2 | from os.path import join 3 | 4 | from etcd.client import Client 5 | from etcd.exceptions import EtcdAlreadyExistsException 6 | 7 | # ssl_ca_bundle_filepath='/home/dustin/development/python/etcd/tests/ssl/rootCA.pem') 8 | 9 | #c = Client() 10 | 11 | ssl_root = '/home/dustin/development/python/etcd/tests/ssl' 12 | 13 | c = Client(host='127.0.0.1',#etcd.local', 14 | # is_ssl=True, 15 | # ssl_ca_bundle_filepath=join(ssl_root, 'demoCA', 'cacert.pem'), 16 | # ssl_client_cert_filepath=join(ssl_root, 'client.crt'), 17 | # ssl_client_key_filepath=join(ssl_root, 'client.key'), 18 | # ssl_client_cert_filepath='/home/dustin/development/python/etcd/tests/ssl/cert_2_newca/alien_client.crt', 19 | # ssl_client_key_filepath='/home/dustin/development/python/etcd/tests/ssl/cert_2_newca/alien_client.key') 20 | ) 21 | 22 | #machines = c.server.get_machines() 23 | #for machine in machines: 24 | # print(machine) 25 | # 26 | #print 27 | # 28 | #exit(0) 29 | 30 | print("LEADER") 31 | 32 | s = c.stat.get_leader_stats() 33 | print(s) 34 | 35 | print("SELF") 36 | 37 | s = c.stat.get_self_stats() 38 | print(s) 39 | 40 | exit(0) 41 | 42 | r = c.node.set('/node_test/testkey', 'some_existing_value') 43 | print(r.node) 44 | print(r.prev_node) 45 | 46 | exit(0) 47 | 48 | try: 49 | r = c.directory.create('/dir_test/bb') 50 | except EtcdAlreadyExistsException: 51 | c.directory.delete('/dir_test/bb') 52 | r = c.directory.create('/dir_test/bb') 53 | 54 | print(r) 55 | 56 | #r = c.directory.create('/test_2056/new_dir')#, ttl=60) 57 | 58 | r = c.directory.delete_if_index('/dir_test/bb', r.node.modified_index + 999) 59 | print(r) 60 | 61 | exit(0) 62 | 63 | r = c.node.set('/node_test/testkey', 'some_existing_value') 64 | print(r) 65 | 66 | #r = c.node.delete_if_value('/node_test/testkey', 'some_existing_value') 67 | #print(r) 68 | 69 | r = c.node.delete_if_index('/node_test/testkey', r.node.modified_index) 70 | print(r) 71 | 72 | exit(0) 73 | 74 | r = c.node.set('/node_test/subkey1', 20) 75 | 76 | exit(0) 77 | 78 | print(c.server.get_version()) 79 | 80 | exit(0) 81 | #print(c.server.get_leader_url_prefix()) 82 | #for machine in c.server.get_machines().node.children: 83 | # print(machine.value) 84 | # 85 | #print(c.server.get_dashboard_url()) 86 | # 87 | #exit(0) 88 | 89 | #l = c.module.lock.get_lock('test_lock_2') 90 | #l.acquire(10) 91 | #l.renew(150) 92 | #l.release() 93 | 94 | #with c.module.lock.get_lock('test_lock_2', 10): 95 | # print("In lock.") 96 | 97 | #with c.module.lock.get_rlock('test_lock_2', 'host1', 10): 98 | # print("In lock 2.") 99 | 100 | #exit(0) 101 | 102 | #key = 'abc' 103 | #value = 'some_value' 104 | #c.module.leader.set_or_renew(key, value, 10) 105 | #c.module.leader.get(key) 106 | #c.module.leader.delete(key, value) 107 | # 108 | #exit(0) 109 | 110 | # TODO: Is a lock deleted implicitly after expiration, or is it just somehow deactivated? I tried one key with the index lock, and I subsequently used the same key for a value lock, and I got a 500. 111 | 112 | #r = c.lock.get_rlock('test_lock_3', 'proc3') 113 | #r.acquire(30) 114 | #r.release() 115 | # 116 | #r = c.lock.get_rlock('test_lock_3', 'proc3') 117 | #r.acquire(60) 118 | #r.release() 119 | # 120 | #r = c.lock.get_rlock('test_lock_3', 'proc4') 121 | #r.acquire(30) 122 | #r.release() 123 | 124 | #print("Active") 125 | #print(r.get_active_value()) 126 | 127 | #exit(0) 128 | 129 | #q = c.queue.get_queue('/queue_2302') 130 | # 131 | #value = 'value9999' 132 | #r = q.add(value) 133 | #print("Add " + value) 134 | #print(r) 135 | # 136 | #value = 'value1111' 137 | #r = q.add(value) 138 | #print("Add " + value) 139 | #print(r) 140 | # 141 | #print 142 | # 143 | #r = q.list() 144 | #print(r) 145 | # 146 | #print 147 | # 148 | #for child in r.node.children: 149 | # print(child) 150 | # print(child.value) 151 | # 152 | #print 153 | 154 | #r = c.directory.delete_recursive('/queue_2302') 155 | #print(r) 156 | 157 | #exit(0) 158 | 159 | r = c.node.set('/test_2056/val1', 5, ttl=60) 160 | print(r) 161 | 162 | exit(0) 163 | 164 | print 165 | 166 | r = c.node.set('/test_2056/val2', 10) 167 | print(r) 168 | 169 | print 170 | 171 | r = c.node.set('/test_2056/dir1/val11', 20) 172 | print(r) 173 | 174 | print 175 | 176 | r = c.node.get('/test_2056/val1') 177 | print(r) 178 | #print(r.node.value) 179 | 180 | print 181 | 182 | #r = c.node.get('/test_2056', recursive=False) 183 | #print(r) 184 | 185 | r = c.node.get('/test_2056', recursive=True) 186 | print(r) 187 | 188 | for node in r.node.children: 189 | print("CHILD: %s" % (node)) 190 | 191 | exit(0) 192 | 193 | #print("Collection:") 194 | #for node in r.node.children: 195 | # print(node) 196 | 197 | #r = c.node.delete('/test_2056/val1') 198 | #print(r) 199 | 200 | #r = c.node.delete('/test_2056/val2') 201 | #print(r) 202 | 203 | #r = c.directory.delete('/test_2056') 204 | #print(r) 205 | 206 | #print 207 | 208 | #c.node.create_only('/test_2056/val3', 5) 209 | #c.node.update_only('/test_2056/val1', 10) 210 | #c.node.update_if_index('/test_2056/val1', 15, r.node.created_index) 211 | #c.node.update_if_value('/test_2056/val1', 20, 5) 212 | 213 | #r = c.node.compare_and_swap('/test_2056/val1', 30, current_value=5, prev_exists=True) 214 | #print(r) 215 | 216 | #r = c.node.get('/test_2056/val1') 217 | #print(r.node.value) 218 | 219 | r = c.directory.create('/test_2056/new_dir', ttl=60) 220 | print(r) 221 | 222 | print 223 | 224 | r = c.directory.delete_recursive('/test_2056') 225 | print(r) 226 | -------------------------------------------------------------------------------- /dev/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | 3 | from sys import exit, path 4 | path.insert(0, '..') 5 | 6 | import os 7 | from os.path import join 8 | 9 | os.environ['DEBUG'] = '1' 10 | 11 | import etcd.log 12 | 13 | from etcd.client import Client 14 | from etcd.exceptions import EtcdAlreadyExistsException 15 | 16 | #os.environ['PEC_SCHEME'] = 'https' 17 | #os.environ['PEC_HOSTNAME'] = 'server.etcd' 18 | #os.environ['PEC_SSL_CA_BUNDLE_FILEPATH'] = 'ssl/ca.crt.pem' 19 | 20 | # ssl_ca_bundle_filepath='/home/dustin/development/python/etcd/tests/ssl/rootCA.pem') 21 | 22 | #c = Client() 23 | 24 | ssl_root = '/Users/dustin/development/python/etcd/dev/ssl' 25 | 26 | c = Client(host='server.etcd', 27 | is_ssl=True, 28 | ssl_ca_bundle_filepath=join(ssl_root, 'ca.crt.pem'), 29 | ssl_client_cert_filepath=join(ssl_root, 'client.crt.pem'), 30 | ssl_client_key_filepath=join(ssl_root, 'client.key.pem')) 31 | 32 | #machines = c.server.get_machines() 33 | #for machine in machines: 34 | # print(machine) 35 | # 36 | #print 37 | # 38 | #exit(0) 39 | 40 | #print("LEADER") 41 | 42 | #s = c.stat.get_leader_stats() 43 | #print(s) 44 | 45 | #print("SELF") 46 | 47 | s = c.stat.get_self_stats() 48 | print(s) 49 | 50 | exit(0) 51 | 52 | r = c.node.set('/node_test/testkey', 'some_existing_value') 53 | print(r.node) 54 | print(r.prev_node) 55 | 56 | exit(0) 57 | 58 | try: 59 | r = c.directory.create('/dir_test/bb') 60 | except EtcdAlreadyExistsException: 61 | c.directory.delete('/dir_test/bb') 62 | r = c.directory.create('/dir_test/bb') 63 | 64 | print(r) 65 | 66 | #r = c.directory.create('/test_2056/new_dir')#, ttl=60) 67 | 68 | r = c.directory.delete_if_index('/dir_test/bb', r.node.modified_index + 999) 69 | print(r) 70 | 71 | exit(0) 72 | 73 | r = c.node.set('/node_test/testkey', 'some_existing_value') 74 | print(r) 75 | 76 | #r = c.node.delete_if_value('/node_test/testkey', 'some_existing_value') 77 | #print(r) 78 | 79 | r = c.node.delete_if_index('/node_test/testkey', r.node.modified_index) 80 | print(r) 81 | 82 | exit(0) 83 | 84 | r = c.node.set('/node_test/subkey1', 20) 85 | 86 | exit(0) 87 | 88 | print(c.server.get_version()) 89 | 90 | exit(0) 91 | #print(c.server.get_leader_url_prefix()) 92 | #for machine in c.server.get_machines().node.children: 93 | # print(machine.value) 94 | # 95 | #print(c.server.get_dashboard_url()) 96 | # 97 | #exit(0) 98 | 99 | #l = c.module.lock.get_lock('test_lock_2') 100 | #l.acquire(10) 101 | #l.renew(150) 102 | #l.release() 103 | 104 | #with c.module.lock.get_lock('test_lock_2', 10): 105 | # print("In lock.") 106 | 107 | #with c.module.lock.get_rlock('test_lock_2', 'host1', 10): 108 | # print("In lock 2.") 109 | 110 | #exit(0) 111 | 112 | #key = 'abc' 113 | #value = 'some_value' 114 | #c.module.leader.set_or_renew(key, value, 10) 115 | #c.module.leader.get(key) 116 | #c.module.leader.delete(key, value) 117 | # 118 | #exit(0) 119 | 120 | # TODO: Is a lock deleted implicitly after expiration, or is it just somehow deactivated? I tried one key with the index lock, and I subsequently used the same key for a value lock, and I got a 500. 121 | 122 | #r = c.lock.get_rlock('test_lock_3', 'proc3') 123 | #r.acquire(30) 124 | #r.release() 125 | # 126 | #r = c.lock.get_rlock('test_lock_3', 'proc3') 127 | #r.acquire(60) 128 | #r.release() 129 | # 130 | #r = c.lock.get_rlock('test_lock_3', 'proc4') 131 | #r.acquire(30) 132 | #r.release() 133 | 134 | #print("Active") 135 | #print(r.get_active_value()) 136 | 137 | #exit(0) 138 | 139 | #q = c.queue.get_queue('/queue_2302') 140 | # 141 | #value = 'value9999' 142 | #r = q.add(value) 143 | #print("Add " + value) 144 | #print(r) 145 | # 146 | #value = 'value1111' 147 | #r = q.add(value) 148 | #print("Add " + value) 149 | #print(r) 150 | # 151 | #print 152 | # 153 | #r = q.list() 154 | #print(r) 155 | # 156 | #print 157 | # 158 | #for child in r.node.children: 159 | # print(child) 160 | # print(child.value) 161 | # 162 | #print 163 | 164 | #r = c.directory.delete_recursive('/queue_2302') 165 | #print(r) 166 | 167 | #exit(0) 168 | 169 | r = c.node.set('/test_2056/val1', 5, ttl=60) 170 | print(r) 171 | 172 | exit(0) 173 | 174 | print 175 | 176 | r = c.node.set('/test_2056/val2', 10) 177 | print(r) 178 | 179 | print 180 | 181 | r = c.node.set('/test_2056/dir1/val11', 20) 182 | print(r) 183 | 184 | print 185 | 186 | r = c.node.get('/test_2056/val1') 187 | print(r) 188 | #print(r.node.value) 189 | 190 | print 191 | 192 | #r = c.node.get('/test_2056', recursive=False) 193 | #print(r) 194 | 195 | r = c.node.get('/test_2056', recursive=True) 196 | print(r) 197 | 198 | for node in r.node.children: 199 | print("CHILD: %s" % (node)) 200 | 201 | exit(0) 202 | 203 | #print("Collection:") 204 | #for node in r.node.children: 205 | # print(node) 206 | 207 | #r = c.node.delete('/test_2056/val1') 208 | #print(r) 209 | 210 | #r = c.node.delete('/test_2056/val2') 211 | #print(r) 212 | 213 | #r = c.directory.delete('/test_2056') 214 | #print(r) 215 | 216 | #print 217 | 218 | #c.node.create_only('/test_2056/val3', 5) 219 | #c.node.update_only('/test_2056/val1', 10) 220 | #c.node.update_if_index('/test_2056/val1', 15, r.node.created_index) 221 | #c.node.update_if_value('/test_2056/val1', 20, 5) 222 | 223 | #r = c.node.compare_and_swap('/test_2056/val1', 30, current_value=5, prev_exists=True) 224 | #print(r) 225 | 226 | #r = c.node.get('/test_2056/val1') 227 | #print(r.node.value) 228 | 229 | r = c.directory.create('/test_2056/new_dir', ttl=60) 230 | print(r) 231 | 232 | print 233 | 234 | r = c.directory.delete_recursive('/test_2056') 235 | print(r) 236 | -------------------------------------------------------------------------------- /etcd/directory_ops.py: -------------------------------------------------------------------------------- 1 | from requests.exceptions import HTTPError 2 | from requests.status_codes import codes 3 | 4 | from etcd.exceptions import EtcdAlreadyExistsException, translate_exceptions 5 | from etcd.common_ops import CommonOps 6 | 7 | # TODO(dustin): We may need a directory-specific version of 8 | # translate_exceptions. We'll see. 9 | 10 | 11 | class DirectoryOps(CommonOps): 12 | """Functions specific to directory management.""" 13 | 14 | @translate_exceptions 15 | def list(self, path, recursive=False, force_consistent=False, force_quorum=False): 16 | """Return a list of the nodes. 17 | 18 | :param recursive: Return all children, and children-of-children. 19 | :type recursive: bool 20 | 21 | :param force_consistent: Only interact with the current leader so 22 | propagation is not a concern. 23 | :type force_consistent: bool 24 | 25 | :returns: Response object 26 | :rtype: :class:`etcd.response.ResponseV2` 27 | """ 28 | 29 | fq_path = self.get_fq_node_path(path) 30 | 31 | parameters = {} 32 | if recursive is True: 33 | parameters['recursive'] = 'true' 34 | 35 | if force_consistent is True: 36 | parameters['consistent'] = 'true' 37 | 38 | if force_quorum is True: 39 | parameters['quorum'] = 'true' 40 | 41 | return self.client.send(2, 'get', fq_path, parameters=parameters) 42 | 43 | @translate_exceptions 44 | def create(self, path, ttl=None): 45 | """A normal node-set will implicitly create directories on the way to 46 | setting a value. This call exists for when you'd like to -explicitly- 47 | create one. 48 | 49 | We implicitly fail if the directory already exists. 50 | 51 | :param path: Key 52 | :type path: string 53 | 54 | :param ttl: Time until removed 55 | :type ttl: int or None 56 | 57 | :returns: Response object 58 | :rtype: :class:`etcd.response.ResponseV2` 59 | :raises: EtcdAlreadyExistsException 60 | """ 61 | 62 | fq_path = self.get_fq_node_path(path) 63 | data = { 'dir': 'true' } 64 | 65 | if ttl is not None: 66 | data['ttl'] = ttl 67 | 68 | try: 69 | return self.client.send(2, 'put', fq_path, data=data) 70 | except HTTPError as e: 71 | if e.response.status_code == codes.forbidden: 72 | try: 73 | j = e.response.json() 74 | except ValueError: 75 | pass 76 | else: 77 | # TODO(dustin): Complain about this error message. 78 | # "message" == "Not a file" 79 | if j['errorCode'] == 102: 80 | raise EtcdAlreadyExistsException(path) 81 | 82 | raise 83 | 84 | @translate_exceptions 85 | def delete(self, path, current_value=None, current_index=None): 86 | """Delete the given directory. It must be empty. 87 | 88 | :param path: Key 89 | :type path: string 90 | 91 | :param current_index: Current index to check 92 | :type current_index: int or None 93 | 94 | :returns: Response object 95 | :rtype: :class:`etcd.response.ResponseV2` 96 | """ 97 | 98 | if current_index is not None: 99 | return self.compare_and_delete(path, is_dir=True, 100 | current_index=current_index) 101 | 102 | fq_path = self.get_fq_node_path(path) 103 | 104 | parameters = { 'dir': 'true' } 105 | return self.client.send(2, 'delete', fq_path, parameters=parameters) 106 | 107 | @translate_exceptions 108 | def delete_if_index(self, path, current_index): 109 | """Only delete the given directory if the node is at the given index. 110 | It must be empty. 111 | 112 | :param path: Key 113 | :type path: string 114 | 115 | :param current_index: Current index to check 116 | :type current_index: int or None 117 | 118 | :returns: Response object 119 | :rtype: :class:`etcd.response.ResponseV2` 120 | """ 121 | 122 | return self.compare_and_delete(path, is_dir=True, 123 | current_index=current_index) 124 | 125 | @translate_exceptions 126 | def delete_recursive(self, path, current_index=None): 127 | """Delete the given directory, along with any children. 128 | 129 | :param path: Key 130 | :type path: string 131 | 132 | :param current_index: Current index to check 133 | :type current_index: int or None 134 | 135 | :returns: Response object 136 | :rtype: :class:`etcd.response.ResponseV2` 137 | """ 138 | 139 | if current_index is not None: 140 | return self.compare_and_delete(path, is_recursive=True, 141 | current_index=current_index) 142 | 143 | fq_path = self.get_fq_node_path(path) 144 | 145 | parameters = { 'dir': 'true', 'recursive': 'true' } 146 | return self.client.send(2, 'delete', fq_path, parameters=parameters) 147 | 148 | @translate_exceptions 149 | def delete_recursive_if_index(self, path, current_index): 150 | """Only delete the given directory (and its children) if the node is at 151 | the given index. 152 | 153 | :param path: Key 154 | :type path: string 155 | 156 | :param current_index: Current index to check 157 | :type current_index: int or None 158 | 159 | :returns: Response object 160 | :rtype: :class:`etcd.response.ResponseV2` 161 | """ 162 | 163 | return self.compare_and_delete(path, is_recursive=True, 164 | current_index=current_index) 165 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PythonetcdClient.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PythonetcdClient.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PythonetcdClient" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PythonetcdClient" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PythonetcdClient.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PythonetcdClient.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /etcd/response.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | import simplejson 3 | 4 | import etcd.exceptions 5 | 6 | from collections import namedtuple 7 | from os.path import basename 8 | from pytz import timezone 9 | from datetime import datetime, timedelta 10 | 11 | A__PREVNODE = '_(pnode)' 12 | 13 | A_GET = 'get' 14 | A_SET = 'set' 15 | A_UPDATE = 'update' 16 | A_CREATE = 'create' 17 | A_DELETE = 'delete' 18 | A_CAS = 'compareAndSwap' 19 | A_CAD = 'compareAndDelete' 20 | 21 | def _build_node_object(action, node): 22 | if 'dir' not in node: 23 | node['dir'] = False 24 | 25 | if node['dir'] == True: 26 | if action in (A_DELETE, A_CAD): 27 | return ResponseV2DeletedDirectoryNode(action, node) 28 | # TODO: Specifically, what actions can happen for a DIRECTORY? 29 | else: 30 | return ResponseV2AliveDirectoryNode(action, node) 31 | else: 32 | if action in (A_DELETE, A_CAD): 33 | return ResponseV2DeletedNode(action, node) 34 | # TODO: Specifically, what actions can happen for a non-directory? 35 | else: 36 | return ResponseV2AliveNode(action, node) 37 | 38 | 39 | class ResponseV2BasicNode(object): 40 | """Base-class representing all nodes: deleted, alive, or a collection. 41 | 42 | :param action: Action type 43 | :param node: Node dictionary 44 | 45 | :type action: string 46 | :type node: dictionary 47 | 48 | :returns: Response object 49 | :rtype: etcd.response.ResponseV2 50 | """ 51 | 52 | def __init__(self, action, node): 53 | self.action = action 54 | self.raw_node = node 55 | self.created_index = node['createdIndex'] 56 | self.modified_index = node['modifiedIndex'] 57 | self.key = node['key'] 58 | 59 | # This is as involved as we'll get with whether nodes are hidden. Any 60 | # more, and we'd have to manage and, therefore, translate every key 61 | # reported by the server. 62 | self.is_hidden = basename(node['key']).startswith('_') 63 | 64 | # >> Process TTL-related stuff. 65 | 66 | try: 67 | expiration = node['expiration'] 68 | except KeyError: 69 | self.expiration = None 70 | self.ttl = None 71 | self.ttl_phrase = 'None' 72 | else: 73 | self.ttl = node['ttl'] 74 | 75 | first_part = expiration[:19] 76 | naive_dt = datetime.strptime(first_part, '%Y-%m-%dT%H:%M:%S') 77 | tz_offset_hours = int(expiration[-5:-3]) 78 | tz_offset_minutes = int(expiration[-2:]) 79 | 80 | tz_offset = timedelta(seconds=(tz_offset_hours * 60 * 60 + 81 | tz_offset_minutes * 60)) 82 | 83 | self.expiration = (naive_dt + tz_offset).replace(tzinfo=pytz.UTC) 84 | self.ttl_phrase = ('%d: %s' % (self.ttl, self.expiration)) 85 | 86 | # << 87 | 88 | try: 89 | self.initialize(node) 90 | except NotImplementedError: 91 | pass 92 | 93 | def initialize(self, node): 94 | """This function acts as the constructor for subclasses. 95 | 96 | :param node: Node dictionary 97 | :type node: dictionary 98 | """ 99 | 100 | raise NotImplementedError() 101 | 102 | def __repr__(self): 103 | return ('' % 105 | (self.__class__.__name__, self.action, self.key, 106 | self.is_hidden, self.is_deleted, self.is_directory, 107 | self.is_collection, self.ttl_phrase, self.created_index, 108 | self.modified_index)) 109 | 110 | @property 111 | def is_deleted(self): 112 | """Is the node deleted? 113 | 114 | :rtype: bool 115 | """ 116 | 117 | return False 118 | 119 | @property 120 | def is_directory(self): 121 | """Is the node a directory? 122 | 123 | :rtype: bool 124 | """ 125 | 126 | return False 127 | 128 | @property 129 | def is_collection(self): 130 | """If the node is a directory, do we have the collection of children 131 | nodes? 132 | 133 | :rtype: bool 134 | """ 135 | 136 | return False 137 | 138 | 139 | class ResponseV2AliveNode(ResponseV2BasicNode): 140 | "Base-class representing a single, non-deleted node." 141 | 142 | def initialize(self, node): 143 | self.value = node['value'] 144 | 145 | 146 | class ResponseV2DeletedNode(ResponseV2BasicNode): 147 | "Represents a single, deleted node." 148 | 149 | @property 150 | def is_deleted(self): 151 | return True 152 | 153 | 154 | class ResponseV2DirectoryNode(ResponseV2BasicNode): 155 | """A base-class representing a single directory node.""" 156 | 157 | @property 158 | def is_directory(self): 159 | return True 160 | 161 | 162 | class ResponseV2AliveDirectoryNode(ResponseV2DirectoryNode): 163 | """Represents a directory node, which may also be accompanied by children 164 | that can be enumerated. 165 | """ 166 | 167 | def initialize(self, node): 168 | if node.get('dir', False) is True: 169 | self.__is_collection = True 170 | self.__raw_nodes = node.get('nodes', []) 171 | else: 172 | self.__is_collection = False 173 | self.__raw_nodes = None 174 | 175 | def __repr__(self): 176 | node_count_phrase = (len(self.__raw_nodes) \ 177 | if self.__raw_nodes is not None \ 178 | else '') 179 | 180 | return ('' % 182 | (self.__class__.__name__, self.action, self.key, 183 | self.is_hidden, self.ttl_phrase, self.is_directory, 184 | self.__is_collection, node_count_phrase, 185 | self.created_index, self.modified_index)) 186 | 187 | @property 188 | def is_collection(self): 189 | return self.__is_collection 190 | 191 | @property 192 | def children(self): 193 | if self.__is_collection is False: 194 | raise ValueError("This directory node is not a collection.") 195 | 196 | # TODO: Cache the new objects for the benefit of repeated enumerations? 197 | for node in self.__raw_nodes: 198 | yield _build_node_object(self.action, node) 199 | 200 | 201 | class ResponseV2DeletedDirectoryNode(ResponseV2DirectoryNode): 202 | """Represents a single DIRECTORY node either appearing in isolation or 203 | among siblings. 204 | """ 205 | 206 | @property 207 | def is_deleted(self): 208 | return True 209 | 210 | 211 | class ResponseV2(object): 212 | """An object that describes a response for every V2 request. 213 | 214 | :param response: Raw Requests response object 215 | :param request_verb: Request verb ('get', post', 'put', etc..) 216 | :param request_path: Node key 217 | 218 | :type response: requests.models.Response 219 | :type request_verb: string 220 | :type request_path: string 221 | 222 | :returns: Response object 223 | :rtype: etcd.response.ResponseV2 224 | """ 225 | 226 | def __init__(self, response, request_verb, request_path): 227 | try: 228 | response_raw = response.json() 229 | except simplejson.JSONDecodeError: 230 | # Bug #1120: Wait will timeout with a JSON-message of zero-length. 231 | if response.text == '': 232 | raise etcd.exceptions.EtcdEmptyResponseError() 233 | else: 234 | raise 235 | 236 | self.node = _build_node_object(response_raw['action'], 237 | response_raw['node']) 238 | 239 | # We have to fake the action (since we don't know what the last actual 240 | # was), but we can reasonably assume it was a SET action (it doesn't 241 | # really matter, as long as it's not a DELETE/CAD action). 242 | # TODO: We're assuming that the 'dir' flag will be set, in prevNode, when 243 | # appropriate. 244 | if 'prevNode' in response_raw: 245 | self.prev_node = _build_node_object(A__PREVNODE, 246 | response_raw['prevNode']) 247 | else: 248 | self.prev_node = None 249 | 250 | def __repr__(self): 251 | return ('' % (self.node)) 252 | 253 | -------------------------------------------------------------------------------- /etcd/modules/lock.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from requests.status_codes import codes 4 | from requests.exceptions import HTTPError 5 | 6 | from etcd.common_ops import CommonOps 7 | 8 | 9 | class _LockBase(object): 10 | def __init__(self, client, lock_name, ttl): 11 | self.__client = client 12 | self.__lock_name = lock_name 13 | self.__path = '/' + lock_name 14 | self.__ttl = ttl 15 | 16 | def __enter__(self): 17 | self.acquire() 18 | return self 19 | 20 | def __exit__(self, exc_type, exc_val, exc_tb): 21 | self.release() 22 | 23 | def acquire(self): 24 | raise NotImplementedError() 25 | 26 | def renew(self, ttl): 27 | raise NotImplementedError() 28 | 29 | def release(self): 30 | raise NotImplementedError() 31 | 32 | @property 33 | def client(self): 34 | return self.__client 35 | 36 | @property 37 | def lock_name(self): 38 | return self.__lock_name 39 | 40 | @property 41 | def path(self): 42 | return self.__path 43 | 44 | @property 45 | def ttl(self): 46 | return self.__ttl 47 | 48 | 49 | class _Lock(_LockBase): 50 | """This lock will seek acquire an exclusive lock every time.""" 51 | 52 | def __init__(self, client, lock_name, ttl): 53 | super(_Lock, self).__init__(client, lock_name, ttl) 54 | 55 | self.__index = None 56 | 57 | def acquire(self): 58 | self.client.debug("Acquiring lock: %s" % (self.path)) 59 | 60 | parameters = { 'ttl': self.ttl } 61 | 62 | try: 63 | r = self.client.send(2, 64 | 'post', 65 | self.path, 66 | module='lock', 67 | parameters=parameters, 68 | return_raw=True) 69 | except HTTPError as e: 70 | if e.response.status_code == codes.internal_server_error: 71 | self.client.debug("There was a server-error while trying to " 72 | "ACQUIRE an index lock. Make sure the key " 73 | "hasn't been used for any other data: %s" % 74 | (self.path)) 75 | 76 | raise 77 | else: 78 | self.__index = int(r.text) 79 | 80 | def renew(self, ttl): 81 | if self.__index is None: 82 | raise ValueError("Could not renew unacquired lock: %s" % (path)) 83 | 84 | self.client.debug("Renewing lock: %s" % (self.path)) 85 | 86 | parameters = { 'ttl': ttl } 87 | data = { 'index': self.__index } 88 | 89 | try: 90 | self.client.send(2, 91 | 'put', 92 | self.path, 93 | module='lock', 94 | parameters=parameters, 95 | data=data, 96 | return_raw=True) 97 | except HTTPError as e: 98 | if e.response.status_code == codes.internal_server_error: 99 | self.client.debug("There was a server-error while trying to " 100 | "RENEW an index lock. Make sure the key " 101 | "has been acquired: %s" % (self.path)) 102 | 103 | raise 104 | 105 | def get_active_index(self): 106 | parameters = { 'field': 'index' } 107 | 108 | try: 109 | r = self.client.send(2, 110 | 'get', 111 | self.path, 112 | module='lock', 113 | parameters=parameters, 114 | return_raw=True) 115 | except HTTPError as e: 116 | if e.response.status_code == codes.internal_server_error: 117 | self.client.debug("There was a server-error while trying to " 118 | "get the active index of an index lock. Make " 119 | "sure the key hasn't been used for any other " 120 | "data: %s" % (self.path)) 121 | 122 | raise 123 | else: 124 | return int(r.text) if r.text != '' else None 125 | 126 | def release(self): 127 | if self.__index is None: 128 | raise ValueError("Could not release unacquired lock: %s" % (path)) 129 | 130 | self.client.debug("Releasing lock: %s" % (self.path)) 131 | 132 | parameters = { 'index': self.__index } 133 | 134 | try: 135 | self.client.send(2, 136 | 'delete', 137 | self.path, 138 | module='lock', 139 | parameters=parameters, 140 | return_raw=True) 141 | except HTTPError as e: 142 | if e.response.status_code == codes.internal_server_error: 143 | self.client.debug("There was a server-error while trying to " 144 | "release an index lock. Make sure the key " 145 | "hasn't been used for any other data: %s" % 146 | (self.path)) 147 | 148 | raise 149 | finally: 150 | self.__index = None 151 | 152 | 153 | class _ReentrantLock(_LockBase): 154 | """This lock will allow the lock to be reacquired without blocking by 155 | anything with the same instance-value. 156 | """ 157 | 158 | def __init__(self, client, lock_name, instance_value, ttl): 159 | super(_ReentrantLock, self).__init__(client, lock_name, ttl) 160 | 161 | self.__instance_value = instance_value 162 | 163 | def acquire(self): 164 | self.client.debug("Acquiring rlock [%s]: %s" % 165 | (self.__instance_value, self.path)) 166 | 167 | parameters = { 'ttl': self.ttl } 168 | 169 | try: 170 | self.client.send(2, 171 | 'post', 172 | self.path, 173 | module='lock', 174 | parameters=parameters, 175 | value=self.__instance_value, 176 | return_raw=True) 177 | except HTTPError as e: 178 | if e.response.status_code == codes.internal_server_error: 179 | self.client.debug("There was a server-error while trying to " 180 | "ACQUIRE a value lock [%s]. Make sure the key " 181 | "hasn't been used for any other data: %s" % 182 | (self.__instance_value, self.path)) 183 | 184 | raise 185 | 186 | def renew(self, ttl): 187 | self.client.debug("Renewing rlock [%s]: %s" % 188 | (self.__instance_value, self.path)) 189 | 190 | parameters = { 'ttl': ttl } 191 | 192 | try: 193 | self.client.send(2, 194 | 'put', 195 | self.path, 196 | module='lock', 197 | parameters=parameters, 198 | value=self.__instance_value, 199 | return_raw=True) 200 | except HTTPError as e: 201 | if e.response.status_code == codes.internal_server_error: 202 | self.client.debug("There was a server-error while trying to " 203 | "RENEW a value lock [%s]. Make sure the key " 204 | "has been acquired: %s" % 205 | (self.__instance_value, self.path)) 206 | 207 | raise 208 | 209 | def get_active_value(self): 210 | try: 211 | r = self.client.send(2, 212 | 'get', 213 | self.path, 214 | module='lock', 215 | return_raw=True) 216 | except HTTPError as e: 217 | if e.response.status_code == codes.internal_server_error: 218 | self.client.debug("There was a server-error while trying to " 219 | "get the active value of a value lock [%s]. " 220 | "Make sure the key hasn't been used for any " 221 | "other data: %s" % 222 | (self.__instance_value, self.path)) 223 | 224 | raise 225 | 226 | return r.text if r.text != '' else None 227 | 228 | def release(self): 229 | self.client.debug("Releasing rlock [%s]: %s" % 230 | (self.__instance_value, self.path)) 231 | 232 | parameters = { 'value': self.__instance_value } 233 | 234 | try: 235 | self.client.send(2, 236 | 'delete', 237 | self.path, 238 | module='lock', 239 | parameters=parameters, 240 | return_raw=True) 241 | except HTTPError as e: 242 | if e.response.status_code == codes.internal_server_error: 243 | self.client.debug("There was a server-error while trying to " 244 | "release a value lock [%s]. Make " 245 | "sure the key hasn't been used for any other " 246 | "data: %s" % (self.__instance_value, self.path)) 247 | 248 | raise 249 | 250 | self.__instance_value = None 251 | 252 | 253 | class LockMod(CommonOps): 254 | def get_lock(self, lock_name, ttl): 255 | return _Lock(self.client, lock_name, ttl) 256 | 257 | def get_rlock(self, lock_name, instance_value, ttl): 258 | return _ReentrantLock(self.client, lock_name, instance_value, ttl) 259 | 260 | # TODO: Is a lock deleted implicitly after expiration, or is it just somehow deactivated? I tried one key with the index lock, and I subsequently used the same key for a value lock, and I got a 500. 261 | -------------------------------------------------------------------------------- /etcd/node_ops.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from requests.exceptions import HTTPError, ChunkedEncodingError 4 | from requests.status_codes import codes 5 | 6 | import etcd.config 7 | 8 | from etcd.exceptions import EtcdPreconditionException, EtcdAtomicWriteError, \ 9 | translate_exceptions 10 | from etcd.common_ops import CommonOps 11 | from etcd.response import ResponseV2 12 | 13 | _logger = logging.getLogger(__name__) 14 | 15 | 16 | class NodeOps(CommonOps): 17 | """Common key-value functions.""" 18 | 19 | @translate_exceptions 20 | def get(self, path, force_consistent=False, force_quorum=False): 21 | """Get the given node. 22 | 23 | :param path: Node key 24 | :type path: string 25 | 26 | :param force_consistent: Only interact with the current leader so 27 | propagation is not a concern. 28 | :type force_consistent: bool 29 | 30 | :returns: Response object 31 | :rtype: :class:`etcd.response.ResponseV2` 32 | 33 | :raises: KeyError 34 | """ 35 | 36 | parameters = {} 37 | if force_consistent is True: 38 | parameters['consistent'] = 'true' 39 | 40 | if force_quorum is True: 41 | parameters['quorum'] = 'true' 42 | 43 | fq_path = self.get_fq_node_path(path) 44 | return self.client.send(2, 'get', fq_path, parameters=parameters) 45 | 46 | @translate_exceptions 47 | def set(self, path, value, ttl=None): 48 | """Set the given node. 49 | 50 | :param path: Node key 51 | :type path: string 52 | 53 | :param value: Value to assign 54 | :type value: scalar 55 | 56 | :param ttl: Number of seconds until expiration 57 | :type ttl: int or None 58 | 59 | :returns: Response object 60 | :rtype: :class:`etcd.response.ResponseV2` 61 | """ 62 | 63 | fq_path = self.get_fq_node_path(path) 64 | data = { } 65 | 66 | if ttl is not None: 67 | data['ttl'] = ttl 68 | 69 | return self.client.send(2, 'put', fq_path, value, data=data) 70 | 71 | @translate_exceptions 72 | def delete(self, path, current_value=None, current_index=None): 73 | """Delete the given node. 74 | 75 | :param path: Node key 76 | :type path: string 77 | 78 | :param current_value: Current value to check 79 | :type current_value: string or None 80 | 81 | :param current_index: Current index to check 82 | :type current_index: int or None 83 | 84 | :returns: Response object 85 | :rtype: :class:`etcd.response.ResponseV2` 86 | """ 87 | 88 | if current_value is not None or current_index is not None: 89 | return self.compare_and_delete(path, is_dir=False, 90 | current_value=current_value, 91 | current_index=current_index) 92 | 93 | fq_path = self.get_fq_node_path(path) 94 | return self.client.send(2, 'delete', fq_path) 95 | 96 | @translate_exceptions 97 | def delete_if_value(self, path, current_value): 98 | """Only delete the given node if it's at the given value. 99 | 100 | :param path: Key 101 | :type path: string 102 | 103 | :param current_value: Current value to check 104 | :type current_value: string 105 | 106 | :returns: Response object 107 | :rtype: :class:`etcd.response.ResponseV2` 108 | """ 109 | 110 | return self.compare_and_delete(path, is_dir=False, 111 | current_value=current_value) 112 | 113 | @translate_exceptions 114 | def delete_if_index(self, path, current_index): 115 | """Only delete the given node if it's at the given index. 116 | 117 | :param path: Key 118 | :type path: string 119 | 120 | :param current_index: Current index to check 121 | :type current_index: int or None 122 | 123 | :returns: Response object 124 | :rtype: :class:`etcd.response.ResponseV2` 125 | """ 126 | 127 | return self.compare_and_delete(path, is_dir=False, 128 | current_index=current_index) 129 | 130 | @translate_exceptions 131 | def compare_and_swap(self, path, value, current_value=None, 132 | current_index=None, prev_exists=None, ttl=None): 133 | """The base compare-and-swap function for atomic comparisons. A 134 | combination of criteria may be used if necessary. 135 | 136 | :param path: Node key 137 | :type path: string 138 | 139 | :param value: Value to assign 140 | :type value: scalar 141 | 142 | :param current_value: Current value to check 143 | :type current_value: scalar or None 144 | 145 | :param current_index: Current index to check 146 | :type current_index: int or None 147 | 148 | :param prev_exists: Whether the node should exist or not 149 | :type prev_exists: bool or None 150 | 151 | :param ttl: The number of seconds until the node expires 152 | :type ttl: int or None 153 | 154 | :returns: Response object 155 | :rtype: :class:`etcd.response.ResponseV2` 156 | 157 | :raises: :class:`etcd.exceptions.EtcdPreconditionException` 158 | """ 159 | 160 | fq_path = self.get_fq_node_path(path) 161 | 162 | parameters = {} 163 | data = { } 164 | 165 | if current_value is not None: 166 | parameters['prevValue'] = current_value 167 | 168 | if current_index is not None: 169 | parameters['prevIndex'] = current_index 170 | 171 | if prev_exists is not None: 172 | parameters['prevExist'] = 'true' if prev_exists is True \ 173 | else 'false' 174 | 175 | if not parameters: 176 | return self.set(path, value, ttl=ttl) 177 | 178 | if ttl is not None: 179 | data['ttl'] = ttl 180 | 181 | return self.client.send(2, 'put', fq_path, value, data=data, 182 | parameters=parameters) 183 | 184 | @translate_exceptions 185 | def create_only(self, path, value, ttl=None): 186 | """A convenience function that will only set a node if it doesn't 187 | already exist. 188 | 189 | :param path: Node key 190 | :type path: string 191 | 192 | :param value: Value to assign 193 | :type value: scalar 194 | 195 | :param ttl: The number of seconds until the node expires 196 | :type ttl: int or None 197 | 198 | :returns: Response object 199 | :rtype: :class:`etcd.response.ResponseV2` 200 | """ 201 | 202 | # This will have a return "action" of "create". 203 | return self.compare_and_swap(path, value, prev_exists=False, ttl=ttl) 204 | 205 | @translate_exceptions 206 | def update_only(self, path, value, ttl=None): 207 | """A convenience function that will only set a node if it already 208 | exists. 209 | 210 | :param path: Node key 211 | :type path: string 212 | 213 | :param value: Value to assign 214 | :type value: scalar 215 | 216 | :param ttl: The number of seconds until the node expires 217 | :type ttl: int or None 218 | 219 | :returns: Response object 220 | :rtype: :class:`etcd.response.ResponseV2` 221 | """ 222 | 223 | # This will have a return "action" of "update". 224 | return self.compare_and_swap(path, value, prev_exists=True, ttl=ttl) 225 | 226 | @translate_exceptions 227 | def update_if_index(self, path, value, current_index, ttl=None): 228 | """A convenience function that will only set a node if its existing 229 | "modified index" matches. 230 | 231 | :param path: Node key 232 | :type path: string 233 | 234 | :param value: Value to assign 235 | :type value: scalar 236 | 237 | :param current_index: Current index to check 238 | :type current_index: int 239 | 240 | :param ttl: The number of seconds until the node expires 241 | :type ttl: int or None 242 | 243 | :returns: Response object 244 | :rtype: :class:`etcd.response.ResponseV2` 245 | """ 246 | 247 | # This will have a return "action" of "compareAndSwap". 248 | return self.compare_and_swap(path, value, current_index=current_index, ttl=ttl) 249 | 250 | @translate_exceptions 251 | def update_if_value(self, path, value, current_value, ttl=None): 252 | """A convenience function that will only set a node if its existing value 253 | matches. 254 | 255 | :param path: Node key 256 | :type path: string 257 | 258 | :param value: Value to assign 259 | :type value: scalar 260 | 261 | :param current_value: Current value to check 262 | :type current_value: scalar or None 263 | 264 | :param ttl: The number of seconds until the node expires 265 | :type ttl: int or None 266 | 267 | :returns: Response object 268 | :rtype: :class:`etcd.response.ResponseV2` 269 | """ 270 | 271 | # This will have a return "action" of "compareAndSwap". 272 | return self.compare_and_swap(path, value, current_value=current_value, ttl=ttl) 273 | 274 | @translate_exceptions 275 | def wait(self, path, force_consistent=False): 276 | return super(NodeOps, self).wait(path, force_consistent=force_consistent) 277 | 278 | @translate_exceptions 279 | def atomic_update(self, path, update_value_cb, 280 | max_attempts=etcd.config.ATOMIC_MAX_ATTEMPTS, ttl=None): 281 | """Retrieve the value for the given path, pass it to the callback, get 282 | an update value back, and try updating. Loop until the update can be 283 | performed atomically. 284 | 285 | :param path: Node key 286 | :type path: string 287 | 288 | :param update_value_cb: Callback 289 | :type update_value_cb: callback 290 | """ 291 | 292 | i = max_attempts 293 | while i > 0: 294 | response = self.get(path) 295 | value = update_value_cb(response.node.value) 296 | 297 | try: 298 | return self.update_if_index( 299 | path, 300 | value, 301 | response.node.modified_index, 302 | ttl=ttl) 303 | except EtcdPreconditionException: 304 | pass 305 | 306 | i -= 1 307 | 308 | raise EtcdAtomicWriteError("Atomic update failed (%d): %s" % (i, path)) 309 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Python etcd Client documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Jan 6 09:05:18 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | 'sphinx.ext.viewcode', 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix of source filenames. 40 | source_suffix = '.rst' 41 | 42 | # The encoding of source files. 43 | #source_encoding = 'utf-8-sig' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = u'Python etcd Client' 50 | copyright = u'2014, Dustin Oprea' 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | version = '1.1.3' 58 | # The full version, including alpha/beta/rc tags. 59 | release = '1.1.3' 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | #language = None 64 | 65 | # There are two options for replacing |today|: either, you set today to some 66 | # non-false value, then it is used: 67 | #today = '' 68 | # Else, today_fmt is used as the format for a strftime call. 69 | #today_fmt = '%B %d, %Y' 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | exclude_patterns = ['_build'] 74 | 75 | # The reST default role (used for this markup: `text`) to use for all 76 | # documents. 77 | #default_role = None 78 | 79 | # If true, '()' will be appended to :func: etc. cross-reference text. 80 | #add_function_parentheses = True 81 | 82 | # If true, the current module name will be prepended to all description 83 | # unit titles (such as .. function::). 84 | #add_module_names = True 85 | 86 | # If true, sectionauthor and moduleauthor directives will be shown in the 87 | # output. They are ignored by default. 88 | #show_authors = False 89 | 90 | # The name of the Pygments (syntax highlighting) style to use. 91 | pygments_style = 'sphinx' 92 | 93 | # A list of ignored prefixes for module index sorting. 94 | #modindex_common_prefix = [] 95 | 96 | # If true, keep warnings as "system message" paragraphs in the built documents. 97 | #keep_warnings = False 98 | 99 | 100 | # -- Options for HTML output ---------------------------------------------- 101 | 102 | # The theme to use for HTML and HTML Help pages. See the documentation for 103 | # a list of builtin themes. 104 | html_theme = 'default' 105 | 106 | # Theme options are theme-specific and customize the look and feel of a theme 107 | # further. For a list of options available for each theme, see the 108 | # documentation. 109 | #html_theme_options = {} 110 | 111 | # Add any paths that contain custom themes here, relative to this directory. 112 | #html_theme_path = [] 113 | 114 | # The name for this set of Sphinx documents. If None, it defaults to 115 | # " v documentation". 116 | #html_title = None 117 | 118 | # A shorter title for the navigation bar. Default is the same as html_title. 119 | #html_short_title = None 120 | 121 | # The name of an image file (relative to this directory) to place at the top 122 | # of the sidebar. 123 | #html_logo = None 124 | 125 | # The name of an image file (within the static path) to use as favicon of the 126 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 127 | # pixels large. 128 | #html_favicon = None 129 | 130 | # Add any paths that contain custom static files (such as style sheets) here, 131 | # relative to this directory. They are copied after the builtin static files, 132 | # so a file named "default.css" will overwrite the builtin "default.css". 133 | html_static_path = ['_static'] 134 | 135 | # Add any extra paths that contain custom files (such as robots.txt or 136 | # .htaccess) here, relative to this directory. These files are copied 137 | # directly to the root of the documentation. 138 | #html_extra_path = [] 139 | 140 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 141 | # using the given strftime format. 142 | #html_last_updated_fmt = '%b %d, %Y' 143 | 144 | # If true, SmartyPants will be used to convert quotes and dashes to 145 | # typographically correct entities. 146 | #html_use_smartypants = True 147 | 148 | # Custom sidebar templates, maps document names to template names. 149 | #html_sidebars = {} 150 | 151 | # Additional templates that should be rendered to pages, maps page names to 152 | # template names. 153 | #html_additional_pages = {} 154 | 155 | # If false, no module index is generated. 156 | #html_domain_indices = True 157 | 158 | # If false, no index is generated. 159 | #html_use_index = True 160 | 161 | # If true, the index is split into individual pages for each letter. 162 | #html_split_index = False 163 | 164 | # If true, links to the reST sources are added to the pages. 165 | #html_show_sourcelink = True 166 | 167 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 168 | #html_show_sphinx = True 169 | 170 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 171 | #html_show_copyright = True 172 | 173 | # If true, an OpenSearch description file will be output, and all pages will 174 | # contain a tag referring to it. The value of this option must be the 175 | # base URL from which the finished HTML is served. 176 | #html_use_opensearch = '' 177 | 178 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 179 | #html_file_suffix = None 180 | 181 | # Output file base name for HTML help builder. 182 | htmlhelp_basename = 'PythonetcdClientdoc' 183 | 184 | 185 | # -- Options for LaTeX output --------------------------------------------- 186 | 187 | latex_elements = { 188 | # The paper size ('letterpaper' or 'a4paper'). 189 | #'papersize': 'letterpaper', 190 | 191 | # The font size ('10pt', '11pt' or '12pt'). 192 | #'pointsize': '10pt', 193 | 194 | # Additional stuff for the LaTeX preamble. 195 | #'preamble': '', 196 | } 197 | 198 | # Grouping the document tree into LaTeX files. List of tuples 199 | # (source start file, target name, title, 200 | # author, documentclass [howto, manual, or own class]). 201 | latex_documents = [ 202 | ('index', 'PythonetcdClient.tex', u'Python etcd Client Documentation', 203 | u'Dustin Oprea', 'manual'), 204 | ] 205 | 206 | # The name of an image file (relative to this directory) to place at the top of 207 | # the title page. 208 | #latex_logo = None 209 | 210 | # For "manual" documents, if this is true, then toplevel headings are parts, 211 | # not chapters. 212 | #latex_use_parts = False 213 | 214 | # If true, show page references after internal links. 215 | #latex_show_pagerefs = False 216 | 217 | # If true, show URL addresses after external links. 218 | #latex_show_urls = False 219 | 220 | # Documents to append as an appendix to all manuals. 221 | #latex_appendices = [] 222 | 223 | # If false, no module index is generated. 224 | #latex_domain_indices = True 225 | 226 | 227 | # -- Options for manual page output --------------------------------------- 228 | 229 | # One entry per manual page. List of tuples 230 | # (source start file, name, description, authors, manual section). 231 | man_pages = [ 232 | ('index', 'pythonetcdclient', u'Python etcd Client Documentation', 233 | [u'Dustin Oprea'], 1) 234 | ] 235 | 236 | # If true, show URL addresses after external links. 237 | #man_show_urls = False 238 | 239 | 240 | # -- Options for Texinfo output ------------------------------------------- 241 | 242 | # Grouping the document tree into Texinfo files. List of tuples 243 | # (source start file, target name, title, author, 244 | # dir menu entry, description, category) 245 | texinfo_documents = [ 246 | ('index', 'PythonetcdClient', u'Python etcd Client Documentation', 247 | u'Dustin Oprea', 'PythonetcdClient', 'One line description of project.', 248 | 'Miscellaneous'), 249 | ] 250 | 251 | # Documents to append as an appendix to all manuals. 252 | #texinfo_appendices = [] 253 | 254 | # If false, no module index is generated. 255 | #texinfo_domain_indices = True 256 | 257 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 258 | #texinfo_show_urls = 'footnote' 259 | 260 | # If true, do not generate a @detailmenu in the "Top" node's menu. 261 | #texinfo_no_detailmenu = False 262 | 263 | 264 | # -- Options for Epub output ---------------------------------------------- 265 | 266 | # Bibliographic Dublin Core info. 267 | epub_title = u'Python etcd Client' 268 | epub_author = u'Dustin Oprea' 269 | epub_publisher = u'Dustin Oprea' 270 | epub_copyright = u'2014, Dustin Oprea' 271 | 272 | # The basename for the epub file. It defaults to the project name. 273 | #epub_basename = u'Python etcd Client' 274 | 275 | # The HTML theme for the epub output. Since the default themes are not optimized 276 | # for small screen space, using the same theme for HTML and epub output is 277 | # usually not wise. This defaults to 'epub', a theme designed to save visual 278 | # space. 279 | #epub_theme = 'epub' 280 | 281 | # The language of the text. It defaults to the language option 282 | # or en if the language is not set. 283 | #epub_language = '' 284 | 285 | # The scheme of the identifier. Typical schemes are ISBN or URL. 286 | #epub_scheme = '' 287 | 288 | # The unique identifier of the text. This can be a ISBN number 289 | # or the project homepage. 290 | #epub_identifier = '' 291 | 292 | # A unique identification for the text. 293 | #epub_uid = '' 294 | 295 | # A tuple containing the cover image and cover page html template filenames. 296 | #epub_cover = () 297 | 298 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 299 | #epub_guide = () 300 | 301 | # HTML files that should be inserted before the pages created by sphinx. 302 | # The format is a list of tuples containing the path and title. 303 | #epub_pre_files = [] 304 | 305 | # HTML files shat should be inserted after the pages created by sphinx. 306 | # The format is a list of tuples containing the path and title. 307 | #epub_post_files = [] 308 | 309 | # A list of files that should not be packed into the epub file. 310 | #epub_exclude_files = [] 311 | 312 | # The depth of the table of contents in toc.ncx. 313 | #epub_tocdepth = 3 314 | 315 | # Allow duplicate toc entries. 316 | #epub_tocdup = True 317 | 318 | # Choose between 'default' and 'includehidden'. 319 | #epub_tocscope = 'default' 320 | 321 | # Fix unsupported image types using the PIL. 322 | #epub_fix_images = False 323 | 324 | # Scale large images. 325 | #epub_max_image_width = 0 326 | 327 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 328 | #epub_show_urls = 'inline' 329 | 330 | # If false, no index is generated. 331 | #epub_use_index = True 332 | -------------------------------------------------------------------------------- /etcd/docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Python etcd Client documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Jan 5 22:26:36 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | 'sphinx.ext.viewcode', 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix of source filenames. 40 | source_suffix = '.rst' 41 | 42 | # The encoding of source files. 43 | #source_encoding = 'utf-8-sig' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = u'Python etcd Client' 50 | copyright = u'2014, Dustin Oprea' 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | version = '1.1.3' 58 | # The full version, including alpha/beta/rc tags. 59 | release = '1.1.3' 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | #language = None 64 | 65 | # There are two options for replacing |today|: either, you set today to some 66 | # non-false value, then it is used: 67 | #today = '' 68 | # Else, today_fmt is used as the format for a strftime call. 69 | #today_fmt = '%B %d, %Y' 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | exclude_patterns = ['_build'] 74 | 75 | # The reST default role (used for this markup: `text`) to use for all 76 | # documents. 77 | #default_role = None 78 | 79 | # If true, '()' will be appended to :func: etc. cross-reference text. 80 | #add_function_parentheses = True 81 | 82 | # If true, the current module name will be prepended to all description 83 | # unit titles (such as .. function::). 84 | #add_module_names = True 85 | 86 | # If true, sectionauthor and moduleauthor directives will be shown in the 87 | # output. They are ignored by default. 88 | #show_authors = False 89 | 90 | # The name of the Pygments (syntax highlighting) style to use. 91 | pygments_style = 'sphinx' 92 | 93 | # A list of ignored prefixes for module index sorting. 94 | #modindex_common_prefix = [] 95 | 96 | # If true, keep warnings as "system message" paragraphs in the built documents. 97 | #keep_warnings = False 98 | 99 | 100 | # -- Options for HTML output ---------------------------------------------- 101 | 102 | # The theme to use for HTML and HTML Help pages. See the documentation for 103 | # a list of builtin themes. 104 | html_theme = 'default' 105 | 106 | # Theme options are theme-specific and customize the look and feel of a theme 107 | # further. For a list of options available for each theme, see the 108 | # documentation. 109 | #html_theme_options = {} 110 | 111 | # Add any paths that contain custom themes here, relative to this directory. 112 | #html_theme_path = [] 113 | 114 | # The name for this set of Sphinx documents. If None, it defaults to 115 | # " v documentation". 116 | #html_title = None 117 | 118 | # A shorter title for the navigation bar. Default is the same as html_title. 119 | #html_short_title = None 120 | 121 | # The name of an image file (relative to this directory) to place at the top 122 | # of the sidebar. 123 | #html_logo = None 124 | 125 | # The name of an image file (within the static path) to use as favicon of the 126 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 127 | # pixels large. 128 | #html_favicon = None 129 | 130 | # Add any paths that contain custom static files (such as style sheets) here, 131 | # relative to this directory. They are copied after the builtin static files, 132 | # so a file named "default.css" will overwrite the builtin "default.css". 133 | html_static_path = ['_static'] 134 | 135 | # Add any extra paths that contain custom files (such as robots.txt or 136 | # .htaccess) here, relative to this directory. These files are copied 137 | # directly to the root of the documentation. 138 | #html_extra_path = [] 139 | 140 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 141 | # using the given strftime format. 142 | #html_last_updated_fmt = '%b %d, %Y' 143 | 144 | # If true, SmartyPants will be used to convert quotes and dashes to 145 | # typographically correct entities. 146 | #html_use_smartypants = True 147 | 148 | # Custom sidebar templates, maps document names to template names. 149 | #html_sidebars = {} 150 | 151 | # Additional templates that should be rendered to pages, maps page names to 152 | # template names. 153 | #html_additional_pages = {} 154 | 155 | # If false, no module index is generated. 156 | #html_domain_indices = True 157 | 158 | # If false, no index is generated. 159 | #html_use_index = True 160 | 161 | # If true, the index is split into individual pages for each letter. 162 | #html_split_index = False 163 | 164 | # If true, links to the reST sources are added to the pages. 165 | #html_show_sourcelink = True 166 | 167 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 168 | #html_show_sphinx = True 169 | 170 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 171 | #html_show_copyright = True 172 | 173 | # If true, an OpenSearch description file will be output, and all pages will 174 | # contain a tag referring to it. The value of this option must be the 175 | # base URL from which the finished HTML is served. 176 | #html_use_opensearch = '' 177 | 178 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 179 | #html_file_suffix = None 180 | 181 | # Output file base name for HTML help builder. 182 | htmlhelp_basename = 'PythonetcdClientdoc' 183 | 184 | 185 | # -- Options for LaTeX output --------------------------------------------- 186 | 187 | latex_elements = { 188 | # The paper size ('letterpaper' or 'a4paper'). 189 | #'papersize': 'letterpaper', 190 | 191 | # The font size ('10pt', '11pt' or '12pt'). 192 | #'pointsize': '10pt', 193 | 194 | # Additional stuff for the LaTeX preamble. 195 | #'preamble': '', 196 | } 197 | 198 | # Grouping the document tree into LaTeX files. List of tuples 199 | # (source start file, target name, title, 200 | # author, documentclass [howto, manual, or own class]). 201 | latex_documents = [ 202 | ('index', 'PythonetcdClient.tex', u'Python etcd Client Documentation', 203 | u'Dustin Oprea', 'manual'), 204 | ] 205 | 206 | # The name of an image file (relative to this directory) to place at the top of 207 | # the title page. 208 | #latex_logo = None 209 | 210 | # For "manual" documents, if this is true, then toplevel headings are parts, 211 | # not chapters. 212 | #latex_use_parts = False 213 | 214 | # If true, show page references after internal links. 215 | #latex_show_pagerefs = False 216 | 217 | # If true, show URL addresses after external links. 218 | #latex_show_urls = False 219 | 220 | # Documents to append as an appendix to all manuals. 221 | #latex_appendices = [] 222 | 223 | # If false, no module index is generated. 224 | #latex_domain_indices = True 225 | 226 | 227 | # -- Options for manual page output --------------------------------------- 228 | 229 | # One entry per manual page. List of tuples 230 | # (source start file, name, description, authors, manual section). 231 | man_pages = [ 232 | ('index', 'pythonetcdclient', u'Python etcd Client Documentation', 233 | [u'Dustin Oprea'], 1) 234 | ] 235 | 236 | # If true, show URL addresses after external links. 237 | #man_show_urls = False 238 | 239 | 240 | # -- Options for Texinfo output ------------------------------------------- 241 | 242 | # Grouping the document tree into Texinfo files. List of tuples 243 | # (source start file, target name, title, author, 244 | # dir menu entry, description, category) 245 | texinfo_documents = [ 246 | ('index', 'PythonetcdClient', u'Python etcd Client Documentation', 247 | u'Dustin Oprea', 'PythonetcdClient', 'One line description of project.', 248 | 'Miscellaneous'), 249 | ] 250 | 251 | # Documents to append as an appendix to all manuals. 252 | #texinfo_appendices = [] 253 | 254 | # If false, no module index is generated. 255 | #texinfo_domain_indices = True 256 | 257 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 258 | #texinfo_show_urls = 'footnote' 259 | 260 | # If true, do not generate a @detailmenu in the "Top" node's menu. 261 | #texinfo_no_detailmenu = False 262 | 263 | 264 | # -- Options for Epub output ---------------------------------------------- 265 | 266 | # Bibliographic Dublin Core info. 267 | epub_title = u'Python etcd Client' 268 | epub_author = u'Dustin Oprea' 269 | epub_publisher = u'Dustin Oprea' 270 | epub_copyright = u'2014, Dustin Oprea' 271 | 272 | # The basename for the epub file. It defaults to the project name. 273 | #epub_basename = u'Python etcd Client' 274 | 275 | # The HTML theme for the epub output. Since the default themes are not optimized 276 | # for small screen space, using the same theme for HTML and epub output is 277 | # usually not wise. This defaults to 'epub', a theme designed to save visual 278 | # space. 279 | #epub_theme = 'epub' 280 | 281 | # The language of the text. It defaults to the language option 282 | # or en if the language is not set. 283 | #epub_language = '' 284 | 285 | # The scheme of the identifier. Typical schemes are ISBN or URL. 286 | #epub_scheme = '' 287 | 288 | # The unique identifier of the text. This can be a ISBN number 289 | # or the project homepage. 290 | #epub_identifier = '' 291 | 292 | # A unique identification for the text. 293 | #epub_uid = '' 294 | 295 | # A tuple containing the cover image and cover page html template filenames. 296 | #epub_cover = () 297 | 298 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 299 | #epub_guide = () 300 | 301 | # HTML files that should be inserted before the pages created by sphinx. 302 | # The format is a list of tuples containing the path and title. 303 | #epub_pre_files = [] 304 | 305 | # HTML files shat should be inserted after the pages created by sphinx. 306 | # The format is a list of tuples containing the path and title. 307 | #epub_post_files = [] 308 | 309 | # A list of files that should not be packed into the epub file. 310 | #epub_exclude_files = [] 311 | 312 | # The depth of the table of contents in toc.ncx. 313 | #epub_tocdepth = 3 314 | 315 | # Allow duplicate toc entries. 316 | #epub_tocdup = True 317 | 318 | # Choose between 'default' and 'includehidden'. 319 | #epub_tocscope = 'default' 320 | 321 | # Fix unsupported image types using the PIL. 322 | #epub_fix_images = False 323 | 324 | # Scale large images. 325 | #epub_max_image_width = 0 326 | 327 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 328 | #epub_show_urls = 'inline' 329 | 330 | # If false, no index is generated. 331 | #epub_use_index = True 332 | -------------------------------------------------------------------------------- /etcd/client.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import ssl 3 | import logging 4 | 5 | from os import environ 6 | from requests.exceptions import ConnectionError 7 | from requests.adapters import HTTPAdapter 8 | from requests.packages.urllib3.poolmanager import PoolManager 9 | from datetime import datetime 10 | 11 | from etcd.config import HOST_FAIL_WAIT_S 12 | from etcd.directory_ops import DirectoryOps 13 | from etcd.node_ops import NodeOps 14 | from etcd.server_ops import ServerOps 15 | from etcd.stat_ops import StatOps 16 | from etcd.inorder_ops import InOrderOps 17 | from etcd.modules.lock import LockMod 18 | from etcd.modules.leader import LeaderMod 19 | from etcd.response import ResponseV2 20 | 21 | logging.getLogger('requests.packages.urllib3').setLevel(logging.WARN) 22 | 23 | _logger = logging.getLogger(__name__) 24 | 25 | _SSL_DO_VERIFY = bool(int(environ.get('PEC_SSL_DO_VERIFY', '1'))) 26 | 27 | _SSL_CA_BUNDLE_FILEPATH = environ.get( 28 | 'PEC_SSL_CA_BUNDLE_FILEPATH', 29 | '') or None 30 | _SSL_CLIENT_CRT_FILEPATH = environ.get( 31 | 'PEC_SSL_CLIENT_CRT_FILEPATH', 32 | '') or None 33 | 34 | _SSL_CLIENT_KEY_FILEPATH = environ.get( 35 | 'PEC_SSL_CLIENT_KEY_FILEPATH', 36 | '') or None 37 | 38 | 39 | class _Ssl3HttpAdapter(HTTPAdapter): 40 | """"Transport adapter" that allows us to use SSLv3.""" 41 | 42 | def init_poolmanager(self, connections, maxsize, block=False): 43 | self.poolmanager = PoolManager(num_pools=connections, 44 | maxsize=maxsize, 45 | block=block, 46 | ssl_version=ssl.PROTOCOL_SSLv3) 47 | 48 | 49 | class _Modules(object): 50 | """Intermediate container that holds functionality related to modules. 51 | 52 | :param client: Client instance 53 | :type client: :class:`etcd.client.Client` 54 | """ 55 | 56 | def __init__(self, client): 57 | self.__client = client 58 | 59 | @property 60 | def lock(self): 61 | """Return an instance of the class having the lock functionality. 62 | 63 | :rtype: :class:`etcd.response.ResponseV2` 64 | """ 65 | 66 | try: 67 | return self.__lock 68 | except AttributeError: 69 | self.__lock = LockMod(self.__client) 70 | return self.__lock 71 | 72 | @property 73 | def leader(self): 74 | """Return an instance of the class having the leader-election 75 | functionality. 76 | 77 | :rtype: :class:`etcd.modules.leader.LeaderMod` 78 | """ 79 | 80 | try: 81 | return self.__leader 82 | except AttributeError: 83 | self.__leader = LeaderMod(self.__client) 84 | return self.__leader 85 | 86 | 87 | class Client(object): 88 | """The main channel of functionality for the client. Connects to the 89 | server, and provides functions via properties. 90 | 91 | :param host: Hostname or IP of server 92 | :type host: string 93 | 94 | :param port: Port of server 95 | :type port: int 96 | 97 | :param is_ssl: Whether to use 'http://' or 'https://'. 98 | :type is_ssl: bool 99 | 100 | :param ssl_do_verify: Whether to verify the certificate hostname. 101 | :type ssl_do_verify: bool or None 102 | 103 | :param ssl_ca_bundle_filepath: A bundle of rootCAs for verifications. 104 | :type ssl_ca_bundle_filepath: string or None 105 | 106 | :param ssl_client_cert_filepath: A client certificate, for authentication. 107 | :type ssl_client_cert_filepath: string or None 108 | 109 | :param ssl_client_key_filepath: A client key, for authentication. 110 | :type ssl_client_key_filepath: string or None 111 | 112 | :raises: ValueError 113 | """ 114 | 115 | def __init__(self, host='127.0.0.1', port=4001, 116 | is_ssl=False, ssl_do_verify=_SSL_DO_VERIFY, 117 | ssl_ca_bundle_filepath=_SSL_CA_BUNDLE_FILEPATH, 118 | ssl_client_cert_filepath=_SSL_CLIENT_CRT_FILEPATH, 119 | ssl_client_key_filepath=_SSL_CLIENT_KEY_FILEPATH): 120 | 121 | if ssl_do_verify is not None: 122 | _logger.debug("SSL: Explicit verify setting given: [%s]", ssl_do_verify) 123 | self.__ssl_verify = ssl_do_verify 124 | 125 | elif ssl_ca_bundle_filepath is not None: 126 | _logger.debug("SSL: We'll be verifying against a CA bundle: [%s]", 127 | ssl_ca_bundle_filepath) 128 | 129 | self.__ssl_verify = ssl_ca_bundle_filepath 130 | 131 | else: 132 | _logger.debug("SSL: We'll verify the CA certificate, by default.") 133 | 134 | self.__ssl_verify = True 135 | 136 | if ssl_client_cert_filepath is None: 137 | _logger.debug("SSL: No client key/certificate will be used.") 138 | self.__ssl_cert = None 139 | 140 | elif ssl_client_key_filepath is not None: 141 | _logger.debug("SSL: Client key and certificate will be used: " 142 | "KEY=[%s] CERTIFICATE=[%s]", 143 | ssl_client_key_filepath, ssl_client_cert_filepath) 144 | 145 | self.__ssl_cert = \ 146 | (ssl_client_cert_filepath, 147 | ssl_client_key_filepath) 148 | 149 | else: 150 | _logger.debug("SSL: Client certificate will be used (without a " 151 | "key): [%s]", ssl_client_cert_filepath) 152 | 153 | self.__ssl_cert = ssl_client_cert_filepath 154 | 155 | scheme = 'http' if is_ssl is False else 'https' 156 | self.__prefix = ('%s://%s:%s' % (scheme, host, port)) 157 | _logger.debug("PREFIX= [%s]", self.__prefix) 158 | 159 | self.__session = requests.Session() 160 | 161 | # Define an adapter for when SSL is requested. 162 | self.__session.mount('https://', _Ssl3HttpAdapter()) 163 | 164 | # TODO: Remove the version check after debugging. 165 | # TODO: Can we implicitly read the version from the response/headers? 166 | # self.__version = self.server.get_version() 167 | # self.debug("Version: %s" % (self.__version)) 168 | # 169 | # if self.__version.startswith('0.2') is False: 170 | # raise ValueError("We don't support an etcd version older than 0.2.0 .") 171 | 172 | self.__machines = [[dict(machine_info)['etcd'], None] 173 | for machine_info 174 | in self.server.get_machines()] 175 | 176 | _logger.debug("Cluster machines: %s", self.__machines) 177 | 178 | # This will fail if the given server does appear in the published list 179 | # of servers. This might only happen because of a hostname being used 180 | # instead of an IP, or vice-versa. 181 | self.__machine_index = None 182 | i = 0 183 | for (prefix, last_fail_dt) in self.__machines: 184 | if prefix == self.__prefix: 185 | self.__machine_index = i 186 | break 187 | 188 | i += 1 189 | 190 | if self.__machine_index is None: 191 | raise ValueError("Could not identify given prefix [%s] among " 192 | "published prefixes: %s" % 193 | (self.__prefix, self.__machines)) 194 | 195 | _logger.debug("The initial machine is at index (%d).", 196 | self.__machine_index) 197 | 198 | def __str__(self): 199 | return ('' % (self.__prefix)) 200 | 201 | def send(self, version, verb, path, value=None, parameters=None, data=None, 202 | module=None, return_raw=False, allow_reconnect=True): 203 | """Build and execute a request. 204 | 205 | :param version: Version of API 206 | :type version: int 207 | 208 | :param verb: Verb of request ('get', 'post', etc..) 209 | :type verb: string 210 | 211 | :param path: URL path 212 | :type path: string 213 | 214 | :param value: Value to be converted to string and passed as "value" in 215 | the POST data. 216 | :type value: scalar or None 217 | 218 | :param parameters: Dictionary of values to be passed via URL query. 219 | :type parameters: dictionary or None 220 | 221 | :param data: Dictionary of values to be passed via POST data. 222 | :type data: dictionary or None 223 | 224 | :param module: Name of the etcd module that hosts the functionality. 225 | :type module: string or None 226 | 227 | :param return_raw: Whether to return a 228 | :class:`etcd.response.ResponseV2` object or the raw 229 | Requests response. 230 | :type return_raw: bool 231 | 232 | :param allow_reconnect: Allow the client to consider alternate hosts if 233 | the current host fails connection. 234 | :type allow_reconnect: bool 235 | 236 | :returns: Response object 237 | :rtype: :class:`etcd.response.ResponseV2` 238 | """ 239 | 240 | if parameters is None: 241 | parameters = {} 242 | 243 | if data is None: 244 | data = {} 245 | 246 | if version != 2: 247 | raise ValueError("We were told to send a version (%d) request, " 248 | "which is not supported." % (version)) 249 | else: 250 | response_cls = ResponseV2 251 | 252 | if module is None: 253 | url = ('%s/v%d%s' % (self.__prefix, version, path)) 254 | else: 255 | url = ('%s/mod/v%d/%s%s' % (self.__prefix, version, module, path)) 256 | 257 | if value is not None: 258 | data['value'] = value 259 | 260 | args = { 'params': parameters, 261 | 'data': data, 262 | 'verify': self.__ssl_verify, 263 | 'cert': self.__ssl_cert } 264 | 265 | _logger.debug("Request(%s)=[%s] params=[%s] data_keys=[%s]", 266 | verb, url, parameters, args['data'].keys()) 267 | 268 | send = getattr(self.__session, verb) 269 | 270 | while 1: 271 | try: 272 | r = send(url, **args) 273 | except ConnectionError as e: 274 | _logger.debug("Connection error with [%s] [%s]: %s", 275 | self.__prefix, e.__class__.__name__, str(e)) 276 | 277 | if allow_reconnect is False: 278 | raise 279 | else: 280 | break 281 | 282 | # If we get here, there was a connection problem. Rotate the server 283 | # that we're using, excluding any that have recently failed. 284 | 285 | now_dt = datetime.now() 286 | self.__machines[self.__machine_index][1] = now_dt 287 | 288 | len_ = len(self.__machines) 289 | i = 0 290 | elected = None 291 | while i < len_: 292 | machine_index = (self.__machine_index + 1) % len_ 293 | (prefix, last_fail_dt) = self.__machines[machine_index] 294 | 295 | if last_fail_dt is None or \ 296 | (now_dt - last_fail_dt).total_seconds() > \ 297 | HOST_FAIL_WAIT_S: 298 | elected = prefix 299 | 300 | i += 1 301 | 302 | if elected is None: 303 | raise SystemError("All servers have failed: %s" % 304 | (self.__machines,)) 305 | 306 | self.__prefix = elected 307 | self.__machine_index = machine_index 308 | 309 | _logger.debug("Retrying with next machine: %s", self.__prefix) 310 | 311 | r.raise_for_status() 312 | 313 | if return_raw is True: 314 | return r 315 | 316 | return response_cls(r, verb, path) 317 | 318 | @property 319 | def session(self): 320 | return self.__session 321 | 322 | @property 323 | def prefix(self): 324 | """Return the URL prefix for the server. 325 | 326 | :rtype: string 327 | """ 328 | 329 | return self.__prefix 330 | 331 | @property 332 | def directory(self): 333 | """Return an instance of the class having the directory functionality. 334 | 335 | :rtype: :class:`etcd.directory_ops.DirectoryOps` 336 | """ 337 | 338 | try: 339 | return self.__directory 340 | except AttributeError: 341 | self.__directory = DirectoryOps(self) 342 | return self.__directory 343 | 344 | @property 345 | def node(self): 346 | """Return an instance of the class having the general node 347 | functionality. 348 | 349 | :rtype: :class:`etcd.node_ops.NodeOps` 350 | """ 351 | 352 | try: 353 | return self.__node 354 | except AttributeError: 355 | self.__node = NodeOps(self) 356 | return self.__node 357 | 358 | @property 359 | def server(self): 360 | """Return an instance of the class having the server functionality. 361 | 362 | :rtype: :class:`etcd.server_ops.ServerOps` 363 | """ 364 | 365 | try: 366 | return self.__server 367 | except AttributeError: 368 | self.__server = ServerOps(self) 369 | return self.__server 370 | 371 | @property 372 | def stat(self): 373 | """Return an instance of the class having the stat functionality. 374 | 375 | :rtype: :class:`etcd.stat_ops.StatOps` 376 | """ 377 | 378 | try: 379 | return self.__stat 380 | except AttributeError: 381 | self.__stat = StatOps(self) 382 | return self.__stat 383 | 384 | @property 385 | def inorder(self): 386 | """Return an instance of the class having the "in-order keys" 387 | functionality. 388 | 389 | :rtype: :class:`etcd.inorder_ops.InOrderOps` 390 | """ 391 | 392 | try: 393 | return self.__inorder 394 | except AttributeError: 395 | self.__inorder = InOrderOps(self) 396 | return self.__inorder 397 | 398 | @property 399 | def module(self): 400 | """Return an instance of the class that hosts the functionality 401 | provided by individual modules. 402 | 403 | :rtype: :class:`etcd.client._Modules` 404 | """ 405 | 406 | try: 407 | return self.__module 408 | except AttributeError: 409 | self.__module = _Modules(self) 410 | return self.__module 411 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Introduction 2 | ------------ 3 | 4 | *PEC* was created as a more elegant and proper client for *etcd* than existing 5 | solutions. It has an intuitive construction, provides access to the 6 | complete *etcd* API (of 0.2.0+), and just works. 7 | 8 | Every request returns a standard and obvious result, and HTTP exceptions are 9 | re-raised as Python-standard exceptions where it makes sense (like "KeyError"). 10 | 11 | The full API is documented [here](http://python-etcd-client.readthedocs.org/en/latest/). 12 | 13 | 14 | Quick Start 15 | ----------- 16 | 17 | There's almost nothing to it: 18 | 19 | ```python 20 | from etcd.client import Client 21 | 22 | # Uses the default *etcd* port on *localhost* unless told differently. 23 | c = Client() 24 | 25 | c.node.set('/test/key', 5) 26 | 27 | r = c.node.get('/test/key') 28 | 29 | print(r.node.value) 30 | # Displays "5". 31 | 32 | r = c.node.set('/test/key', 10) 33 | print(r.prev_node.value) 34 | # Displays "5". 35 | ``` 36 | 37 | 38 | SSL 39 | --- 40 | *PEC* also allows for SSL authentication and encrypted communication. 41 | 42 | To use it for communication, pass the hostname as the *host* parameter and a 43 | *is_ssl* of *True*. If you need to pass a bundle of CA certificates (for less 44 | well-known root authorities), pass *ssl_ca_bundle_filepath*. 45 | 46 | ```python 47 | c = Client(host='etcd.local', 48 | is_ssl=True, 49 | ssl_ca_bundle_filepath='ssl/rootCA.pem') 50 | ``` 51 | 52 | 53 | General Functions 54 | ----------------- 55 | 56 | These functions represent the basic key-value functionality of *etcd*. 57 | 58 | Set a value: 59 | 60 | ```python 61 | # Can provide a "ttl" parameter with seconds, for expiration. 62 | r = c.node.set('/node_test/subkey1', 5) 63 | 64 | print(r) 65 | # Prints: > 68 | ``` 69 | 70 | Get a value: 71 | 72 | ```python 73 | r = c.node.get('/node_test/subkey1') 74 | 75 | print(r) 76 | # Prints: > 79 | 80 | print(r.node.value) 81 | # Prints "5" 82 | ``` 83 | 84 | Wait for a change to a specific node: 85 | 86 | ```python 87 | r = c.node.wait('/node_test/subkey1') 88 | 89 | print(r) 90 | # Prints: > 93 | 94 | print(r.node.value) 95 | # Prints "20" 96 | ``` 97 | 98 | In this case, a set with a value of (20) was performed from another terminal, 99 | and we were given the same exact response that they got. We can set the 100 | *recursive* parameter to *True* to watch subdirectories and subdirectories-of- 101 | subdirectories as well. 102 | 103 | Get children: 104 | 105 | ```python 106 | r = c.node.set('/node_test/subkey2', 10) 107 | 108 | print(r) 109 | # Prints: > 112 | 113 | r = c.node.get('/node_test') 114 | 115 | print(r) 116 | # Prints: > 119 | ``` 120 | 121 | Get children, recursively: 122 | 123 | ```python 124 | r = c.node.get('/node_test', recursive=True) 125 | 126 | print(r) 127 | # Prints: > 130 | 131 | for node in r.node.children: 132 | print(node) 133 | 134 | # Prints: 135 | # 137 | # 139 | ``` 140 | 141 | Delete node: 142 | 143 | ```python 144 | r = c.node.delete('/node_test/subkey2') 145 | 146 | print(r) 147 | # Prints: > 150 | ``` 151 | 152 | 153 | Compare and Swap (CAS) Functions 154 | -------------------------------- 155 | 156 | These functions represent *etcd*'s atomic comparisons. These allow for a "set"- 157 | type operation when one or more conditions are met. 158 | 159 | The core call takes one or more of the following conditions as arguments: 160 | 161 | current_value 162 | prev_exists 163 | current_index 164 | 165 | If none of the conditions are given, the call acts like a *set()*. If a 166 | condition is given that fails, a *etcd.exceptions.EtcdPreconditionException* is 167 | raised. 168 | 169 | The core call is: 170 | 171 | ```python 172 | r = c.node.compare_and_swap('/cas_test/val1', 30, current_value=5, 173 | prev_exists=True, current_index=5) 174 | ``` 175 | 176 | The following convenience functions are also provided. They only allow you to 177 | check one, specific condition: 178 | 179 | ```python 180 | r = c.node.create_only('/cas_test/val1', 5) 181 | 182 | print(r) 183 | # Prints: > 186 | 187 | r = c.node.update_only('/cas_test/val1', 10) 188 | 189 | print(r) 190 | # Prints: > 193 | 194 | r = c.node.update_if_index('/cas_test/val1', 15, r.node.modified_index) 195 | 196 | print(r) 197 | # Prints: > 200 | 201 | r = c.node.update_if_value('/cas_test/val1', 20, 15) 202 | 203 | print(r) 204 | # Prints: > 207 | ``` 208 | 209 | Directory Functions 210 | ------------------- 211 | 212 | These functions represent directory-specific calls. Whereas creating a node has 213 | side-effects that contribute to directory management (like creating a node 214 | under a directory implicitly creates the directory), these functions are 215 | directory specific. 216 | 217 | Create directory: 218 | 219 | ```python 220 | # Can provide a "ttl" parameter with seconds, for expiration. 221 | r = c.directory.create('/dir_test/new_dir') 222 | 223 | print(r) 224 | # Prints: ] CI=(16) MI=(16)> 227 | ``` 228 | 229 | Remove an empty directory: 230 | 231 | ```python 232 | r = c.directory.delete('/dir_test/new_dir') 233 | 234 | print(r) 235 | # Prints: > 238 | ``` 239 | 240 | Recursively remove a directory, and any contents: 241 | 242 | ```python 243 | c.directory.create('/dir_test/new_dir') 244 | c.directory.create('/dir_test/new_dir/new_subdir') 245 | 246 | # This will raise a requests.exceptions.HTTPError ("403 Client Error: 247 | # Forbidden") because it has children. 248 | r = c.directory.delete('/dir_test/new_dir') 249 | 250 | # You have to recursively delete it. 251 | r = c.directory.delete_recursive('/dir_test') 252 | 253 | print(r) 254 | # Prints: > 257 | ``` 258 | 259 | 260 | Compare and Delete (CAD) 261 | ------------------------ 262 | 263 | Similar to the "compare and swap" functionality (mentioned above), 264 | compare-and-delete functionality (introduced in *etcd 0.3.0*) is also exposed. 265 | This functionality allows you to delete a node only if its value or index meets 266 | conditions. Unlike the CAS functionality, CAD functionality is located in the 267 | node and directory functions. 268 | 269 | ```python 270 | r = c.node.delete_if_value('/node_test/testkey', 'some_existing_value') 271 | 272 | print(r) 273 | # Prints: > 276 | 277 | r = c.node.delete_if_index('/node_test/testkey2', 22) 278 | 279 | print(r) 280 | # (waiting on bugfixes, to test) 281 | 282 | r = c.directory.delete_if_index('/dir_test/testkey2', 22) 283 | 284 | print(r) 285 | # (waiting on bugfixes, to test) 286 | 287 | r = c.directory.delete_recursive_if_index('/dir_test/testkey2', 22) 288 | 289 | print(r) 290 | # (waiting on bugfixes, to test) 291 | 292 | 293 | Server Functions 294 | ---------------- 295 | 296 | These functions represent calls that return information about the server or 297 | cluster. 298 | 299 | Get version of the specific host being connected to: 300 | 301 | ```python 302 | r = c.server.get_version() 303 | 304 | print(r) 305 | # Prints "0.2.0-45-g98351b9", on my system. 306 | ``` 307 | 308 | The URL prefix of the current cluster leader: 309 | 310 | ```python 311 | r = c.server.get_leader_url_prefix() 312 | 313 | print(r) 314 | # Prints "http://127.0.0.1:7001" with my single-host configuration. 315 | ``` 316 | 317 | Enumerate the prefixes of the hosts in the cluster: 318 | 319 | ```python 320 | machines = c.server.get_machines() 321 | 322 | print(machines) 323 | # Prints: [(u'etcd', u'http://127.0.0.1:4001'), 324 | # (u'raft', u'http://127.0.0.1:7001')] 325 | ``` 326 | 327 | Get URL of the dashboard for the server being connected-to: 328 | 329 | ```python 330 | r = c.server.get_dashboard_url() 331 | 332 | print(r) 333 | # Prints: http://127.0.0.1:4001/mod/dashboard 334 | ``` 335 | 336 | 337 | In-Order-Keys Functions 338 | ----------------------- 339 | 340 | These calls represent the in-order functionality, where a directory can be used 341 | to store a series of values with automatically-assigned, increasing keys. 342 | Though not quite sufficient as a queue, this might be used to automatically 343 | generate unique keys for a set of values. 344 | 345 | Enqueue values: 346 | 347 | ```python 348 | io = c.inorder.get_inorder('/queue_test') 349 | 350 | io.add('value1') 351 | io.add('value2') 352 | ``` 353 | 354 | Enumerate existing values: 355 | 356 | ```python 357 | # If you want to specifically return the entries in order of the keys 358 | # (which is to say that they're in insert-order), use the "sorted" 359 | # parameter. 360 | r = io.list() 361 | 362 | for child in r.node.children: 363 | print(child.value) 364 | 365 | # Prints: 366 | # value1 367 | # value2 368 | ``` 369 | 370 | 371 | Statistics Functions 372 | -------------------- 373 | 374 | This functions provide access to the statistics information published by the 375 | *etcd* hosts. 376 | 377 | ```python 378 | s = c.stat.get_leader_stats() 379 | 380 | print(s) 381 | # Prints: (u'test01', {u'dustinlenovo': LStatFollower(counts= 382 | # LStatCounts(fail=412, success=75214), latency= 383 | # LStatLatency(average=7.201827094703149, current=0.535978, 384 | # maximum=350.543234, minimum=0.462994, 385 | # standard_deviation=22.639299448915402))}) 386 | 387 | s = c.stat.get_self_stats() 388 | 389 | print(s) 390 | # Prints: SStat(leader_info=SStatLeader(leader=u'test01', 391 | # uptime=datetime.timedelta(0, 3971, 790306)), name=u'test01', 392 | # recv_append_request_cnt=0, send_append_request_cnt=75626, 393 | # send_bandwidth_rate=538.5960990745054, 394 | # send_pkg_rate=20.10061948402707, 395 | # start_time=datetime.datetime(2014, 2, 8, 16, 26, 13), 396 | # state=u'leader') 397 | ``` 398 | 399 | 400 | Locking Module Functions 401 | ------------------------ 402 | 403 | These functions represent the fair locking functionality that comes packaged. 404 | 405 | ### Standard Locking 406 | 407 | A simple, distributed lock: 408 | 409 | ```python 410 | l = c.module.lock.get_lock('test_lock_1', ttl=10) 411 | l.acquire() 412 | l.renew(ttl=30) 413 | l.release() 414 | ``` 415 | 416 | This returns the index of the current lock holder: 417 | 418 | ```python 419 | l.get_active_index() 420 | ``` 421 | 422 | It's also available as a *with* statement: 423 | 424 | ```python 425 | with c.module.lock.get_lock('test_lock_1', ttl=10): 426 | print("In lock 1.") 427 | ``` 428 | 429 | ### Reentrant Locking 430 | 431 | Here, a name for the lock is provided, as well as a value that represents a 432 | certain locking purpose, process, or host. Subsequent requests having the same 433 | value currently stored for the lock will return immediately, where others will 434 | block until the current lock has been released or expired. 435 | 436 | This can be used when certain parts of the logic need to participate during the 437 | same lock, or a specific function/etc might be invoked multiple times during a 438 | lock. 439 | 440 | This is the basic usage (nearly identical to the traditional lock): 441 | 442 | ```python 443 | rl = c.module.lock.get_rlock('test_lock_2', 'proc1', ttl=10) 444 | rl.acquire() 445 | rl.renew(ttl=30) 446 | rl.release() 447 | ``` 448 | 449 | This returns the current value of the lock holder(s): 450 | 451 | ```python 452 | rl.get_active_value() 453 | ``` 454 | 455 | This is also provided as a *with* statement: 456 | 457 | ```python 458 | with c.module.lock.get_rlock('test_lock_2', 'proc1', ttl=10): 459 | print("In lock 2.") 460 | ``` 461 | 462 | 463 | Leader Election Module Functions 464 | -------------------------------- 465 | 466 | The leader-election API does consensus-based assignment, meaning that, of all 467 | of the clients potentially attempting to assign a value to a name, only one 468 | assignment will be allowed for a certain period of time, or until the key is 469 | deleted. 470 | 471 | To set or renew a value: 472 | 473 | ```python 474 | c.module.leader.set_or_renew('consensus-based key', 'test value', ttl=10) 475 | ``` 476 | 477 | To get the current value: 478 | 479 | ```python 480 | # This will return: 481 | # > A non-empty string if the key is set and unexpired. 482 | # > None, if the key was set but has been expired or deleted. 483 | r = c.module.leader.get('consensus-based key') 484 | 485 | print(r) 486 | # Prints "test value". 487 | ``` 488 | 489 | To delete the value (will fail unless there's an unexpired value): 490 | 491 | ```python 492 | c.module.leader.delete('consensus-based key', 'test value') 493 | ``` 494 | --------------------------------------------------------------------------------