├── test ├── __init__.py ├── cfssl │ ├── client-key.pem │ ├── server-key.pem │ ├── client-root-key.pem │ ├── client.pem │ ├── client-root.pem │ ├── server.pem │ └── ca.pem ├── utils.py ├── test_cluster.py ├── test_lease.py ├── test_kv.py ├── test_auth.py └── test_watch.py ├── aioetcd3 ├── _etcdv3 │ ├── __init__.py │ ├── auth_pb2_grpc.py │ ├── kv_pb2_grpc.py │ ├── kv_pb2.py │ ├── auth_pb2.py │ └── rpc_pb2_grpc.py ├── __init__.py ├── protos │ ├── auth.proto │ ├── kv.proto │ └── rpc.proto ├── produce.sh ├── exceptions.py ├── maintenance.py ├── transaction.py ├── utils.py ├── help.py ├── cluster.py ├── base.py ├── lease.py ├── auth.py ├── client.py ├── kv.py └── watch.py ├── requirements.txt ├── codecov.yml ├── .coveragerc ├── setup.py ├── README.md ├── .travis.yml └── .gitignore /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /aioetcd3/_etcdv3/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /aioetcd3/__init__.py: -------------------------------------------------------------------------------- 1 | from aioetcd3.help import * -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiogrpc>=1.4 2 | protobuf 3 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | threshold: 5 -------------------------------------------------------------------------------- /aioetcd3/_etcdv3/auth_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | import grpc 3 | 4 | -------------------------------------------------------------------------------- /aioetcd3/_etcdv3/kv_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | import grpc 3 | 4 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = aioetcd3 3 | omit = aioetcd3/_etcdv3/* 4 | 5 | [report] 6 | exclude_lines = 7 | pragma: no cover 8 | pass -------------------------------------------------------------------------------- /test/cfssl/client-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIKIpaNwPGovuVxJM1GMGiJDHZJWaDzJyXeXPoIjGsJqfoAoGCCqGSM49 3 | AwEHoUQDQgAEYwsP1I307m9u1wrsqjacF5xdSk67iifUGT/MqbSSBtGlRK05VDn2 4 | 87ghkIrsX1B7j/LJcUCDLnzmJVPjfa8lWg== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /test/cfssl/server-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIKiO/qjfQEIJS4OYW3rHUQodAEk2/PzNj+V6Oy/+JSnIoAoGCCqGSM49 3 | AwEHoUQDQgAE2azbBMQaqhuTm5d+7rzIdqlXnBrv0LxwDLIQnUTosRYlrS19+gEg 4 | AccoyJEyzGFzd3+Ot6OOX3nXUxDSVFzlLA== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /test/cfssl/client-root-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIMD5KcGoWPEneh+YMw36oJqC8wwJHyTlP1saFYuqUSKGoAoGCCqGSM49 3 | AwEHoUQDQgAELMjGp4dyHA3vW3nU8XHh+JIT5B/bdIaSvVpJIoTgFPNoLpDspJmt 4 | GAxStBczUE8rwmRLfNbk0aG8zn8EsZoqDA== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /aioetcd3/protos/auth.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package authpb; 3 | 4 | 5 | 6 | // User is a single entry in the bucket authUsers 7 | message User { 8 | bytes name = 1; 9 | bytes password = 2; 10 | repeated string roles = 3; 11 | } 12 | 13 | // Permission is a single entity 14 | message Permission { 15 | enum Type { 16 | READ = 0; 17 | WRITE = 1; 18 | READWRITE = 2; 19 | } 20 | Type permType = 1; 21 | 22 | bytes key = 2; 23 | bytes range_end = 3; 24 | } 25 | 26 | // Role is a single entry in the bucket authRoles 27 | message Role { 28 | bytes name = 1; 29 | 30 | repeated Permission keyPermission = 2; 31 | } 32 | -------------------------------------------------------------------------------- /test/utils.py: -------------------------------------------------------------------------------- 1 | async def switch_auth_on(client): 2 | await client.user_add(username="root", password="root") 3 | await client.role_add(name="root") 4 | await client.user_grant_role(username="root", role="root") 5 | 6 | await client.user_add(username="client", password="client") 7 | await client.role_add(name="client") 8 | await client.user_grant_role(username="client", role="client") 9 | await client.auth_enable() 10 | 11 | 12 | async def switch_auth_off(root_client, unautheticated_client): 13 | await root_client.auth_disable() 14 | await unautheticated_client.user_delete("client") 15 | await unautheticated_client.user_delete("root") 16 | await unautheticated_client.role_delete("client") 17 | await unautheticated_client.role_delete("root") 18 | -------------------------------------------------------------------------------- /aioetcd3/produce.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sed -i -e '/gogoproto/d' protos/rpc.proto 4 | sed -i -e 's/etcd\/mvcc\/mvccpb\/kv.proto/kv.proto/g' protos/rpc.proto 5 | sed -i -e 's/etcd\/auth\/authpb\/auth.proto/auth.proto/g' protos/rpc.proto 6 | sed -i -e '/google\/api\/annotations.proto/d' protos/rpc.proto 7 | sed -i -e '/option (google.api.http)/,+3d' protos/rpc.proto 8 | 9 | sed -i -e '/gogoproto/d' protos/kv.proto 10 | 11 | sed -i -e '/gogoproto/d' protos/auth.proto 12 | 13 | python3 -m grpc.tools.protoc -Iprotos --python_out=rpc --grpc_python_out=rpc protos/rpc.proto protos/auth.proto protos/kv.proto 14 | 15 | 16 | sed -i -e 's/import auth_pb2/from aioetcd3.rpc import auth_pb2/g' rpc/rpc_pb2.py 17 | sed -i -e 's/import kv_pb2/from aioetcd3.rpc import kv_pb2/g' rpc/rpc_pb2.py 18 | 19 | 20 | -------------------------------------------------------------------------------- /aioetcd3/exceptions.py: -------------------------------------------------------------------------------- 1 | # All of the custom errors are inherited from the grpc.RpcError 2 | # for the backward compatibility 3 | from grpc import RpcError, StatusCode 4 | 5 | 6 | class EtcdError(RpcError): 7 | code = StatusCode.UNKNOWN 8 | 9 | def __init__(self, details, debug_info=None): 10 | self.details = details 11 | self.debug_info = debug_info 12 | 13 | def __repr__(self): 14 | return "`{}`: reason: `{}`".format(self.code, self.details) 15 | 16 | 17 | class AuthError(EtcdError): 18 | code = StatusCode.INVALID_ARGUMENT 19 | 20 | 21 | class Unauthenticated(EtcdError): 22 | code = StatusCode.UNAUTHENTICATED 23 | 24 | 25 | class InvalidArgument(EtcdError): 26 | code = StatusCode.INVALID_ARGUMENT 27 | 28 | 29 | class PermissionDenied(EtcdError): 30 | code = StatusCode.PERMISSION_DENIED 31 | 32 | 33 | class FailedPrecondition(EtcdError): 34 | code = StatusCode.FAILED_PRECONDITION 35 | 36 | 37 | STATUS_MAP = { 38 | StatusCode.UNAUTHENTICATED: Unauthenticated, 39 | StatusCode.PERMISSION_DENIED: PermissionDenied, 40 | StatusCode.FAILED_PRECONDITION: FailedPrecondition, 41 | } 42 | -------------------------------------------------------------------------------- /test/cfssl/client.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC/zCCAeegAwIBAgIUfo2E4cRuVDSGcUlQbc8VWD0EHZcwDQYJKoZIhvcNAQEL 3 | BQAwdTELMAkGA1UEBhMCVVMxFjAUBgNVBAgTDVNhbiBGcmFuY2lzY28xCzAJBgNV 4 | BAcTAkNBMRgwFgYDVQQKEw9NeSBDb21wYW55IE5hbWUxEzARBgNVBAsTCk9yZyBV 5 | bml0IDIxEjAQBgNVBAMTCU15IG93biBDQTAeFw0xNzA4MDEwOTU4MDBaFw0yMjA3 6 | MzEwOTU4MDBaMEMxCzAJBgNVBAYTAlVTMRYwFAYDVQQIEw1TYW4gRnJhbmNpc2Nv 7 | MQswCQYDVQQHEwJDQTEPMA0GA1UEAxMGY2xpZW50MFkwEwYHKoZIzj0CAQYIKoZI 8 | zj0DAQcDQgAEYwsP1I307m9u1wrsqjacF5xdSk67iifUGT/MqbSSBtGlRK05VDn2 9 | 87ghkIrsX1B7j/LJcUCDLnzmJVPjfa8lWqOBgzCBgDAOBgNVHQ8BAf8EBAMCBaAw 10 | EwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUG6Sc 11 | mNr0LfHR3Th2ER8mo8UFOSIwHwYDVR0jBBgwFoAUbGH5lLoeNKPg6S0yWrhNOoNP 12 | fCcwCwYDVR0RBAQwAoIAMA0GCSqGSIb3DQEBCwUAA4IBAQAk5+gTElEnkaKRIuy2 13 | Uf/8GRSFAmlCbuEVGGuQ4Iif1KWVt0sklUC4EpknJGCDBDfRlH/n/O0cIAhxHJVd 14 | 8HXRfYy+ynhr08gdE7lbueavEpvUb3QNFR8ZrODcqwvJgHyWsffk7f87hpsx9lr2 15 | mfLvokau0UhpVlq+x7IQ5dKw/ZzKv/zjI/A2guwK1UWdk5vr0W7LE5XY4pa+9/Qy 16 | y6FxHcKc4z8FJ6ClRGy/RGJrbg0VgCmrTMa7NCpIyZ/onn7RaxHatSn7cnQPVNKq 17 | fBHHBVdl1LlsJSPRZar0zGkUS1UeCtcSW1aqSmkO39p18tJ9hDWM0xkY6FlI3A+4 18 | shAQ 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/cfssl/client-root.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC/TCCAeWgAwIBAgIUD8TzummshnrEovvmr4wqLzawxlowDQYJKoZIhvcNAQEL 3 | BQAwdTELMAkGA1UEBhMCVVMxFjAUBgNVBAgTDVNhbiBGcmFuY2lzY28xCzAJBgNV 4 | BAcTAkNBMRgwFgYDVQQKEw9NeSBDb21wYW55IE5hbWUxEzARBgNVBAsTCk9yZyBV 5 | bml0IDIxEjAQBgNVBAMTCU15IG93biBDQTAeFw0xNzA4MDEwOTU5MDBaFw0yMjA3 6 | MzEwOTU5MDBaMEExCzAJBgNVBAYTAlVTMRYwFAYDVQQIEw1TYW4gRnJhbmNpc2Nv 7 | MQswCQYDVQQHEwJDQTENMAsGA1UEAxMEcm9vdDBZMBMGByqGSM49AgEGCCqGSM49 8 | AwEHA0IABCzIxqeHchwN71t51PFx4fiSE+Qf23SGkr1aSSKE4BTzaC6Q7KSZrRgM 9 | UrQXM1BPK8JkS3zW5NGhvM5/BLGaKgyjgYMwgYAwDgYDVR0PAQH/BAQDAgWgMBMG 10 | A1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFJZWw4xW 11 | lf9oa/ioycvblk/fzktLMB8GA1UdIwQYMBaAFGxh+ZS6HjSj4OktMlq4TTqDT3wn 12 | MAsGA1UdEQQEMAKCADANBgkqhkiG9w0BAQsFAAOCAQEAo4N4uDXl6nIG8ndrOeoe 13 | S2JpPTU+gkaez2fs21DpuGO3SLSEnIYLcaY3p6sdjU2m0m2yGweLKLfVmQLzHO0R 14 | 4sZtKQFY1sklhCAhmiU5YZbb98gyXMfPVaFXCy5IWnajDsmhmh0G0UbVV/zaWJw+ 15 | B+yzGVvWMBI4htG9Zz59yIt4Fku2TgqDudiFEzm9OB9LykYS+oKKLqb2DlLmWSdu 16 | NOr0j+sSwzTiGNSstb4jaXhbO2f80mykg4Rs5oFbiqYMH7qOfk5uR/uSomu+l03v 17 | cv9t71iCZ1/ss+ZgfS24crnsAUqUZBRmPGu1lrXRaVNzkK9BGZ5XAHT2eBEQAlqp 18 | OA== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | version = "1.13" 6 | 7 | try: 8 | import pypandoc 9 | long_description = pypandoc.convert('README.md', 'rst') 10 | except (IOError, ImportError): 11 | long_description = "" 12 | 13 | 14 | setup( 15 | name="aioetcd3", 16 | version=version, 17 | author="gaopeiliang", 18 | author_email="964911957@qq.com", 19 | long_description=long_description, 20 | description="asyncio wrapper for etcd v3", 21 | license="Apache", 22 | classifiers=[ 23 | 'License :: OSI Approved :: MIT License', 24 | 'Intended Audience :: Developers', 25 | 'Framework :: AsyncIO', 26 | 'Programming Language :: Python', 27 | 'Programming Language :: Python :: 3', 28 | 'Programming Language :: Python :: 3.6', 29 | 'Topic :: Software Development', 30 | 'Framework :: AsyncIO', 31 | ], 32 | url="https://github.com/gaopeiliang/aioetcd3", 33 | platforms=['any'], 34 | packages=find_packages(), 35 | python_requires='>=3.6', 36 | install_requires=[ 37 | 'aiogrpc>=1.4', 38 | 'protobuf' 39 | ] 40 | ) 41 | -------------------------------------------------------------------------------- /test/cfssl/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDFjCCAf6gAwIBAgIUHeUTcT0g5zmaUzPKeqEi6AZMZ5wwDQYJKoZIhvcNAQEL 3 | BQAwdTELMAkGA1UEBhMCVVMxFjAUBgNVBAgTDVNhbiBGcmFuY2lzY28xCzAJBgNV 4 | BAcTAkNBMRgwFgYDVQQKEw9NeSBDb21wYW55IE5hbWUxEzARBgNVBAsTCk9yZyBV 5 | bml0IDIxEjAQBgNVBAMTCU15IG93biBDQTAeFw0xNzA4MDIwODM4MDBaFw0yMjA4 6 | MDEwODM4MDBaMEQxCzAJBgNVBAYTAlVTMRYwFAYDVQQIEw1TYW4gRnJhbmNpc2Nv 7 | MQswCQYDVQQHEwJDQTEQMA4GA1UEAxMHbWVtYmVyMzBZMBMGByqGSM49AgEGCCqG 8 | SM49AwEHA0IABNms2wTEGqobk5uXfu68yHapV5wa79C8cAyyEJ1E6LEWJa0tffoB 9 | IAHHKMiRMsxhc3d/jrejjl9511MQ0lRc5SyjgZkwgZYwDgYDVR0PAQH/BAQDAgWg 10 | MBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFKmG 11 | Ok0gF06fFZlNPVR8QoF3i7/XMB8GA1UdIwQYMBaAFGxh+ZS6HjSj4OktMlq4TTqD 12 | T3wnMCEGA1UdEQQaMBiHBH8AAAGHBGRJLgOHBGRJLgSHBGRJLgUwDQYJKoZIhvcN 13 | AQELBQADggEBAMOUiyg+R6Qh6CFmNjxZJCk1hlRPrYucA/7phWRsqOI/PCFpak8d 14 | LIOCQdPwTC20hqZxszN0GmJVdIZy1FEDa7yDZip2pYrmCR2ePAAwXtEhUAGBblIU 15 | IAFiLtFps5GQqi3+f9SUto0HnriQiBt8fSf4KNeD84gkOt524qliAY2bIOdphJaY 16 | Y+Qg6jkn8+lzoY+rD8tzOx37dgL+/V0dCyo7fBLnW6mOEdHk5sMpPPyWzmCYSdeb 17 | SqNpp9+EDOVhz175CQuuq0WD+x3D210UIQsEFvyjg/W6Vinl9W/hOG6yxv01IVcM 18 | yhngfepPKZ20b4njhHJv+geOn5yshCcTQjI= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/test_cluster.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import asyncio 3 | import functools 4 | 5 | from aioetcd3.client import client 6 | from aioetcd3.help import range_all 7 | 8 | 9 | def asynctest(f): 10 | @functools.wraps(f) 11 | def _f(self): 12 | asyncio.get_event_loop().run_until_complete(f(self)) 13 | 14 | return _f 15 | 16 | 17 | class ClusterTest(unittest.TestCase): 18 | def setUp(self): 19 | endpoints = "127.0.0.1:2379" 20 | self.client = client(endpoint=endpoints) 21 | 22 | @asynctest 23 | async def test_member(self): 24 | members = await self.client.member_list() 25 | self.assertTrue(members) 26 | 27 | m = members[0] 28 | # urls = [u for u in m.clientURLs] 29 | # urls = [u.rpartition("//")[2] for u in urls] 30 | 31 | healthy, unhealthy = await self.client.member_healthy([m.clientURLs]) 32 | self.assertTrue(healthy) 33 | self.assertFalse(unhealthy) 34 | 35 | healthy, unhealthy = await self.client.member_healthy() 36 | self.assertTrue(healthy) 37 | self.assertFalse(unhealthy) 38 | 39 | @asynctest 40 | async def tearDown(self): 41 | await self.client.close() 42 | -------------------------------------------------------------------------------- /test/cfssl/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID3jCCAsagAwIBAgIUfLIjRt7LfB0n9SVKLCfJR54lUdYwDQYJKoZIhvcNAQEL 3 | BQAwdTELMAkGA1UEBhMCVVMxFjAUBgNVBAgTDVNhbiBGcmFuY2lzY28xCzAJBgNV 4 | BAcTAkNBMRgwFgYDVQQKEw9NeSBDb21wYW55IE5hbWUxEzARBgNVBAsTCk9yZyBV 5 | bml0IDIxEjAQBgNVBAMTCU15IG93biBDQTAeFw0xNzA4MDEwODM3MDBaFw0yMjA3 6 | MzEwODM3MDBaMHUxCzAJBgNVBAYTAlVTMRYwFAYDVQQIEw1TYW4gRnJhbmNpc2Nv 7 | MQswCQYDVQQHEwJDQTEYMBYGA1UEChMPTXkgQ29tcGFueSBOYW1lMRMwEQYDVQQL 8 | EwpPcmcgVW5pdCAyMRIwEAYDVQQDEwlNeSBvd24gQ0EwggEiMA0GCSqGSIb3DQEB 9 | AQUAA4IBDwAwggEKAoIBAQDb8Ec7DVjLw74wmnG6Ke0DBXBKRxy2MVdQNw6a6vQ1 10 | UPXcjPjctPVoy1IrDK6f7CH3LGeiAv/g2zbgDQdRT/f3b986DxvBQMRj/rmRCsp4 11 | pcd+Nt0LtKBmKJCA7kk+urx/gmAS/9wa7RcC9kRg3husihIpa02AEMtd759Czjgy 12 | JHlCtlIoBSqxCqrEkKzc0Zw8SfDI7zKtOGlfA9bia6lx/y3TMvdCuPrDAvf1FSSj 13 | ECdXL70jYSSgA40VvhBVF5Nom/gsJ+/DmrYNwsGiA4klFp4ip4eKIyBcyk/Ni1uG 14 | wzQoLSaB0UqUCXKvWCimCzEnl/I0IJZ/TcrFmNoauGuxAgMBAAGjZjBkMA4GA1Ud 15 | DwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgECMB0GA1UdDgQWBBRsYfmUuh40 16 | o+DpLTJauE06g098JzAfBgNVHSMEGDAWgBRsYfmUuh40o+DpLTJauE06g098JzAN 17 | BgkqhkiG9w0BAQsFAAOCAQEAR/+35fG9b0Wt7jwoUF3L2A2OrZa36avb8ktkC/OO 18 | qm7skzUBDb7iz+iznEDICgrvecjXwq+te5ob92H3weDs6YJz0+T8EXBnUtzN2+bu 19 | eapdky5dZwweMqofr0FF2hLUVPWErgsZRj1gH1eLbFSirwtCbskmAzqK5TRKCtQL 20 | cOZ/WlsgmCdETzHSLztdKKTau1l/qHJBdH7hIppG4iEISMueHlW+H9+yu9haKu2L 21 | 4J9feFOqC8G/aR+81og79WGwb2HJWgpw92ji8JxLvF5M1B++9AvwndkovkVgjFnk 22 | JBDagrwsg/gr+FVi3uw5NnktLgtzzcMD0VWGCmAEn/R3Mg== 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aioetcd3 2 | [![Build Status](https://travis-ci.org/gaopeiliang/aioetcd3.svg?branch=master)](https://travis-ci.org/gaopeiliang/aioetcd3) 3 | [![Code Coverage](https://codecov.io/gh/gaopeiliang/aioetcd3/branch/master/graphs/badge.svg)](https://codecov.io/gh/gaopeiliang/aioetcd3) 4 | 5 | ## AsyncIO bindings for etcd V3 6 | 7 | example: 8 | ``` 9 | from aioetcd3.client import client 10 | from aioetcd3.help import range_all 11 | from aioetcd3.kv import KV 12 | from aioetcd3 import transaction 13 | 14 | etcd_client = client(endpoints="127.0.0.1:2379") 15 | 16 | await etcd_client.put('/foo', 'foo') 17 | 18 | value, meta = await etcd_client.get('/foo') 19 | 20 | value_list = await etcd_client.range(range_all()) 21 | 22 | await etcd_client.delete('/foo') 23 | 24 | 25 | lease = await etcd_client.grant_lease(ttl=5) 26 | 27 | await etcd_client.put('/foo1', 'foo', lease=lease) 28 | 29 | is_success, response = await etcd_client.txn(compare=[ 30 | transaction.Value('/trans1') == b'trans1', 31 | transaction.Value('/trans2') == b'trans2' 32 | ], success=[ 33 | KV.delete.txn('/trans1'), 34 | KV.put.txn('/trans3', 'trans3', prev_kv=True) 35 | ], fail=[ 36 | KV.delete.txn('/trans1') 37 | ]) 38 | 39 | await self.client.user_add(username="test user", password='1234') 40 | await self.client.role_add(name="test_role") 41 | ``` 42 | 43 | ## Install 44 | ``` 45 | pip install aioetcd3 46 | ``` 47 | -------------------------------------------------------------------------------- /aioetcd3/protos/kv.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package mvccpb; 3 | 4 | 5 | 6 | message KeyValue { 7 | // key is the key in bytes. An empty key is not allowed. 8 | bytes key = 1; 9 | // create_revision is the revision of last creation on this key. 10 | int64 create_revision = 2; 11 | // mod_revision is the revision of last modification on this key. 12 | int64 mod_revision = 3; 13 | // version is the version of the key. A deletion resets 14 | // the version to zero and any modification of the key 15 | // increases its version. 16 | int64 version = 4; 17 | // value is the value held by the key, in bytes. 18 | bytes value = 5; 19 | // lease is the ID of the lease that attached to key. 20 | // When the attached lease expires, the key will be deleted. 21 | // If lease is 0, then no lease is attached to the key. 22 | int64 lease = 6; 23 | } 24 | 25 | message Event { 26 | enum EventType { 27 | PUT = 0; 28 | DELETE = 1; 29 | } 30 | // type is the kind of event. If type is a PUT, it indicates 31 | // new data has been stored to the key. If type is a DELETE, 32 | // it indicates the key was deleted. 33 | EventType type = 1; 34 | // kv holds the KeyValue for the event. 35 | // A PUT event contains current kv pair. 36 | // A PUT event with kv.Version=1 indicates the creation of a key. 37 | // A DELETE/EXPIRE event contains the deleted key with 38 | // its modification revision set to the revision of deletion. 39 | KeyValue kv = 2; 40 | 41 | // prev_kv holds the key-value pair before the event happens. 42 | KeyValue prev_kv = 3; 43 | } 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: python 4 | 5 | python: 6 | - "3.6" 7 | - "3.7" 8 | - "3.8" 9 | 10 | 11 | services: 12 | - docker 13 | 14 | install: 15 | - pip install codecov 16 | - pip install coverage 17 | - pip install -r requirements.txt 18 | - docker run -d -p 2379:2379 -p 2380:2380 --name etcd-v3.2 --volume=/tmp/etcd-data:/etcd-data gcr.io/etcd-development/etcd:v3.2 /usr/local/bin/etcd --name my-etcd-1 --data-dir /etcd-data --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://0.0.0.0:2379 --listen-peer-urls http://0.0.0.0:2380 --initial-advertise-peer-urls http://0.0.0.0:2380 --initial-cluster my-etcd-1=http://0.0.0.0:2380 --initial-cluster-token 123456789 --initial-cluster-state new --auto-compaction-retention 1 19 | - docker run -d -p 2378:2379 -p 2381:2380 --name etcd-v3.2-auth --volume=/tmp/etcd-data2:/etcd-data --volume=`pwd`/test/cfssl:/cfssl gcr.io/etcd-development/etcd:v3.2 /usr/local/bin/etcd --name my-etcd-1 --data-dir /etcd-data --listen-client-urls https://0.0.0.0:2379 --advertise-client-urls https://0.0.0.0:2378 --client-cert-auth --trusted-ca-file=/cfssl/ca.pem --cert-file=/cfssl/server.pem --key-file=/cfssl/server-key.pem 20 | - docker run -d -p 2377:2379 -p 2382:2380 --name etcd-v3.2-ssl --volume=/tmp/etcd-data3:/etcd-data --volume=`pwd`/test/cfssl:/cfssl gcr.io/etcd-development/etcd:v3.2 /usr/local/bin/etcd --name my-etcd-2 --data-dir /etcd-data --listen-client-urls https://0.0.0.0:2379 --advertise-client-urls https://0.0.0.0:2377 --cert-file=/cfssl/server.pem --key-file=/cfssl/server-key.pem 21 | 22 | script: 23 | - pwd 24 | - coverage run -m unittest discover -v 25 | - coverage report 26 | 27 | after_success: 28 | - codecov 29 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # idea 104 | .idea/ -------------------------------------------------------------------------------- /aioetcd3/maintenance.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from aioetcd3._etcdv3 import rpc_pb2 as rpc 4 | from aioetcd3.base import StubMixin 5 | import aioetcd3._etcdv3.rpc_pb2_grpc as stub 6 | 7 | 8 | class Status(object): 9 | def __init__(self, status): 10 | self.version = status.version 11 | self.dbSize = status.dbSize 12 | self.leader = status.leader 13 | self.raftIndex = status.raftIndex 14 | self.raftTerm = status.raftTerm 15 | 16 | 17 | def call_grpc(request, response_func, method): 18 | 19 | def _f(f): 20 | @functools.wraps(f) 21 | async def call(self, *args, **kwargs): 22 | r = await self.grpc_call(method(self), request(*args, **kwargs)) 23 | return response_func(r) 24 | 25 | return call 26 | 27 | return _f 28 | 29 | 30 | class Maintenance(StubMixin): 31 | def _update_channel(self, channel): 32 | super()._update_channel(channel) 33 | self._maintenance_stub = stub.MaintenanceStub(channel) 34 | 35 | @call_grpc(lambda: rpc.StatusRequest(), lambda r: Status(r), 36 | lambda s: s._maintenance_stub.Status) 37 | async def status(self): 38 | pass 39 | 40 | @call_grpc(lambda action, type, mid: rpc.AlarmRequest(action=action, memberID=mid, alarm=type), 41 | lambda r: [m for m in r.alarms], lambda s: s._maintenance_stub.Alarm) 42 | async def alarm(self, action, type, mid=0): 43 | pass 44 | 45 | @call_grpc(lambda: rpc.SnapshotRequest(), lambda r: (r.remaining_bytes, r.blob), 46 | lambda s: s._maintenance_stub.Snapshot) 47 | async def snapshot(self): 48 | pass 49 | 50 | @call_grpc(lambda: rpc.HashRequest(), lambda r: r.hash, 51 | lambda s: s._maintenance_stub.Hash) 52 | async def hash(self): 53 | pass 54 | 55 | @call_grpc(lambda: rpc.DefragmentRequest(), lambda r: None, 56 | lambda s: s._maintenance_stub.Defragment) 57 | async def defragment(self): 58 | pass 59 | -------------------------------------------------------------------------------- /aioetcd3/transaction.py: -------------------------------------------------------------------------------- 1 | from aioetcd3._etcdv3 import rpc_pb2 as rpc 2 | from aioetcd3.utils import to_bytes 3 | 4 | 5 | class BaseCompare(object): 6 | def __init__(self, key): 7 | self.key = key 8 | self.value = None 9 | self.op = None 10 | 11 | # Version, Mod and Create can only be ints 12 | def __eq__(self, other): 13 | self.value = other 14 | self.op = rpc.Compare.EQUAL 15 | return self 16 | 17 | def __ne__(self, other): 18 | self.value = other 19 | self.op = rpc.Compare.NOT_EQUAL 20 | return self 21 | 22 | def __lt__(self, other): 23 | self.value = other 24 | self.op = rpc.Compare.LESS 25 | return self 26 | 27 | def __gt__(self, other): 28 | self.value = other 29 | self.op = rpc.Compare.GREATER 30 | return self 31 | 32 | def __repr__(self): 33 | return "{}: {} {} '{}'".format(self.__class__, self.key, self.op, self.value) 34 | 35 | def build_message(self): 36 | compare = rpc.Compare() 37 | compare.key = to_bytes(self.key) 38 | 39 | if self.op is None: 40 | raise ValueError('op must be one of =, < or >') 41 | 42 | compare.result = self.op 43 | 44 | self.build_compare(compare) 45 | return compare 46 | 47 | def build_compare(self, compare): 48 | raise NotImplementedError 49 | 50 | 51 | class Value(BaseCompare): 52 | def build_compare(self, compare): 53 | compare.target = rpc.Compare.VALUE 54 | compare.value = to_bytes(self.value) 55 | 56 | 57 | class Version(BaseCompare): 58 | def build_compare(self, compare): 59 | compare.target = rpc.Compare.VERSION 60 | compare.version = int(self.value) 61 | 62 | 63 | class Create(BaseCompare): 64 | def build_compare(self, compare): 65 | compare.target = rpc.Compare.CREATE 66 | compare.create_revision = int(self.value) 67 | 68 | 69 | class Mod(BaseCompare): 70 | def build_compare(self, compare): 71 | compare.target = rpc.Compare.MOD 72 | compare.mod_revision = int(self.value) 73 | -------------------------------------------------------------------------------- /aioetcd3/utils.py: -------------------------------------------------------------------------------- 1 | def to_bytes(maybe_bytestring): 2 | """ 3 | Encode string to bytes. 4 | 5 | Convenience function to do a simple encode('utf-8') if the input is not 6 | already bytes. Returns the data unmodified if the input is bytes. 7 | """ 8 | if isinstance(maybe_bytestring, bytes): 9 | return maybe_bytestring 10 | else: 11 | return maybe_bytestring.encode('utf-8') 12 | 13 | 14 | def increment_last_byte(byte_string): 15 | s = bytearray(to_bytes(byte_string)) 16 | for i in range(len(s) - 1, -1, -1): 17 | if s[i] < 0xff: 18 | s[i] += 1 19 | return bytes(s[:i+1]) 20 | else: 21 | return b'\x00' 22 | 23 | 24 | def next_valid_key(byte_string): 25 | return to_bytes(byte_string) + b'\x00' 26 | 27 | 28 | def put_key_range(obj, key_range): 29 | if isinstance(key_range, str) or isinstance(key_range, bytes): 30 | obj.key = to_bytes(key_range) 31 | else: 32 | try: 33 | key, range_end = key_range 34 | except Exception: 35 | raise ValueError("key_range must be either a str/bytes 'key', or ('key', 'range_end') tuple") 36 | obj.key = to_bytes(key) 37 | obj.range_end = to_bytes(range_end) 38 | return obj 39 | 40 | 41 | def ipv4_endpoints(server_list): 42 | return 'ipv4:///' + ','.join( 43 | s + ':2379' if ':' not in s else s 44 | for s in server_list 45 | ) 46 | 47 | 48 | def ipv6_endpoints(server_list): 49 | 50 | format_server_list = [] 51 | for ip_address in server_list: 52 | if ip_address.startswith('['): 53 | if ip_address.endswith(']'): 54 | ip_address = ip_address + ':2379' 55 | else: 56 | if not ip_address.endswith(']'): 57 | ip_address = '[' + ip_address + ']:2379' 58 | 59 | format_server_list.append(ip_address) 60 | 61 | return 'ipv6:///' + ','.join(format_server_list) 62 | 63 | 64 | def dns_endpoint(dns_name): 65 | return 'dns:///' + dns_name 66 | 67 | 68 | def get_secure_creds(ca_cert, cert_key, cert_cert): 69 | ca_cert_value = None 70 | cert_key_value = None 71 | cert_value = None 72 | if ca_cert is not None: 73 | with open(ca_cert, 'rb') as ca_cert_file: 74 | ca_cert_value = ca_cert_file.read() 75 | if cert_key is not None: 76 | with open(cert_key, 'rb') as cert_key_file: 77 | cert_key_value = cert_key_file.read() 78 | if cert_cert is not None: 79 | with open(cert_cert, 'rb') as cert_cert_file: 80 | cert_value = cert_cert_file.read() 81 | return ca_cert_value, cert_key_value, cert_value 82 | -------------------------------------------------------------------------------- /aioetcd3/help.py: -------------------------------------------------------------------------------- 1 | from aioetcd3.utils import increment_last_byte, to_bytes, next_valid_key 2 | from aioetcd3._etcdv3 import auth_pb2 as _auth 3 | from aioetcd3._etcdv3 import rpc_pb2 as _rpc 4 | 5 | SORT_ASCEND = 'ascend' 6 | SORT_DESCEND = 'descend' 7 | 8 | PER_R = _auth.Permission.READ 9 | PER_W = _auth.Permission.WRITE 10 | PER_RW = _auth.Permission.READWRITE 11 | 12 | ALARM_ACTION_GET = _rpc.AlarmRequest.GET 13 | ALARM_ACTION_ACTIVATE = _rpc.AlarmRequest.ACTIVATE 14 | ALARM_ACTION_DEACTIVATE = _rpc.AlarmRequest.DEACTIVATE 15 | 16 | ALARM_TYPE_NONE = _rpc.NONE 17 | ALARM_TYPE_NOSPACE = _rpc.NOSPACE 18 | 19 | 20 | def range_prefix(key): 21 | if not key: 22 | return range_all() 23 | else: 24 | return to_bytes(key), increment_last_byte(to_bytes(key)) 25 | 26 | 27 | def range_prefix_excluding(prefix, with_out): 28 | """ 29 | Return a list of key_range, union of which is a prefix range excluding some keys 30 | 31 | :param prefix: the key to generate the range prefix 32 | :param with_out: a list of key_range (key or (start,end) tuple) 33 | :return: a list of key_range, union of which is a prefix range excluding some keys 34 | """ 35 | return range_excluding(range_prefix(prefix), with_out) 36 | 37 | 38 | def range_excluding(range_, with_out): 39 | """ 40 | Return a list of key_range, union of which is a range excluding some keys 41 | 42 | :param range_: the original range 43 | :param with_out: a list of key_range (key or (start,end) tuple) 44 | :return: a list of key_range, union of which is a prefix range excluding some keys 45 | """ 46 | # Merge with_out 47 | with_out_ranges = [(to_bytes(v), next_valid_key(v)) if isinstance(v, str) or isinstance(v, bytes) 48 | else (to_bytes(v[0]), to_bytes(v[1])) 49 | for v in with_out] 50 | with_out_ranges.sort() 51 | range_start, range_end = range_ 52 | range_start = to_bytes(range_start) 53 | range_end = to_bytes(range_end) 54 | re_range = [] 55 | next_start_key = range_start 56 | for s, e in with_out_ranges: 57 | if s >= range_end != b'\x00': 58 | break 59 | start, end = next_start_key, s 60 | if start < end: 61 | re_range.append((start, end)) 62 | if e == b'\x00': 63 | next_start_key = None 64 | break 65 | else: 66 | next_start_key = max(next_start_key, e) 67 | if next_start_key is not None and \ 68 | (next_start_key < range_end or 69 | range_end == b'\x00'): 70 | re_range.append((next_start_key, range_end)) 71 | return re_range 72 | 73 | 74 | def range_greater(key): 75 | return next_valid_key(key), b'\0' 76 | 77 | 78 | def range_greater_equal(key): 79 | return key, b'\0' 80 | 81 | 82 | def range_less(key): 83 | return b'\0', key 84 | 85 | 86 | def range_less_equal(key): 87 | return b'\0', next_valid_key(key) 88 | 89 | 90 | def range_all(): 91 | return b'\0', b'\0' 92 | -------------------------------------------------------------------------------- /aioetcd3/cluster.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import aiogrpc 3 | import grpc 4 | 5 | from aioetcd3._etcdv3 import rpc_pb2 as rpc 6 | from aioetcd3.base import StubMixin 7 | import aioetcd3._etcdv3.rpc_pb2_grpc as stub 8 | from aioetcd3.utils import ipv4_endpoints 9 | from aioetcd3.maintenance import Maintenance 10 | 11 | 12 | def call_grpc(request, response_func, method): 13 | 14 | def _f(f): 15 | @functools.wraps(f) 16 | async def call(self, *args, **kwargs): 17 | r = await self.grpc_call(method(self), request(*args, **kwargs)) 18 | return response_func(r) 19 | 20 | return call 21 | 22 | return _f 23 | 24 | 25 | class Cluster(StubMixin): 26 | def _update_channel(self, channel): 27 | super()._update_channel(channel) 28 | self._cluster_stub = stub.ClusterStub(channel) 29 | 30 | @call_grpc(lambda peerurls: rpc.MemberAddRequest(peerURLs=peerurls), 31 | lambda r: r.member, lambda s: s._cluster_stub.MemberAdd) 32 | async def member_add(self, peerurls): 33 | pass 34 | 35 | @call_grpc(lambda mid: rpc.MemberRemoveRequest(ID=mid), 36 | lambda r: [m for m in r.members], 37 | lambda s: s._cluster_stub.MemberRemove) 38 | async def member_remove(self, mid): 39 | pass 40 | 41 | @call_grpc(lambda mid, urls: rpc.MemberUpdateRequest(ID=mid, peerURLs=urls), 42 | lambda r: [m for m in r.members], 43 | lambda s: s._cluster_stub.MemberUpdate) 44 | async def member_update(self, mid, peerurls): 45 | pass 46 | 47 | @call_grpc(lambda: rpc.MemberListRequest(), lambda r: [m for m in r.members], 48 | lambda s: s._cluster_stub.MemberList) 49 | async def member_list(self): 50 | pass 51 | 52 | async def member_healthy(self, members=None): 53 | 54 | if not members: 55 | members = await self.member_list() 56 | members = [m.clientURLs for m in members] 57 | 58 | health_members = [] 59 | unhealth_members = [] 60 | for m in members: 61 | 62 | m = [u.rpartition("//")[2] for u in m] 63 | m = [u for u in m if u] 64 | if m: 65 | server_endpoint = ipv4_endpoints(m) 66 | 67 | if self._credentials: 68 | channel = aiogrpc.secure_channel(server_endpoint, self._credentials, options=self._options, 69 | loop=self._loop, executor=self._executor, 70 | standalone_pool_for_streaming=True) 71 | else: 72 | channel = aiogrpc.insecure_channel(server_endpoint, options=self._options, loop=self._loop, 73 | executor=self._executor, standalone_pool_for_streaming=True) 74 | try: 75 | maintenance = Maintenance(channel=channel, timeout=2, username=self.username, password=self.password) 76 | try: 77 | await maintenance.status() 78 | except grpc.RpcError: 79 | unhealth_members.append(m) 80 | else: 81 | health_members.append(m) 82 | finally: 83 | await channel.close() 84 | else: 85 | unhealth_members.append(m) 86 | 87 | return health_members, unhealth_members 88 | 89 | 90 | -------------------------------------------------------------------------------- /aioetcd3/base.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from grpc import ( 3 | metadata_call_credentials, AuthMetadataPlugin, RpcError, StatusCode 4 | ) 5 | 6 | from .exceptions import AuthError, STATUS_MAP 7 | 8 | 9 | _default_timeout = object() 10 | 11 | 12 | class _EtcdTokenCallCredentials(AuthMetadataPlugin): 13 | 14 | def __init__(self, access_token): 15 | self._access_token = access_token 16 | 17 | def __call__(self, context, callback): 18 | metadata = (("token", self._access_token),) 19 | callback(metadata, None) 20 | 21 | 22 | class StubMixin(object): 23 | def __init__(self, channel, timeout, username=None, password=None): 24 | self.username = username 25 | self.password = password 26 | self.channel = channel 27 | self.timeout = timeout 28 | self._auth_lock = asyncio.Lock() 29 | self.last_response_info = None 30 | self._metadata = None 31 | self._call_credentials = None 32 | self._update_channel(channel) 33 | 34 | async def _authenticate(self): 35 | async with self._auth_lock: # Avoiding concurrent authentications for the client instance 36 | if self._metadata is not None: # Avoiding double authentication 37 | return 38 | token = await self.authenticate(username=self.username, password=self.password) 39 | self._metadata = (("token", token),) 40 | self._call_credentials = metadata_call_credentials(_EtcdTokenCallCredentials(token)) 41 | 42 | def _update_channel(self, channel): 43 | self.channel = channel 44 | self._loop = channel._loop 45 | 46 | def _update_cluster_info(self, header): 47 | self.last_response_info = header 48 | 49 | def get_cluster_info(self): 50 | return self.last_response_info 51 | 52 | async def _authenticate_if_needed(self, skip_auth=False): 53 | if self.username is not None and self.password is not None and not skip_auth: 54 | if self._metadata is None: # We need to call self._authenticate for the first rpc call only 55 | try: 56 | await self._authenticate() 57 | except RpcError as exc: 58 | if exc._state.code == StatusCode.INVALID_ARGUMENT: 59 | raise AuthError(exc._state.details, exc._state.debug_error_string) 60 | raise exc 61 | 62 | async def grpc_call(self, stub_func, request, timeout=_default_timeout, skip_auth=False): 63 | if timeout is _default_timeout: 64 | timeout = self.timeout 65 | 66 | # If the username and password are set, trying to call the auth.authenticate 67 | # method to get the auth token. If the token already received - just use it. 68 | await self._authenticate_if_needed(skip_auth=skip_auth) 69 | 70 | try: 71 | response = await stub_func( 72 | request, timeout=timeout, credentials=self._call_credentials, metadata=self._metadata 73 | ) 74 | except RpcError as exc: 75 | _process_rpc_error(exc) 76 | self._update_cluster_info(response.header) 77 | return response 78 | 79 | 80 | def _process_rpc_error(exc: RpcError): 81 | """Wraps grpc.RpcError to a specific library's exception. 82 | If there is no specific exception found in the map, the original 83 | exception will be raised 84 | """ 85 | try: 86 | new_exc = STATUS_MAP.get(exc._state.code) 87 | if new_exc is not None: 88 | raise new_exc(exc._state.details, exc._state.debug_error_string) 89 | except AttributeError: 90 | pass 91 | raise exc 92 | -------------------------------------------------------------------------------- /aioetcd3/lease.py: -------------------------------------------------------------------------------- 1 | from aioetcd3._etcdv3 import rpc_pb2 as rpc 2 | from aioetcd3.base import StubMixin 3 | 4 | import functools 5 | import inspect 6 | import asyncio 7 | import aioetcd3._etcdv3.rpc_pb2_grpc as stub 8 | 9 | 10 | def call_grpc(request, response_func, method): 11 | 12 | def _f(f): 13 | @functools.wraps(f) 14 | async def call(self, *args, **kwargs): 15 | params = inspect.getcallargs(f, self, *args, **kwargs) 16 | params.pop('self') 17 | r = await self.grpc_call(method(self), request(**params)) 18 | return response_func(r, client=self) 19 | 20 | return call 21 | 22 | return _f 23 | 24 | 25 | class RLease(object): 26 | def __init__(self, ttl, id, client): 27 | self.ttl = ttl 28 | self.id = id 29 | self.client = client 30 | 31 | async def __aenter__(self): 32 | lease = await self.client.grant_lease(ttl=self.ttl) 33 | self.ttl = lease.ttl 34 | self.id = lease.id 35 | 36 | refresh_ttl = self.ttl // 2 37 | 38 | async def task(cycle): 39 | while True: 40 | await asyncio.sleep(cycle) 41 | await self.refresh() 42 | 43 | self.refresh_task = asyncio.ensure_future(task(refresh_ttl)) 44 | return self 45 | 46 | async def __aexit__(self, exc_type, exc_val, exc_tb): 47 | if hasattr(self, 'refresh_task'): 48 | self.refresh_task.cancel() 49 | await asyncio.wait([self.refresh_task]) 50 | await self.revoke() 51 | 52 | async def revoke(self): 53 | return await self.client.revoke_lease(self.id) 54 | 55 | async def refresh(self): 56 | return await self.client.refresh_lease(self.id) 57 | 58 | async def info(self): 59 | return await self.client.get_lease_info(self.id) 60 | 61 | 62 | class Lease(StubMixin): 63 | 64 | def _update_channel(self, channel): 65 | super()._update_channel(channel) 66 | self._lease_stub = stub.LeaseStub(channel) 67 | 68 | @call_grpc(lambda ttl, id: rpc.LeaseGrantRequest(TTL=ttl, ID=id), 69 | lambda r, client: RLease(r.TTL, r.ID, client), 70 | lambda s: s._lease_stub.LeaseGrant) 71 | async def grant_lease(self, ttl, id=0): 72 | pass 73 | 74 | def grant_lease_scope(self, ttl, id=0): 75 | return RLease(ttl, id, self) 76 | 77 | @call_grpc(lambda lease: rpc.LeaseRevokeRequest(ID=get_lease_id(lease)), 78 | lambda r, client: None, lambda s: s._lease_stub.LeaseRevoke) 79 | async def revoke_lease(self, lease): 80 | pass 81 | 82 | async def refresh_lease(self, lease): 83 | lease_id = get_lease_id(lease) 84 | lease_request = rpc.LeaseKeepAliveRequest(ID=lease_id) 85 | 86 | async def generate_request(request): 87 | for re in [request]: 88 | yield re 89 | 90 | await self._authenticate_if_needed() 91 | new_lease = None 92 | async with self._lease_stub.LeaseKeepAlive.with_scope( 93 | generate_request(lease_request), 94 | credentials=self._call_credentials, 95 | metadata=self._metadata 96 | ) as result: 97 | async for r in result: 98 | self._update_cluster_info(r.header) 99 | new_lease = RLease(r.TTL, r.ID, self) 100 | 101 | return new_lease 102 | 103 | @call_grpc(lambda lease: rpc.LeaseTimeToLiveRequest(ID=get_lease_id(lease), keys=True), 104 | lambda r, client: (RLease(r.TTL, r.ID, client), [k for k in r.keys]) if r.TTL >= 0 else (None, []), 105 | lambda s: s._lease_stub.LeaseTimeToLive) 106 | async def get_lease_info(self, lease): 107 | pass 108 | 109 | 110 | def get_lease_id(lease): 111 | if hasattr(lease, 'id'): 112 | return lease.id 113 | else: 114 | return lease 115 | 116 | -------------------------------------------------------------------------------- /test/test_lease.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import asyncio 3 | import functools 4 | 5 | from aioetcd3.client import client 6 | from aioetcd3.help import range_all, range_prefix, PER_RW 7 | 8 | from .utils import switch_auth_on, switch_auth_off 9 | 10 | 11 | def asynctest(f): 12 | @functools.wraps(f) 13 | def _f(self): 14 | asyncio.get_event_loop().run_until_complete(f(self)) 15 | 16 | return _f 17 | 18 | 19 | class LeaseTest(unittest.TestCase): 20 | @asynctest 21 | async def setUp(self): 22 | self.endpoints = "127.0.0.1:2379" 23 | self.client = client(endpoint=self.endpoints) 24 | 25 | await self.cleanUp() 26 | 27 | async def _lease_1(self): 28 | lease = await self.client.grant_lease(ttl=5) 29 | self.assertEqual(lease.ttl, 5) 30 | 31 | await asyncio.sleep(1) 32 | lease, keys = await self.client.get_lease_info(lease) 33 | self.assertLessEqual(lease.ttl, 4) 34 | self.assertEqual(len(keys), 0) 35 | 36 | lease = await self.client.refresh_lease(lease) 37 | self.assertEqual(lease.ttl, 5) 38 | 39 | await self.client.revoke_lease(lease) 40 | 41 | lease, keys = await self.client.get_lease_info(lease) 42 | self.assertIsNone(lease) 43 | self.assertEqual(len(keys), 0) 44 | 45 | @asynctest 46 | async def test_lease_1(self): 47 | await self._lease_1() 48 | 49 | async def _lease_2(self): 50 | lease = await self.client.grant_lease(ttl=5) 51 | self.assertEqual(lease.ttl, 5) 52 | 53 | await asyncio.sleep(1) 54 | lease, keys = await lease.info() 55 | self.assertLessEqual(lease.ttl, 4) 56 | self.assertEqual(len(keys), 0) 57 | 58 | lease = await lease.refresh() 59 | self.assertEqual(lease.ttl, 5) 60 | 61 | await lease.revoke() 62 | lease, keys = await lease.info() 63 | self.assertIsNone(lease) 64 | self.assertEqual(len(keys), 0) 65 | 66 | lease = None 67 | async with self.client.grant_lease_scope(ttl=5) as l: 68 | lease = l 69 | await asyncio.sleep(1) 70 | 71 | lease, keys = await lease.info() 72 | self.assertIsNone(lease) 73 | self.assertEqual(len(keys), 0) 74 | 75 | @asynctest 76 | async def test_lease_2(self): 77 | await self._lease_2() 78 | 79 | async def _lease_3(self): 80 | lease = await self.client.grant_lease(ttl=5) 81 | self.assertEqual(lease.ttl, 5) 82 | 83 | await self.client.put("/testlease", "testlease", lease=lease) 84 | 85 | await asyncio.sleep(6) 86 | lease, keys = await lease.info() 87 | self.assertIsNone(lease, None) 88 | self.assertEqual(len(keys), 0) 89 | 90 | value, meta = await self.client.get('/testlease') 91 | self.assertIsNone(value) 92 | self.assertIsNone(meta) 93 | 94 | @asynctest 95 | async def test_lease_3(self): 96 | await self._lease_3() 97 | 98 | async def _run_test_with_auth(self, test): 99 | default_client = self.client 100 | await switch_auth_on(default_client) 101 | root_client = client(endpoint=self.endpoints, username="root", password="root") 102 | await root_client.role_grant_permission(name='client', key_range=range_prefix('/testlease'), permission=PER_RW) 103 | self.client = client(endpoint=self.endpoints, username="client", password="client") 104 | try: 105 | await test() 106 | finally: 107 | await switch_auth_off( 108 | root_client, 109 | default_client 110 | ) 111 | await root_client.close() 112 | await self.client.close() 113 | self.client = default_client 114 | 115 | @asynctest 116 | async def test_lease_1_with_auth(self): 117 | await self._run_test_with_auth(self._lease_1) 118 | 119 | @asynctest 120 | async def test_lease_2_with_auth(self): 121 | await self._run_test_with_auth(self._lease_2) 122 | 123 | @asynctest 124 | async def test_lease_3_with_auth(self): 125 | await self._run_test_with_auth(self._lease_3) 126 | 127 | @asynctest 128 | async def tearDown(self): 129 | await self.cleanUp() 130 | await self.client.close() 131 | 132 | async def cleanUp(self): 133 | await self.client.delete(range_all()) 134 | -------------------------------------------------------------------------------- /aioetcd3/auth.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from aioetcd3.base import StubMixin 4 | from aioetcd3._etcdv3 import rpc_pb2 as rpc 5 | from aioetcd3._etcdv3 import auth_pb2 as auth 6 | from aioetcd3.utils import put_key_range 7 | import aioetcd3._etcdv3.rpc_pb2_grpc as stub 8 | 9 | 10 | def call_grpc(request, response_func, method, skip_auth=False): 11 | 12 | def _f(f): 13 | @functools.wraps(f) 14 | async def call(self, *args, **kwargs): 15 | r = await self.grpc_call(method(self), request(*args, **kwargs), skip_auth=skip_auth) 16 | return response_func(r) 17 | 18 | return call 19 | 20 | return _f 21 | 22 | 23 | class Auth(StubMixin): 24 | def _update_channel(self, channel): 25 | super()._update_channel(channel) 26 | self._auth_stub = stub.AuthStub(channel) 27 | 28 | @call_grpc(lambda: rpc.AuthEnableRequest(), lambda r: None, lambda s: s._auth_stub.AuthEnable) 29 | async def auth_enable(self): 30 | pass 31 | 32 | @call_grpc(lambda: rpc.AuthDisableRequest(), lambda r: None, lambda s: s._auth_stub.AuthDisable) 33 | async def auth_disable(self): 34 | pass 35 | 36 | # The method should be called without password authentication to avoid the infinite recursion 37 | @call_grpc(lambda username, password: rpc.AuthenticateRequest(name=username, password=password), 38 | lambda r: r.token, lambda s: s._auth_stub.Authenticate, skip_auth=True) 39 | async def authenticate(self, username, password): 40 | pass 41 | 42 | @call_grpc(lambda: rpc.AuthUserListRequest(), lambda r: [u for u in r.users], lambda s: s._auth_stub.UserList) 43 | async def user_list(self): 44 | pass 45 | 46 | @call_grpc(lambda username: rpc.AuthUserGetRequest(name=username), lambda r: [r for r in r.roles], 47 | lambda s: s._auth_stub.UserGet) 48 | async def user_get(self, username): 49 | pass 50 | 51 | @call_grpc(lambda username, password: rpc.AuthUserAddRequest(name=username, password=password), lambda r: None, 52 | lambda s: s._auth_stub.UserAdd) 53 | async def user_add(self, username, password): 54 | pass 55 | 56 | @call_grpc(lambda username: rpc.AuthUserDeleteRequest(name=username), lambda r: None, 57 | lambda s: s._auth_stub.UserDelete) 58 | async def user_delete(self, username): 59 | pass 60 | 61 | @call_grpc(lambda username, password: rpc.AuthUserChangePasswordRequest(name=username, password=password), 62 | lambda r: None, lambda s: s._auth_stub.UserChangePassword) 63 | async def user_change_password(self, username, password): 64 | pass 65 | 66 | @call_grpc(lambda username, role: rpc.AuthUserGrantRoleRequest(user=username, role=role), lambda r: None, 67 | lambda s: s._auth_stub.UserGrantRole) 68 | async def user_grant_role(self, username, role): 69 | pass 70 | 71 | @call_grpc(lambda username, role: rpc.AuthUserRevokeRoleRequest(name=username, role=role), lambda r: None, 72 | lambda s: s._auth_stub.UserRevokeRole) 73 | async def user_revoke_role(self, username, role): 74 | pass 75 | 76 | @call_grpc(lambda: rpc.AuthRoleListRequest(), lambda r: [role for role in r.roles], 77 | lambda s: s._auth_stub.RoleList) 78 | async def role_list(self): 79 | pass 80 | 81 | @call_grpc(lambda name: rpc.AuthRoleGetRequest(role=name), lambda r: [p for p in r.perm], 82 | lambda s: s._auth_stub.RoleGet) 83 | async def role_get(self, name): 84 | pass 85 | 86 | @call_grpc(lambda name: rpc.AuthRoleAddRequest(name=name), lambda r: None, lambda s: s._auth_stub.RoleAdd) 87 | async def role_add(self, name): 88 | pass 89 | 90 | @call_grpc(lambda name: rpc.AuthRoleDeleteRequest(role=name), lambda r: None, lambda s: s._auth_stub.RoleDelete) 91 | async def role_delete(self, name): 92 | pass 93 | 94 | @staticmethod 95 | def role_grant_request(name, key_range, permission): 96 | if permission not in [auth.Permission.READ, auth.Permission.WRITE, auth.Permission.READWRITE]: 97 | raise ValueError("permission must be read, write or readwrite") 98 | per = auth.Permission(permType=permission) 99 | put_key_range(per, key_range) 100 | 101 | request = rpc.AuthRoleGrantPermissionRequest(name=name, perm=per) 102 | 103 | return request 104 | 105 | @call_grpc(role_grant_request.__func__, lambda r: None, lambda s: s._auth_stub.RoleGrantPermission) 106 | async def role_grant_permission(self, name, key_range, permission): 107 | pass 108 | 109 | @staticmethod 110 | def role_revoke_request(name, key_range): 111 | request = rpc.AuthRoleRevokePermissionRequest(role=name) 112 | put_key_range(request, key_range) 113 | 114 | return request 115 | 116 | @call_grpc(role_revoke_request.__func__, lambda r: None, lambda s: s._auth_stub.RoleRevokePermission) 117 | async def role_revoke_permission(self, name, key_range): 118 | pass 119 | -------------------------------------------------------------------------------- /aioetcd3/client.py: -------------------------------------------------------------------------------- 1 | import aiogrpc 2 | import os 3 | import logging 4 | from aiogrpc.channel import Channel 5 | from aioetcd3.kv import KV 6 | from aioetcd3.lease import Lease 7 | from aioetcd3.auth import Auth 8 | from aioetcd3.watch import Watch 9 | from aioetcd3.maintenance import Maintenance 10 | from aioetcd3.cluster import Cluster 11 | from aioetcd3.utils import get_secure_creds 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class Client(KV, Lease, Auth, Watch, Maintenance, Cluster): 17 | def __init__(self, endpoint, ssl=False, 18 | ca_cert=None, cert_key=None, cert_cert=None, 19 | default_ca=False, grpc_options=None, timeout=5, 20 | username=None, password=None, 21 | *, loop=None, executor=None): 22 | channel = self._create_grpc_channel(endpoint=endpoint, ssl=ssl, 23 | ca_cert=ca_cert, 24 | cert_key=cert_key, cert_cert=cert_cert, 25 | default_ca=default_ca, 26 | options=grpc_options, 27 | loop=loop, 28 | executor=executor) 29 | if cert_key and cert_cert and username and password: 30 | logger.warning("Certificate and password authentication methods are used simultaneously") 31 | super().__init__(channel, timeout, username=username, password=password) 32 | 33 | def update_server_list(self, endpoint): 34 | self.close() 35 | channel = self._recreate_grpc_channel(endpoint) 36 | self._update_channel(channel) 37 | 38 | def _create_grpc_channel(self, endpoint, ssl=False, 39 | ca_cert=None, cert_key=None, cert_cert=None, default_ca=False, options=None, 40 | *, loop=None, executor=None): 41 | credentials = None 42 | if not ssl: 43 | channel = aiogrpc.insecure_channel(endpoint, options=options, loop=loop, executor=executor, 44 | standalone_pool_for_streaming=True) 45 | else: 46 | if default_ca: 47 | ca_cert = None 48 | else: 49 | if ca_cert is None: 50 | logger.warning("Certificate authority is not specified. Empty CA will be used. To use system CA set" 51 | " `default_ca=True`") 52 | ca_cert = '' 53 | 54 | # to ensure ssl connect , set grpc env 55 | # os.environ['GRPC_SSL_CIPHER_SUITES'] = 'ECDHE-ECDSA-AES256-GCM-SHA384' 56 | 57 | credentials = aiogrpc.ssl_channel_credentials(ca_cert, cert_key, cert_cert) 58 | channel = aiogrpc.secure_channel(endpoint, credentials, options=options, 59 | loop=loop, executor=executor, 60 | standalone_pool_for_streaming=True) 61 | 62 | # Save parameters for auto-recreate 63 | self._credentials = credentials 64 | self._options = options 65 | self._loop = channel._loop 66 | self._executor = executor 67 | return channel 68 | 69 | def _recreate_grpc_channel(self, endpoint): 70 | self._call_credentials = None 71 | self._metadata = None 72 | if self._credentials: 73 | channel = aiogrpc.secure_channel(endpoint, self._credentials, options=self._options, 74 | loop=self._loop, executor=self._executor, 75 | standalone_pool_for_streaming=True) 76 | else: 77 | channel = aiogrpc.insecure_channel(endpoint, options=self._options, loop=self._loop, 78 | executor=self._executor, standalone_pool_for_streaming=True) 79 | return channel 80 | 81 | def close(self): 82 | return self.channel.close() 83 | 84 | 85 | def client(endpoint, grpc_options=None, timeout=None, username=None, password=None): 86 | 87 | # user `ip:port,ip:port` to user grpc balance 88 | return Client(endpoint, grpc_options=grpc_options, username=username, password=password, timeout=timeout) 89 | 90 | 91 | def ssl_client(endpoint, ca_file=None, cert_file=None, key_file=None, default_ca=False, grpc_options=None, 92 | timeout=None, username=None, password=None): 93 | ca, key, cert = get_secure_creds(ca_cert=ca_file, cert_cert=cert_file, cert_key=key_file) 94 | return Client(endpoint, ssl=True, ca_cert=ca, cert_key=key, cert_cert=cert, 95 | default_ca=default_ca, grpc_options=grpc_options, timeout=timeout, 96 | username=username, password=password) 97 | 98 | 99 | def set_grpc_cipher(enable_rsa=True, enable_ecdsa=True, ciphers=None): 100 | """ 101 | Set GRPC_SSL_CIPHER_SUITES environment variable to change the SSL cipher 102 | used by GRPC. By default the GRPC C core only supports RSA. 103 | 104 | :param enable_rsa: Enable RSA cipher 105 | :param enable_ecdsa: Enable ECDSA cipher 106 | :param ciphers: Override the cipher list to a list of strings 107 | """ 108 | if ciphers: 109 | os.environ['GRPC_SSL_CIPHER_SUITES'] = ':'.join(ciphers) 110 | else: 111 | rsa_ciphers = 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:' \ 112 | 'ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-GCM-SHA384' 113 | ecdsa_ciphers = 'ECDHE-ECDSA-AES256-GCM-SHA384' 114 | if enable_rsa: 115 | if enable_ecdsa: 116 | env = rsa_ciphers + ':' + ecdsa_ciphers 117 | else: 118 | env = rsa_ciphers 119 | else: 120 | if enable_ecdsa: 121 | env = ecdsa_ciphers 122 | else: 123 | env = None 124 | if env is None: 125 | if 'GRPC_SSL_CIPHER_SUITES' in os.environ: 126 | del os.environ['GRPC_SSL_CIPHER_SUITES'] 127 | else: 128 | os.environ['GRPC_SSL_CIPHER_SUITES'] = env 129 | -------------------------------------------------------------------------------- /test/test_kv.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import asyncio 3 | import functools 4 | from aioetcd3.client import client 5 | from aioetcd3.kv import KV 6 | from aioetcd3.help import range_all, range_prefix, range_greater, range_greater_equal 7 | from aioetcd3 import transaction 8 | 9 | 10 | def asynctest(f): 11 | @functools.wraps(f) 12 | def _f(self): 13 | return asyncio.get_event_loop().run_until_complete(f(self)) 14 | 15 | return _f 16 | 17 | 18 | class KVTest(unittest.TestCase): 19 | 20 | @asynctest 21 | async def setUp(self): 22 | endpoints = "127.0.0.1:2379" 23 | self.client = client(endpoint=endpoints) 24 | endpoints = "127.0.0.1:2379" 25 | self.client.update_server_list(endpoint=endpoints) 26 | await self.cleanUp() 27 | 28 | async def cleanUp(self): 29 | await self.client.delete(key_range=range_all()) 30 | 31 | @asynctest 32 | async def tearDown(self): 33 | await self.cleanUp() 34 | await self.client.close() 35 | 36 | @asynctest 37 | async def test_put_get(self): 38 | 39 | for i in range(0, 10): 40 | key = '/test' + str(i) 41 | value, meta = await self.client.put(key, str(i)) 42 | self.assertIsNone(value) 43 | self.assertIsNone(meta) 44 | 45 | value, meta = await self.client.put('/test9', "10", prev_kv=True) 46 | self.assertEqual(value, b'9') 47 | self.assertIsNotNone(meta) 48 | 49 | value, meta = await self.client.put('/test9', "9", prev_kv=True, ignore_value=True) 50 | self.assertEqual(value, b'10') 51 | self.assertIsNotNone(meta) 52 | 53 | value, meta = await self.client.put('/test9', "9", prev_kv=True) 54 | self.assertEqual(value, b'10') 55 | self.assertIsNotNone(meta) 56 | 57 | count = await self.client.count(key_range=range_all()) 58 | self.assertEqual(count, 10) 59 | 60 | value, meta = await self.client.get("/test9") 61 | self.assertEqual(value, b'9') 62 | self.assertIsNotNone(meta) 63 | 64 | keys_list = await self.client.range_keys(key_range=range_all()) 65 | self.assertEqual(len(keys_list), 10) 66 | 67 | value_list = await self.client.range(key_range=range_all()) 68 | self.assertEqual(len(value_list), 10) 69 | value = [v[1].decode('utf-8') for v in value_list] 70 | value.sort() 71 | real_value = [str(i) for i in range(0, 10)] 72 | self.assertEqual(value, real_value) 73 | 74 | value_list = await self.client.range(key_range=range_all(), limit=5) 75 | self.assertEqual(len(value_list), 5) 76 | 77 | value_list = await self.client.range(key_range=range_prefix('/')) 78 | self.assertEqual(len(value_list), 10) 79 | 80 | value_list = await self.client.range(key_range=range_prefix('/'), limit=11) 81 | self.assertEqual(len(value_list), 10) 82 | 83 | value_list = await self.client.range(key_range=range_greater_equal('/test8')) 84 | self.assertEqual(len(value_list), 2) 85 | self.assertEqual(value_list[0][1], b'8') 86 | self.assertEqual(value_list[1][1], b'9') 87 | 88 | value_list = await self.client.range(key_range=range_greater('/testa')) 89 | self.assertEqual(len(value_list), 0) 90 | 91 | await self.client.delete(key_range='/test9') 92 | value, meta = await self.client.get("/test9") 93 | self.assertIsNone(value) 94 | self.assertIsNone(meta) 95 | 96 | value_list = await self.client.pop(key_range='/test8') 97 | self.assertEqual(len(value_list), 1) 98 | self.assertEqual(value_list[0][0], b'/test8') 99 | self.assertEqual(value_list[0][1], b'8') 100 | 101 | value_list = await self.client.delete(key_range=range_prefix('/'), prev_kv=True) 102 | self.assertEqual(len(value_list), 8) 103 | 104 | @asynctest 105 | async def test_transaction(self): 106 | await self.client.put('/trans1', 'trans1') 107 | await self.client.put('/trans2', 'trans2') 108 | 109 | is_success, response = await self.client.txn(compare=[ 110 | transaction.Value('/trans1') == b'trans1', 111 | transaction.Value('/trans2') == b'trans2' 112 | ], success=[ 113 | KV.get.txn('/trans1'), 114 | KV.range.txn('/trans2') 115 | ], fail=[ 116 | KV.delete.txn('/trans1') 117 | ]) 118 | 119 | self.assertEqual(is_success, True) 120 | self.assertEqual(len(response), 2) 121 | 122 | self.assertEqual(response[0][0], b'trans1') 123 | self.assertEqual(response[1][0][:2], (b'/trans2', b'trans2')) 124 | 125 | is_success, response = await self.client.txn(compare=[ 126 | transaction.Value('/trans1') == b'trans1', 127 | transaction.Value('/trans2') == b'trans2' 128 | ], success=[ 129 | KV.delete.txn('/trans1'), 130 | KV.put.txn('/trans2', 'trans2', prev_kv=True), 131 | KV.put.txn('/trans3', 'trans3', prev_kv=True) 132 | ], fail=[ 133 | KV.delete.txn('/trans1') 134 | ]) 135 | 136 | self.assertEqual(is_success, True) 137 | self.assertEqual(len(response), 3) 138 | del_response = response[0] 139 | self.assertEqual(del_response, 1) 140 | put_response = response[1] 141 | self.assertEqual(put_response[0], b'trans2') 142 | put_response = response[2] 143 | # there is not pre_kv None 144 | self.assertIsNone(put_response[0]) 145 | 146 | is_success, response = await self.client.txn(compare=[ 147 | transaction.Value('/trans3') != b'trans3', 148 | transaction.Version('/trans3') < 1000, 149 | transaction.Mod('/trans3') > 100, 150 | transaction.Create('/trans3') != 200 151 | ], success=[ 152 | ], fail=[ 153 | KV.delete.txn('/trans3', prev_kv=True) 154 | ]) 155 | 156 | self.assertEqual(is_success, False) 157 | self.assertEqual(len(response), 1) 158 | self.assertEqual(len(response[0]), 1) 159 | self.assertEqual(response[0][0][:2], (b'/trans3', b'trans3')) 160 | 161 | if __name__ == '__main__': 162 | unittest.main() 163 | 164 | -------------------------------------------------------------------------------- /aioetcd3/_etcdv3/kv_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # source: kv.proto 3 | 4 | import sys 5 | _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import message as _message 8 | from google.protobuf import reflection as _reflection 9 | from google.protobuf import symbol_database as _symbol_database 10 | from google.protobuf import descriptor_pb2 11 | # @@protoc_insertion_point(imports) 12 | 13 | _sym_db = _symbol_database.Default() 14 | 15 | 16 | 17 | 18 | DESCRIPTOR = _descriptor.FileDescriptor( 19 | name='kv.proto', 20 | package='mvccpb', 21 | syntax='proto3', 22 | serialized_pb=_b('\n\x08kv.proto\x12\x06mvccpb\"u\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\x17\n\x0f\x63reate_revision\x18\x02 \x01(\x03\x12\x14\n\x0cmod_revision\x18\x03 \x01(\x03\x12\x0f\n\x07version\x18\x04 \x01(\x03\x12\r\n\x05value\x18\x05 \x01(\x0c\x12\r\n\x05lease\x18\x06 \x01(\x03\"\x91\x01\n\x05\x45vent\x12%\n\x04type\x18\x01 \x01(\x0e\x32\x17.mvccpb.Event.EventType\x12\x1c\n\x02kv\x18\x02 \x01(\x0b\x32\x10.mvccpb.KeyValue\x12!\n\x07prev_kv\x18\x03 \x01(\x0b\x32\x10.mvccpb.KeyValue\" \n\tEventType\x12\x07\n\x03PUT\x10\x00\x12\n\n\x06\x44\x45LETE\x10\x01\x62\x06proto3') 23 | ) 24 | 25 | 26 | 27 | _EVENT_EVENTTYPE = _descriptor.EnumDescriptor( 28 | name='EventType', 29 | full_name='mvccpb.Event.EventType', 30 | filename=None, 31 | file=DESCRIPTOR, 32 | values=[ 33 | _descriptor.EnumValueDescriptor( 34 | name='PUT', index=0, number=0, 35 | options=None, 36 | type=None), 37 | _descriptor.EnumValueDescriptor( 38 | name='DELETE', index=1, number=1, 39 | options=None, 40 | type=None), 41 | ], 42 | containing_type=None, 43 | options=None, 44 | serialized_start=253, 45 | serialized_end=285, 46 | ) 47 | _sym_db.RegisterEnumDescriptor(_EVENT_EVENTTYPE) 48 | 49 | 50 | _KEYVALUE = _descriptor.Descriptor( 51 | name='KeyValue', 52 | full_name='mvccpb.KeyValue', 53 | filename=None, 54 | file=DESCRIPTOR, 55 | containing_type=None, 56 | fields=[ 57 | _descriptor.FieldDescriptor( 58 | name='key', full_name='mvccpb.KeyValue.key', index=0, 59 | number=1, type=12, cpp_type=9, label=1, 60 | has_default_value=False, default_value=_b(""), 61 | message_type=None, enum_type=None, containing_type=None, 62 | is_extension=False, extension_scope=None, 63 | options=None), 64 | _descriptor.FieldDescriptor( 65 | name='create_revision', full_name='mvccpb.KeyValue.create_revision', index=1, 66 | number=2, type=3, cpp_type=2, label=1, 67 | has_default_value=False, default_value=0, 68 | message_type=None, enum_type=None, containing_type=None, 69 | is_extension=False, extension_scope=None, 70 | options=None), 71 | _descriptor.FieldDescriptor( 72 | name='mod_revision', full_name='mvccpb.KeyValue.mod_revision', index=2, 73 | number=3, type=3, cpp_type=2, label=1, 74 | has_default_value=False, default_value=0, 75 | message_type=None, enum_type=None, containing_type=None, 76 | is_extension=False, extension_scope=None, 77 | options=None), 78 | _descriptor.FieldDescriptor( 79 | name='version', full_name='mvccpb.KeyValue.version', index=3, 80 | number=4, type=3, cpp_type=2, label=1, 81 | has_default_value=False, default_value=0, 82 | message_type=None, enum_type=None, containing_type=None, 83 | is_extension=False, extension_scope=None, 84 | options=None), 85 | _descriptor.FieldDescriptor( 86 | name='value', full_name='mvccpb.KeyValue.value', index=4, 87 | number=5, type=12, cpp_type=9, label=1, 88 | has_default_value=False, default_value=_b(""), 89 | message_type=None, enum_type=None, containing_type=None, 90 | is_extension=False, extension_scope=None, 91 | options=None), 92 | _descriptor.FieldDescriptor( 93 | name='lease', full_name='mvccpb.KeyValue.lease', index=5, 94 | number=6, type=3, cpp_type=2, label=1, 95 | has_default_value=False, default_value=0, 96 | message_type=None, enum_type=None, containing_type=None, 97 | is_extension=False, extension_scope=None, 98 | options=None), 99 | ], 100 | extensions=[ 101 | ], 102 | nested_types=[], 103 | enum_types=[ 104 | ], 105 | options=None, 106 | is_extendable=False, 107 | syntax='proto3', 108 | extension_ranges=[], 109 | oneofs=[ 110 | ], 111 | serialized_start=20, 112 | serialized_end=137, 113 | ) 114 | 115 | 116 | _EVENT = _descriptor.Descriptor( 117 | name='Event', 118 | full_name='mvccpb.Event', 119 | filename=None, 120 | file=DESCRIPTOR, 121 | containing_type=None, 122 | fields=[ 123 | _descriptor.FieldDescriptor( 124 | name='type', full_name='mvccpb.Event.type', index=0, 125 | number=1, type=14, cpp_type=8, label=1, 126 | has_default_value=False, default_value=0, 127 | message_type=None, enum_type=None, containing_type=None, 128 | is_extension=False, extension_scope=None, 129 | options=None), 130 | _descriptor.FieldDescriptor( 131 | name='kv', full_name='mvccpb.Event.kv', index=1, 132 | number=2, type=11, cpp_type=10, label=1, 133 | has_default_value=False, default_value=None, 134 | message_type=None, enum_type=None, containing_type=None, 135 | is_extension=False, extension_scope=None, 136 | options=None), 137 | _descriptor.FieldDescriptor( 138 | name='prev_kv', full_name='mvccpb.Event.prev_kv', index=2, 139 | number=3, type=11, cpp_type=10, label=1, 140 | has_default_value=False, default_value=None, 141 | message_type=None, enum_type=None, containing_type=None, 142 | is_extension=False, extension_scope=None, 143 | options=None), 144 | ], 145 | extensions=[ 146 | ], 147 | nested_types=[], 148 | enum_types=[ 149 | _EVENT_EVENTTYPE, 150 | ], 151 | options=None, 152 | is_extendable=False, 153 | syntax='proto3', 154 | extension_ranges=[], 155 | oneofs=[ 156 | ], 157 | serialized_start=140, 158 | serialized_end=285, 159 | ) 160 | 161 | _EVENT.fields_by_name['type'].enum_type = _EVENT_EVENTTYPE 162 | _EVENT.fields_by_name['kv'].message_type = _KEYVALUE 163 | _EVENT.fields_by_name['prev_kv'].message_type = _KEYVALUE 164 | _EVENT_EVENTTYPE.containing_type = _EVENT 165 | DESCRIPTOR.message_types_by_name['KeyValue'] = _KEYVALUE 166 | DESCRIPTOR.message_types_by_name['Event'] = _EVENT 167 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 168 | 169 | KeyValue = _reflection.GeneratedProtocolMessageType('KeyValue', (_message.Message,), dict( 170 | DESCRIPTOR = _KEYVALUE, 171 | __module__ = 'kv_pb2' 172 | # @@protoc_insertion_point(class_scope:mvccpb.KeyValue) 173 | )) 174 | _sym_db.RegisterMessage(KeyValue) 175 | 176 | Event = _reflection.GeneratedProtocolMessageType('Event', (_message.Message,), dict( 177 | DESCRIPTOR = _EVENT, 178 | __module__ = 'kv_pb2' 179 | # @@protoc_insertion_point(class_scope:mvccpb.Event) 180 | )) 181 | _sym_db.RegisterMessage(Event) 182 | 183 | 184 | try: 185 | # THESE ELEMENTS WILL BE DEPRECATED. 186 | # Please use the generated *_pb2_grpc.py files instead. 187 | import grpc 188 | from grpc.beta import implementations as beta_implementations 189 | from grpc.beta import interfaces as beta_interfaces 190 | from grpc.framework.common import cardinality 191 | from grpc.framework.interfaces.face import utilities as face_utilities 192 | except ImportError: 193 | pass 194 | # @@protoc_insertion_point(module_scope) 195 | -------------------------------------------------------------------------------- /aioetcd3/_etcdv3/auth_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # source: auth.proto 3 | 4 | import sys 5 | _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import message as _message 8 | from google.protobuf import reflection as _reflection 9 | from google.protobuf import symbol_database as _symbol_database 10 | from google.protobuf import descriptor_pb2 11 | # @@protoc_insertion_point(imports) 12 | 13 | _sym_db = _symbol_database.Default() 14 | 15 | 16 | 17 | 18 | DESCRIPTOR = _descriptor.FileDescriptor( 19 | name='auth.proto', 20 | package='authpb', 21 | syntax='proto3', 22 | serialized_pb=_b('\n\nauth.proto\x12\x06\x61uthpb\"5\n\x04User\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12\x10\n\x08password\x18\x02 \x01(\x0c\x12\r\n\x05roles\x18\x03 \x03(\t\"\x83\x01\n\nPermission\x12)\n\x08permType\x18\x01 \x01(\x0e\x32\x17.authpb.Permission.Type\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x11\n\trange_end\x18\x03 \x01(\x0c\"*\n\x04Type\x12\x08\n\x04READ\x10\x00\x12\t\n\x05WRITE\x10\x01\x12\r\n\tREADWRITE\x10\x02\"?\n\x04Role\x12\x0c\n\x04name\x18\x01 \x01(\x0c\x12)\n\rkeyPermission\x18\x02 \x03(\x0b\x32\x12.authpb.Permissionb\x06proto3') 23 | ) 24 | 25 | 26 | 27 | _PERMISSION_TYPE = _descriptor.EnumDescriptor( 28 | name='Type', 29 | full_name='authpb.Permission.Type', 30 | filename=None, 31 | file=DESCRIPTOR, 32 | values=[ 33 | _descriptor.EnumValueDescriptor( 34 | name='READ', index=0, number=0, 35 | options=None, 36 | type=None), 37 | _descriptor.EnumValueDescriptor( 38 | name='WRITE', index=1, number=1, 39 | options=None, 40 | type=None), 41 | _descriptor.EnumValueDescriptor( 42 | name='READWRITE', index=2, number=2, 43 | options=None, 44 | type=None), 45 | ], 46 | containing_type=None, 47 | options=None, 48 | serialized_start=167, 49 | serialized_end=209, 50 | ) 51 | _sym_db.RegisterEnumDescriptor(_PERMISSION_TYPE) 52 | 53 | 54 | _USER = _descriptor.Descriptor( 55 | name='User', 56 | full_name='authpb.User', 57 | filename=None, 58 | file=DESCRIPTOR, 59 | containing_type=None, 60 | fields=[ 61 | _descriptor.FieldDescriptor( 62 | name='name', full_name='authpb.User.name', index=0, 63 | number=1, type=12, cpp_type=9, label=1, 64 | has_default_value=False, default_value=_b(""), 65 | message_type=None, enum_type=None, containing_type=None, 66 | is_extension=False, extension_scope=None, 67 | options=None), 68 | _descriptor.FieldDescriptor( 69 | name='password', full_name='authpb.User.password', index=1, 70 | number=2, type=12, cpp_type=9, label=1, 71 | has_default_value=False, default_value=_b(""), 72 | message_type=None, enum_type=None, containing_type=None, 73 | is_extension=False, extension_scope=None, 74 | options=None), 75 | _descriptor.FieldDescriptor( 76 | name='roles', full_name='authpb.User.roles', index=2, 77 | number=3, type=9, cpp_type=9, label=3, 78 | has_default_value=False, default_value=[], 79 | message_type=None, enum_type=None, containing_type=None, 80 | is_extension=False, extension_scope=None, 81 | options=None), 82 | ], 83 | extensions=[ 84 | ], 85 | nested_types=[], 86 | enum_types=[ 87 | ], 88 | options=None, 89 | is_extendable=False, 90 | syntax='proto3', 91 | extension_ranges=[], 92 | oneofs=[ 93 | ], 94 | serialized_start=22, 95 | serialized_end=75, 96 | ) 97 | 98 | 99 | _PERMISSION = _descriptor.Descriptor( 100 | name='Permission', 101 | full_name='authpb.Permission', 102 | filename=None, 103 | file=DESCRIPTOR, 104 | containing_type=None, 105 | fields=[ 106 | _descriptor.FieldDescriptor( 107 | name='permType', full_name='authpb.Permission.permType', index=0, 108 | number=1, type=14, cpp_type=8, label=1, 109 | has_default_value=False, default_value=0, 110 | message_type=None, enum_type=None, containing_type=None, 111 | is_extension=False, extension_scope=None, 112 | options=None), 113 | _descriptor.FieldDescriptor( 114 | name='key', full_name='authpb.Permission.key', index=1, 115 | number=2, type=12, cpp_type=9, label=1, 116 | has_default_value=False, default_value=_b(""), 117 | message_type=None, enum_type=None, containing_type=None, 118 | is_extension=False, extension_scope=None, 119 | options=None), 120 | _descriptor.FieldDescriptor( 121 | name='range_end', full_name='authpb.Permission.range_end', index=2, 122 | number=3, type=12, cpp_type=9, label=1, 123 | has_default_value=False, default_value=_b(""), 124 | message_type=None, enum_type=None, containing_type=None, 125 | is_extension=False, extension_scope=None, 126 | options=None), 127 | ], 128 | extensions=[ 129 | ], 130 | nested_types=[], 131 | enum_types=[ 132 | _PERMISSION_TYPE, 133 | ], 134 | options=None, 135 | is_extendable=False, 136 | syntax='proto3', 137 | extension_ranges=[], 138 | oneofs=[ 139 | ], 140 | serialized_start=78, 141 | serialized_end=209, 142 | ) 143 | 144 | 145 | _ROLE = _descriptor.Descriptor( 146 | name='Role', 147 | full_name='authpb.Role', 148 | filename=None, 149 | file=DESCRIPTOR, 150 | containing_type=None, 151 | fields=[ 152 | _descriptor.FieldDescriptor( 153 | name='name', full_name='authpb.Role.name', index=0, 154 | number=1, type=12, cpp_type=9, label=1, 155 | has_default_value=False, default_value=_b(""), 156 | message_type=None, enum_type=None, containing_type=None, 157 | is_extension=False, extension_scope=None, 158 | options=None), 159 | _descriptor.FieldDescriptor( 160 | name='keyPermission', full_name='authpb.Role.keyPermission', index=1, 161 | number=2, type=11, cpp_type=10, label=3, 162 | has_default_value=False, default_value=[], 163 | message_type=None, enum_type=None, containing_type=None, 164 | is_extension=False, extension_scope=None, 165 | options=None), 166 | ], 167 | extensions=[ 168 | ], 169 | nested_types=[], 170 | enum_types=[ 171 | ], 172 | options=None, 173 | is_extendable=False, 174 | syntax='proto3', 175 | extension_ranges=[], 176 | oneofs=[ 177 | ], 178 | serialized_start=211, 179 | serialized_end=274, 180 | ) 181 | 182 | _PERMISSION.fields_by_name['permType'].enum_type = _PERMISSION_TYPE 183 | _PERMISSION_TYPE.containing_type = _PERMISSION 184 | _ROLE.fields_by_name['keyPermission'].message_type = _PERMISSION 185 | DESCRIPTOR.message_types_by_name['User'] = _USER 186 | DESCRIPTOR.message_types_by_name['Permission'] = _PERMISSION 187 | DESCRIPTOR.message_types_by_name['Role'] = _ROLE 188 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 189 | 190 | User = _reflection.GeneratedProtocolMessageType('User', (_message.Message,), dict( 191 | DESCRIPTOR = _USER, 192 | __module__ = 'auth_pb2' 193 | # @@protoc_insertion_point(class_scope:authpb.User) 194 | )) 195 | _sym_db.RegisterMessage(User) 196 | 197 | Permission = _reflection.GeneratedProtocolMessageType('Permission', (_message.Message,), dict( 198 | DESCRIPTOR = _PERMISSION, 199 | __module__ = 'auth_pb2' 200 | # @@protoc_insertion_point(class_scope:authpb.Permission) 201 | )) 202 | _sym_db.RegisterMessage(Permission) 203 | 204 | Role = _reflection.GeneratedProtocolMessageType('Role', (_message.Message,), dict( 205 | DESCRIPTOR = _ROLE, 206 | __module__ = 'auth_pb2' 207 | # @@protoc_insertion_point(class_scope:authpb.Role) 208 | )) 209 | _sym_db.RegisterMessage(Role) 210 | 211 | 212 | try: 213 | # THESE ELEMENTS WILL BE DEPRECATED. 214 | # Please use the generated *_pb2_grpc.py files instead. 215 | import grpc 216 | from grpc.beta import implementations as beta_implementations 217 | from grpc.beta import interfaces as beta_interfaces 218 | from grpc.framework.common import cardinality 219 | from grpc.framework.interfaces.face import utilities as face_utilities 220 | except ImportError: 221 | pass 222 | # @@protoc_insertion_point(module_scope) 223 | -------------------------------------------------------------------------------- /aioetcd3/kv.py: -------------------------------------------------------------------------------- 1 | from aioetcd3._etcdv3 import rpc_pb2 as rpc 2 | from aioetcd3.utils import to_bytes, put_key_range 3 | from aioetcd3.base import StubMixin, _default_timeout 4 | from inspect import getcallargs 5 | import functools 6 | import aioetcd3._etcdv3.rpc_pb2_grpc as stub 7 | 8 | 9 | class KVMetadata(object): 10 | def __init__(self, keyvalue): 11 | # self.key = keyvalue.key 12 | self.create_revision = keyvalue.create_revision 13 | self.mod_revision = keyvalue.mod_revision 14 | self.version = keyvalue.version 15 | self.lease_id = keyvalue.lease 16 | 17 | _default = object() 18 | 19 | 20 | _sort_order_dict = {"ascend": rpc.RangeRequest.ASCEND, 21 | None: rpc.RangeRequest.NONE, 22 | "descend": rpc.RangeRequest.DESCEND} 23 | 24 | _sort_target_dict = {"key": rpc.RangeRequest.KEY, 25 | None: rpc.RangeRequest.KEY, 26 | 'version': rpc.RangeRequest.VERSION, 27 | 'create': rpc.RangeRequest.CREATE, 28 | 'mod': rpc.RangeRequest.MOD, 29 | 'value': rpc.RangeRequest.VALUE} 30 | 31 | 32 | def _get_grpc_args(func, *args, **kwargs): 33 | params = getcallargs(func, None, *args, **kwargs) 34 | params.pop('self') 35 | params.pop('timeout') 36 | return params 37 | 38 | 39 | def _kv(request_builder, response_builder, method): 40 | def _decorator(f): 41 | def txn(*args, timeout=_default_timeout, **kwargs): 42 | call_args = _get_grpc_args(f, *args, **kwargs) 43 | return (request_builder(**call_args), response_builder(**call_args)) 44 | f.txn = txn 45 | 46 | @functools.wraps(f) 47 | async def grpc_func(self, *args, timeout=_default_timeout, **kwargs): 48 | request, response = txn(*args, **kwargs) 49 | return response(await self.grpc_call(method(self), request, timeout=timeout)) 50 | return grpc_func 51 | return _decorator 52 | 53 | 54 | def _create_txn_response_builder(success, fail, **kwargs): 55 | def _response_builder(response): 56 | if response.succeeded: 57 | return True, [t[1](_get_op_response(r)) for t, r in zip(success, response.responses)] 58 | else: 59 | return False, [t[1](_get_op_response(r)) for t, r in zip(fail, response.responses)] 60 | return _response_builder 61 | 62 | 63 | 64 | def _range_request(key_range, sort_order=None, sort_target='key', **kwargs): 65 | range_request = rpc.RangeRequest() 66 | put_key_range(range_request, key_range) 67 | 68 | for k, v in kwargs.items(): 69 | if v is not None: 70 | setattr(range_request, k, v) 71 | 72 | if sort_order in _sort_order_dict: 73 | range_request.sort_order = _sort_order_dict[sort_order] 74 | else: 75 | raise ValueError('unknown sort order: "{}"'.format(sort_order)) 76 | 77 | if sort_target in _sort_target_dict: 78 | range_request.sort_target=_sort_target_dict[sort_target] 79 | else: 80 | raise ValueError('sort_target must be one of "key", ' 81 | '"version", "create", "mod" or "value"') 82 | 83 | return range_request 84 | 85 | 86 | def _range_response(kv_response): 87 | result = [] 88 | for kv in kv_response.kvs: 89 | result.append((kv.key, kv.value, KVMetadata(kv))) 90 | return result 91 | 92 | 93 | def _static_builder(f): 94 | def _builder(*args, **kwargs): 95 | return f 96 | return _builder 97 | 98 | 99 | def _partial_builder(f): 100 | def _builder(**kwargs): 101 | return functools.partial(f, **kwargs) 102 | return _builder 103 | 104 | 105 | def _put_request(key, value, lease=None, prev_kv=False, ignore_value=False, ignore_lease=False): 106 | if lease is None: 107 | lease = 0 108 | elif hasattr(lease, 'id'): 109 | lease = lease.id 110 | put_request = rpc.PutRequest(key=to_bytes(key), 111 | prev_kv=prev_kv, ignore_value=ignore_value, 112 | ignore_lease=ignore_lease) 113 | 114 | if not ignore_value: 115 | put_request.value = to_bytes(value) 116 | 117 | if not ignore_lease: 118 | put_request.lease = lease 119 | 120 | return put_request 121 | 122 | 123 | def _delete_request(key_range, prev_kv=False): 124 | 125 | delete_request = rpc.DeleteRangeRequest(prev_kv=prev_kv) 126 | put_key_range(delete_request, key_range) 127 | 128 | return delete_request 129 | 130 | 131 | def _get_response(response): 132 | if response.kvs: 133 | return response.kvs[0].value, KVMetadata(response.kvs[0]) 134 | else: 135 | return None, None 136 | 137 | 138 | def _range_keys_response(response): 139 | result = [] 140 | for kv in response.kvs: 141 | result.append((kv.key, KVMetadata(kv))) 142 | 143 | return result 144 | 145 | 146 | def _delete_response(response, prev_kv=False, **kwargs): 147 | # when set prev_kv to return prev value, 148 | # but it is not existed , response has no prev_kvs 149 | if prev_kv: 150 | r = [] 151 | for kv in response.prev_kvs: 152 | r.append((kv.key, kv.value, KVMetadata(kv))) 153 | return r 154 | else: 155 | return response.deleted 156 | 157 | 158 | def _put_response(response, prev_kv=False, **kwargs): 159 | 160 | # when set prev_kv to return prev value, 161 | # but it is not existed , response has no prev_kv 162 | if prev_kv and response.HasField('prev_kv'): 163 | return response.prev_kv.value, KVMetadata(response.prev_kv) 164 | else: 165 | return None, None 166 | 167 | 168 | def _create_op_request(request): 169 | if isinstance(request, rpc.PutRequest): 170 | return rpc.RequestOp(request_put=request) 171 | elif isinstance(request, rpc.RangeRequest): 172 | return rpc.RequestOp(request_range=request) 173 | elif isinstance(request, rpc.DeleteRangeRequest): 174 | return rpc.RequestOp(request_delete_range=request) 175 | elif isinstance(request, rpc.TxnRequest): 176 | return rpc.RequestOp(request_txn=request) 177 | else: 178 | raise TypeError("Unsupported request OP: " + repr(request)) 179 | 180 | 181 | def _get_op_response(response): 182 | return getattr(response, response.WhichOneof('response')) 183 | 184 | 185 | def _compare_request(compare, success, fail): 186 | compare_message = [c.build_message() for c in compare] 187 | success_message = [_create_op_request(request=r) for r, _ in success] 188 | fail_message = [_create_op_request(request=r) for r, _ in fail] 189 | request = rpc.TxnRequest(compare=compare_message, success=success_message, failure=fail_message) 190 | return request 191 | 192 | 193 | class KV(StubMixin): 194 | def _update_channel(self, channel): 195 | super()._update_channel(channel) 196 | self._kv_stub = stub.KVStub(channel) 197 | 198 | @_kv(_range_request, _static_builder(_range_response), lambda x: x._kv_stub.Range) 199 | async def range(self, key_range, limit=None, revision=None, timeout=_default_timeout, sort_order=None, sort_target='key', 200 | serializable=None, keys_only=None, count_only=None, min_mod_revision=None, max_mod_revision=None, 201 | min_create_revision=None, max_create_revision=None): 202 | # implemented in decorator 203 | pass 204 | 205 | @_kv(functools.partial(_range_request, count_only=True), 206 | _static_builder(lambda r: r.count), lambda x: x._kv_stub.Range) 207 | async def count(self, key_range, revision=None, timeout=_default_timeout, min_mod_revision=None, 208 | max_mod_revision=None, min_create_revision=None, max_create_revision=None): 209 | pass 210 | 211 | @_kv(functools.partial(_range_request, keys_only=True), _static_builder(_range_keys_response), 212 | lambda x: x._kv_stub.Range) 213 | async def range_keys(self, key_range, limit=None, revison=None, sort_order=None, 214 | sort_target='key', timeout=_default_timeout, serializable=None, count_only=None, 215 | min_mod_revision=None, max_mod_revision=None, min_create_revision=None, 216 | max_create_revision=None): 217 | pass 218 | 219 | @_kv(_range_request, _static_builder(_get_response), lambda x: x._kv_stub.Range) 220 | async def get(self, key_range, revision=None, timeout=_default_timeout, serializable=None, 221 | min_mod_revision=None, max_mod_revision=None, min_create_revision=None, 222 | max_create_revision=None): 223 | pass 224 | 225 | @_kv(_put_request, _partial_builder(_put_response), lambda x: x._kv_stub.Put) 226 | async def put(self, key, value, lease=0, prev_kv=False, timeout=_default_timeout, ignore_value=False, ignore_lease=False): 227 | pass 228 | 229 | @_kv(_delete_request, _partial_builder(_delete_response), lambda x: x._kv_stub.DeleteRange) 230 | async def delete(self, key_range, timeout=_default_timeout, prev_kv=False): 231 | pass 232 | 233 | @_kv(functools.partial(_delete_request, prev_kv=True), 234 | _partial_builder(functools.partial(_delete_response, prev_kv=True)), 235 | lambda x: x._kv_stub.DeleteRange) 236 | async def pop(self, key_range, timeout=_default_timeout): 237 | pass 238 | 239 | @_kv(_compare_request, _create_txn_response_builder, lambda x: x._kv_stub.Txn) 240 | async def txn(self, compare, success, fail=[], *, timeout=_default_timeout): 241 | pass 242 | 243 | async def compact(self, revision, physical=False, *, timeout=_default_timeout): 244 | """ 245 | Compact etcd KV storage 246 | 247 | :param revision: compact to specified revision 248 | 249 | :param physical: return until data is physically compacted 250 | 251 | :param timeout: maximum time to wait 252 | """ 253 | await self.grpc_call(self._kv_stub.Compact, 254 | rpc.CompactionRequest(revision=revision, 255 | physical=physical), 256 | timeout=timeout) 257 | -------------------------------------------------------------------------------- /test/test_auth.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import asyncio 3 | import functools 4 | 5 | from aioetcd3.client import client, ssl_client, set_grpc_cipher 6 | from aioetcd3.help import range_all, PER_RW 7 | from aioetcd3.exceptions import AuthError, Unauthenticated, PermissionDenied 8 | 9 | from .utils import switch_auth_off, switch_auth_on 10 | 11 | 12 | def asynctest(f): 13 | @functools.wraps(f) 14 | def _f(self): 15 | asyncio.get_event_loop().run_until_complete(f(self)) 16 | 17 | return _f 18 | 19 | 20 | TEST_USER_NAME = 'test' 21 | TEST_USER_PASSWORD = "test" 22 | TEST_ROLE_NAME = 'admin' 23 | 24 | 25 | class AuthTest(unittest.TestCase): 26 | @asynctest 27 | async def setUp(self): 28 | endpoints = "127.0.0.1:2379" 29 | self.client = client(endpoint=endpoints) 30 | 31 | set_grpc_cipher() 32 | auth_etcd_url = "127.0.0.1:2378" 33 | self.root_client = ssl_client(endpoint=auth_etcd_url, ca_file="test/cfssl/ca.pem", 34 | cert_file="test/cfssl/client-root.pem", 35 | key_file="test/cfssl/client-root-key.pem") 36 | 37 | self.client_client = ssl_client(endpoint=auth_etcd_url, ca_file="test/cfssl/ca.pem", 38 | cert_file="test/cfssl/client.pem", 39 | key_file="test/cfssl/client-key.pem") 40 | 41 | await self.cleanUp() 42 | 43 | @asynctest 44 | async def test_auth_1(self): 45 | 46 | await self.client.user_add(username=TEST_USER_NAME, password='1234') 47 | users = await self.client.user_list() 48 | 49 | self.assertIn(TEST_USER_NAME, users) 50 | 51 | roles = await self.client.user_get(username=TEST_USER_NAME) 52 | self.assertEqual(len(roles), 0) 53 | 54 | await self.client.user_change_password(username=TEST_USER_NAME, password=TEST_USER_PASSWORD) 55 | 56 | await self.client.user_delete(username=TEST_USER_NAME) 57 | 58 | @asynctest 59 | async def test_auth_2(self): 60 | 61 | await self.client.role_add(name=TEST_ROLE_NAME) 62 | 63 | roles = await self.client.role_list() 64 | self.assertIn(TEST_ROLE_NAME, roles) 65 | 66 | role_info = await self.client.role_get(name=TEST_ROLE_NAME) 67 | 68 | await self.client.role_delete(name=TEST_ROLE_NAME) 69 | 70 | @asynctest 71 | async def test_auth_3(self): 72 | 73 | await self.client.user_add(username=TEST_USER_NAME, password=TEST_USER_PASSWORD) 74 | with self.assertRaises(Exception): 75 | await self.client.user_grant_role(username=TEST_USER_NAME, role=TEST_ROLE_NAME) 76 | 77 | await self.client.role_add(name=TEST_ROLE_NAME) 78 | await self.client.user_grant_role(username=TEST_USER_NAME, role=TEST_ROLE_NAME) 79 | 80 | await self.client.role_grant_permission(name=TEST_ROLE_NAME, 81 | key_range=range_all(), 82 | permission=PER_RW) 83 | 84 | await self.client.user_revoke_role(username=TEST_USER_NAME, role=TEST_ROLE_NAME) 85 | 86 | await self.client.role_revoke_permission(name=TEST_ROLE_NAME, 87 | key_range=range_all()) 88 | 89 | 90 | @asynctest 91 | async def test_auth_4(self): 92 | await self.root_client.user_add(username='root', password='root') 93 | await self.root_client.role_add(name='root') 94 | await self.root_client.user_grant_role(username='root', role='root') 95 | await self.root_client.auth_enable() 96 | 97 | await self.root_client.user_add(username='client', password='client') 98 | await self.root_client.role_add(name='client') 99 | 100 | await self.root_client.put('/foo', '/foo') 101 | value, meta = await self.root_client.get('/foo') 102 | self.assertEqual(value, b'/foo') 103 | 104 | with self.assertRaises(Exception): 105 | await self.client_client.get('/foo') 106 | 107 | await self.root_client.role_grant_permission(name='client', key_range='/foo', permission=PER_RW) 108 | await self.root_client.user_grant_role(username='client', role='client') 109 | 110 | value, meta = await self.client_client.get('/foo') 111 | self.assertEqual(value, b'/foo') 112 | 113 | await self.client_client.put('/foo', 'ssss') 114 | 115 | async def delete_all_user(self): 116 | users = await self.client.user_list() 117 | 118 | for u in users: 119 | await self.client.user_delete(username=u) 120 | 121 | users = await self.root_client.user_list() 122 | 123 | for u in users: 124 | await self.root_client.user_delete(username=u) 125 | 126 | async def delete_all_role(self): 127 | roles = await self.client.role_list() 128 | 129 | for r in roles: 130 | await self.client.role_delete(name=r) 131 | 132 | roles = await self.root_client.role_list() 133 | 134 | for r in roles: 135 | await self.root_client.role_delete(name=r) 136 | 137 | async def cleanUp(self): 138 | 139 | await self.client.delete(range_all()) 140 | 141 | await self.root_client.auth_disable() 142 | 143 | await self.delete_all_user() 144 | await self.delete_all_role() 145 | 146 | @asynctest 147 | async def tearDown(self): 148 | await self.cleanUp() 149 | await self.client.close() 150 | 151 | 152 | class PasswordAuthTest(unittest.TestCase): 153 | @asynctest 154 | async def setUp(self): 155 | self.endpoints = "127.0.0.1:2379" 156 | self.unauthenticated_client = client(endpoint=self.endpoints) 157 | await self.cleanUp() 158 | await switch_auth_on(self.unauthenticated_client) 159 | self.client_client = client( 160 | endpoint=self.endpoints, username="client", password="client" 161 | ) 162 | self.root_client = client(endpoint=self.endpoints, username="root", password="root") 163 | 164 | async def create_kv_for_test(self): 165 | await self.root_client.put('/foo', '/foo') 166 | value, meta = await self.root_client.get('/foo') 167 | self.assertEqual(value, b'/foo') 168 | 169 | @asynctest 170 | async def test_auth_1(self): 171 | await self.create_kv_for_test() 172 | 173 | with self.assertRaises(PermissionDenied): 174 | await self.client_client.get('/foo') 175 | 176 | await self.root_client.role_grant_permission(name='client', key_range='/foo', permission=PER_RW) 177 | value, meta = await self.client_client.get('/foo') 178 | self.assertEqual(value, b'/foo') 179 | 180 | await self.client_client.put('/foo', 'ssss') 181 | 182 | @asynctest 183 | async def test_wrong_password(self): 184 | wrong_password_client = client(endpoint=self.endpoints, username="client", password="wrong_password") 185 | with self.assertRaises(AuthError) as exc: 186 | await wrong_password_client.get("/foo") 187 | assert repr(exc.exception) == "`{}`: reason: `{}`".format(exc.exception.code, exc.exception.details) 188 | 189 | @asynctest 190 | async def test_wrong_token(self): 191 | await self.create_kv_for_test() 192 | await self.root_client.role_grant_permission(name='client', key_range='/foo', permission=PER_RW) 193 | 194 | new_client = client(endpoint=self.endpoints, username="client", password="client") 195 | value, meta = await self.client_client.get('/foo') 196 | self.assertEqual(value, b'/foo') 197 | 198 | # Put invalid token 199 | new_client._metadata = (("token", "invalid_token"),) 200 | with self.assertRaises(Unauthenticated) as exc: 201 | await new_client.get("/foo") 202 | 203 | async def cleanUp(self): 204 | await self.unauthenticated_client.delete(range_all()) 205 | 206 | @asynctest 207 | async def tearDown(self): 208 | await switch_auth_off(self.root_client, self.unauthenticated_client) 209 | await self.cleanUp() 210 | 211 | 212 | class PasswordAuthWithSslTest(unittest.TestCase): 213 | @asynctest 214 | async def setUp(self): 215 | self.endpoints = "127.0.0.1:2377" 216 | self.unauthenticated_client = ssl_client( 217 | endpoint=self.endpoints, 218 | ca_file="test/cfssl/ca.pem", 219 | ) 220 | await self.cleanUp() 221 | await switch_auth_on(self.unauthenticated_client) 222 | self.root_client = ssl_client(endpoint=self.endpoints, ca_file="test/cfssl/ca.pem", 223 | username="root", password="root") 224 | 225 | self.client_client = ssl_client(endpoint=self.endpoints, ca_file="test/cfssl/ca.pem", 226 | username="client", password="client") 227 | 228 | async def create_kv_for_test(self): 229 | await self.root_client.put('/foo', '/foo') 230 | value, meta = await self.root_client.get('/foo') 231 | self.assertEqual(value, b'/foo') 232 | 233 | @asynctest 234 | async def test_auth_1(self): 235 | await self.create_kv_for_test() 236 | 237 | with self.assertRaises(PermissionDenied): 238 | await self.client_client.get('/foo') 239 | 240 | await self.root_client.role_grant_permission(name='client', key_range='/foo', permission=PER_RW) 241 | value, meta = await self.client_client.get('/foo') 242 | self.assertEqual(value, b'/foo') 243 | 244 | await self.client_client.put('/foo', 'ssss') 245 | 246 | @asynctest 247 | async def test_wrong_password(self): 248 | wrong_password_client = ssl_client( 249 | endpoint=self.endpoints, ca_file="test/cfssl/ca.pem", 250 | username="client", password="wrong_password" 251 | ) 252 | with self.assertRaises(AuthError) as exc: 253 | await wrong_password_client.get("/foo") 254 | assert repr(exc.exception) == "`{}`: reason: `{}`".format(exc.exception.code, exc.exception.details) 255 | 256 | @asynctest 257 | async def test_wrong_token(self): 258 | await self.create_kv_for_test() 259 | await self.root_client.role_grant_permission(name='client', key_range='/foo', permission=PER_RW) 260 | 261 | new_client = ssl_client( 262 | endpoint=self.endpoints, ca_file="test/cfssl/ca.pem", 263 | username="root", password="root" 264 | ) 265 | value, meta = await new_client.get('/foo') 266 | self.assertEqual(value, b'/foo') 267 | 268 | # Put invalid token 269 | new_client._metadata = (("token", "invalid_token"),) 270 | with self.assertRaises(Unauthenticated) as exc: 271 | await new_client.get("/foo") 272 | 273 | async def cleanUp(self): 274 | await self.unauthenticated_client.delete(range_all()) 275 | 276 | @asynctest 277 | async def tearDown(self): 278 | await switch_auth_off(self.root_client, self.unauthenticated_client) 279 | await self.cleanUp() 280 | -------------------------------------------------------------------------------- /test/test_watch.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import functools 3 | import asyncio 4 | from grpc import RpcError 5 | 6 | from aioetcd3.client import client 7 | from aioetcd3.help import range_all, range_prefix, PER_RW 8 | from aioetcd3.watch import EVENT_TYPE_CREATE,EVENT_TYPE_DELETE,EVENT_TYPE_MODIFY,\ 9 | CompactRevisonException, WatchException 10 | 11 | from .utils import switch_auth_off, switch_auth_on 12 | 13 | 14 | def asynctest(f): 15 | @functools.wraps(f) 16 | def _f(self): 17 | return asyncio.get_event_loop().run_until_complete(f(self)) 18 | 19 | return _f 20 | 21 | 22 | class WatchTest(unittest.TestCase): 23 | @asynctest 24 | async def setUp(self): 25 | self.endpoints = "127.0.0.1:2379" 26 | self.client = client(endpoint=self.endpoints) 27 | await self.cleanUp() 28 | 29 | async def common_watch1(self): 30 | f1 = asyncio.get_event_loop().create_future() 31 | 32 | async def watch_1(): 33 | i = 0 34 | async with self.client.watch_scope('/foo') as response: 35 | f1.set_result(None) 36 | async for event in response: 37 | i = i + 1 38 | if i == 1: 39 | self.assertEqual(event.type, EVENT_TYPE_CREATE) 40 | self.assertEqual(event.key, b'/foo') 41 | self.assertEqual(event.value, b'foo') 42 | elif i == 2: 43 | self.assertEqual(event.type, EVENT_TYPE_MODIFY) 44 | self.assertEqual(event.key, b'/foo') 45 | self.assertEqual(event.value, b'foo1') 46 | elif i == 3: 47 | self.assertEqual(event.type, EVENT_TYPE_DELETE) 48 | self.assertEqual(event.key, b'/foo') 49 | # delete event has no value 50 | # self.assertEqual(event.value, b'foo1') 51 | break 52 | 53 | f2 = asyncio.get_event_loop().create_future() 54 | 55 | async def watch_2(): 56 | i = 0 57 | async for event in self.client.watch('/foo', prev_kv=True, create_event=True): 58 | if event is None: 59 | f2.set_result(None) 60 | continue 61 | 62 | i = i + 1 63 | if i == 1: 64 | self.assertEqual(event.type, EVENT_TYPE_CREATE) 65 | self.assertEqual(event.key, b'/foo') 66 | self.assertEqual(event.value, b'foo') 67 | elif i == 2: 68 | self.assertEqual(event.type, EVENT_TYPE_MODIFY) 69 | self.assertEqual(event.key, b'/foo') 70 | self.assertEqual(event.value, b'foo1') 71 | self.assertEqual(event.pre_value, b'foo') 72 | elif i == 3: 73 | self.assertEqual(event.type, EVENT_TYPE_DELETE) 74 | self.assertEqual(event.key, b'/foo') 75 | # self.assertEqual(event.value, b'foo1') 76 | break 77 | 78 | f3 = asyncio.get_event_loop().create_future() 79 | 80 | async def watch_3(): 81 | i = 0 82 | async for event in self.client.watch('/foo', prev_kv=True, noput=True, create_event=True): 83 | if event is None: 84 | f3.set_result(None) 85 | continue 86 | 87 | i = i + 1 88 | if i == 1: 89 | self.assertEqual(event.type, EVENT_TYPE_DELETE) 90 | self.assertEqual(event.key, b'/foo') 91 | # self.assertEqual(event.value, b'foo1') 92 | break 93 | 94 | f4 = asyncio.get_event_loop().create_future() 95 | 96 | async def watch_4(): 97 | i = 0 98 | async for event in self.client.watch('/foo', prev_kv=True, nodelete=True, create_event=True): 99 | if event is None: 100 | f4.set_result(None) 101 | continue 102 | 103 | i = i + 1 104 | if i == 1: 105 | self.assertEqual(event.type, EVENT_TYPE_CREATE) 106 | self.assertEqual(event.key, b'/foo') 107 | self.assertEqual(event.value, b'foo') 108 | elif i == 2: 109 | self.assertEqual(event.type, EVENT_TYPE_MODIFY) 110 | self.assertEqual(event.key, b'/foo') 111 | self.assertEqual(event.value, b'foo1') 112 | self.assertEqual(event.pre_value, b'foo') 113 | break 114 | 115 | w1 = asyncio.ensure_future(watch_1()) 116 | w2 = asyncio.ensure_future(watch_2()) 117 | w3 = asyncio.ensure_future(watch_3()) 118 | w4 = asyncio.ensure_future(watch_4()) 119 | 120 | await asyncio.wait_for(asyncio.wait([f1, f2, f3, f4]), 2) 121 | 122 | await self.client.put('/foo', 'foo') 123 | await self.client.put('/foo', 'foo1') 124 | await self.client.delete('/foo') 125 | 126 | done, pending = await asyncio.wait([w1, w2, w3, w4], timeout=20) 127 | for t in done: 128 | t.result() 129 | 130 | @asynctest 131 | async def test_watch_1(self): 132 | await self.common_watch1() 133 | 134 | async def watch_reconnect(self): 135 | f1 = asyncio.get_event_loop().create_future() 136 | f2 = asyncio.get_event_loop().create_future() 137 | 138 | async def watch_1(): 139 | i = 0 140 | async with self.client.watch_scope('/foo') as response: 141 | f1.set_result(None) 142 | async for event in response: 143 | i = i + 1 144 | if i == 1: 145 | self.assertEqual(event.type, EVENT_TYPE_CREATE) 146 | self.assertEqual(event.key, b'/foo') 147 | self.assertEqual(event.value, b'foo') 148 | f2.set_result(None) 149 | elif i == 2: 150 | self.assertEqual(event.type, EVENT_TYPE_MODIFY) 151 | self.assertEqual(event.key, b'/foo') 152 | self.assertEqual(event.value, b'foo1') 153 | elif i == 3: 154 | self.assertEqual(event.type, EVENT_TYPE_DELETE) 155 | self.assertEqual(event.key, b'/foo') 156 | # delete event has no value 157 | # self.assertEqual(event.value, b'foo1') 158 | break 159 | 160 | t1 = asyncio.ensure_future(watch_1()) 161 | await f1 162 | await self.client.put('/foo', 'foo') 163 | await f2 164 | self.client.update_server_list(self.endpoints) 165 | await self.client.put('/foo', 'foo1') 166 | await self.client.delete('/foo') 167 | await t1 168 | 169 | @asynctest 170 | async def test_watch_reconnect(self): 171 | await self.watch_reconnect() 172 | 173 | async def watch_create_cancel(self): 174 | async def watch_1(): 175 | async with self.client.watch_scope('/foo') as _: 176 | pass 177 | async def watch_2(): 178 | async with self.client.watch_scope('/foo') as _: 179 | await asyncio.sleep(5) 180 | for _ in range(0, 5): 181 | watches = [asyncio.ensure_future(watch_1() if i % 2 else watch_2()) for i in range(0, 200)] 182 | await asyncio.sleep(1) 183 | for w in watches[::3]: 184 | w.cancel() 185 | self.client.update_server_list(self.endpoints) 186 | await asyncio.sleep(0.01) 187 | for w in watches[1::3]: 188 | w.cancel() 189 | await asyncio.sleep(0.3) 190 | for w in watches[2::3]: 191 | w.cancel() 192 | await asyncio.wait_for(asyncio.wait(watches), 3) 193 | results = await asyncio.gather(*watches, return_exceptions=True) 194 | print("Finished:", len([r for r in results if r is None]), "Cancelled:", len([r for r in results if r is not None])) 195 | self.assertIsNotNone(self.client._watch_task_running) 196 | await asyncio.sleep(3) 197 | self.assertIsNone(self.client._watch_task_running) 198 | 199 | @asynctest 200 | async def test_watch_create_cancel(self): 201 | await self.watch_create_cancel() 202 | 203 | async def batch_events(self): 204 | f1 = asyncio.get_event_loop().create_future() 205 | f2 = asyncio.get_event_loop().create_future() 206 | 207 | def _check_event(e, criterias): 208 | if criterias[0]: 209 | self.assertEqual(e.type, criterias[0]) 210 | if criterias[1]: 211 | self.assertEqual(e.key, criterias[1]) 212 | if criterias[2]: 213 | self.assertEqual(e.value, criterias[2]) 214 | 215 | async def watch_1(): 216 | asserts = [(EVENT_TYPE_CREATE, b'/foo/1', b'1'), 217 | (EVENT_TYPE_CREATE, b'/foo/2', b'2'), 218 | (EVENT_TYPE_MODIFY, b'/foo/1', b'2'), 219 | (EVENT_TYPE_MODIFY, b'/foo/2', b'3'), 220 | (EVENT_TYPE_DELETE, b'/foo/1', None), 221 | (EVENT_TYPE_DELETE, b'/foo/2', None)] 222 | async with self.client.watch_scope(range_prefix('/foo/')) as response: 223 | f1.set_result(None) 224 | async for e in response: 225 | _check_event(e, asserts.pop(0)) 226 | if not asserts: 227 | break 228 | 229 | async def watch_2(): 230 | asserts = [((EVENT_TYPE_CREATE, b'/foo/1', b'1'), 231 | (EVENT_TYPE_CREATE, b'/foo/2', b'2'),), 232 | ((EVENT_TYPE_MODIFY, b'/foo/1', b'2'),), 233 | ((EVENT_TYPE_MODIFY, b'/foo/2', b'3'),), 234 | ((EVENT_TYPE_DELETE, b'/foo/1', None), 235 | (EVENT_TYPE_DELETE, b'/foo/2', None))] 236 | async with self.client.watch_scope(range_prefix('/foo/'), batch_events=True) \ 237 | as response: 238 | f2.set_result(None) 239 | async for es in response: 240 | batch = asserts.pop(0) 241 | self.assertEqual(len(es), len(batch)) 242 | for e, a in zip(es, batch): 243 | _check_event(e, a) 244 | if not asserts: 245 | break 246 | 247 | t1 = asyncio.ensure_future(watch_1()) 248 | t2 = asyncio.ensure_future(watch_2()) 249 | await asyncio.wait_for(asyncio.wait([f1, f2]), 2) 250 | self.assertTrue((await self.client.txn([], [self.client.put.txn('/foo/1', '1'), 251 | self.client.put.txn('/foo/2', '2')], []))[0]) 252 | await self.client.put('/foo/1', '2') 253 | await self.client.put('/foo/2', '3') 254 | self.assertTrue((await self.client.txn([], [self.client.delete.txn('/foo/1'), 255 | self.client.delete.txn('/foo/2')], []))[0]) 256 | await asyncio.gather(t1, t2) 257 | 258 | @asynctest 259 | async def test_batch_events(self): 260 | await self.batch_events() 261 | 262 | async def compact_revision(self): 263 | await self.client.put('/foo', '1') 264 | first_revision = self.client.last_response_info.revision 265 | await self.client.put('/foo', '2') 266 | await self.client.put('/foo', '3') 267 | await self.client.put('/foo', '4') 268 | await self.client.put('/foo', '5') 269 | compact_revision = self.client.last_response_info.revision 270 | await self.client.compact(compact_revision, True) 271 | 272 | async def watch_1(): 273 | async with self.client.watch_scope('/foo', start_revision=first_revision) as response: 274 | with self.assertRaises(CompactRevisonException) as cm: 275 | async for e in response: 276 | raise ValueError("Not raised") 277 | self.assertEqual(cm.exception.revision, compact_revision) 278 | 279 | async def watch_2(): 280 | async with self.client.watch_scope('/foo', ignore_compact=True, start_revision=first_revision) as responses: 281 | async for e in responses: 282 | self.assertEqual(e.type, EVENT_TYPE_MODIFY) 283 | self.assertEqual(e.key, b'/foo') 284 | self.assertEqual(e.value, b'5') 285 | self.assertEqual(e.revision, compact_revision) 286 | break 287 | 288 | await watch_1() 289 | await watch_2() 290 | 291 | @asynctest 292 | async def test_compact_revision(self): 293 | await self.compact_revision() 294 | 295 | async def watch_exception(self): 296 | f1 = asyncio.get_event_loop().create_future() 297 | f2 = asyncio.get_event_loop().create_future() 298 | 299 | async def watch_1(): 300 | i = 0 301 | async with self.client.watch_scope('/foo') as response: 302 | f1.set_result(None) 303 | with self.assertRaises(WatchException): 304 | async for event in response: 305 | i = i + 1 306 | if i == 1: 307 | self.assertEqual(event.type, EVENT_TYPE_CREATE) 308 | self.assertEqual(event.key, b'/foo') 309 | self.assertEqual(event.value, b'foo') 310 | f2.set_result(None) 311 | elif i == 2: 312 | raise ValueError("Not raised") 313 | 314 | f3 = asyncio.get_event_loop().create_future() 315 | f4 = asyncio.get_event_loop().create_future() 316 | 317 | async def watch_2(): 318 | i = 0 319 | async with self.client.watch_scope('/foo', always_reconnect=True) as response: 320 | f3.set_result(None) 321 | async for event in response: 322 | i = i + 1 323 | if i == 1: 324 | self.assertEqual(event.type, EVENT_TYPE_CREATE) 325 | self.assertEqual(event.key, b'/foo') 326 | self.assertEqual(event.value, b'foo') 327 | f4.set_result(None) 328 | elif i == 2: 329 | self.assertEqual(event.type, EVENT_TYPE_MODIFY) 330 | self.assertEqual(event.key, b'/foo') 331 | self.assertEqual(event.value, b'foo1') 332 | elif i == 3: 333 | self.assertEqual(event.type, EVENT_TYPE_DELETE) 334 | self.assertEqual(event.key, b'/foo') 335 | # delete event has no value 336 | # self.assertEqual(event.value, b'foo1') 337 | break 338 | 339 | t1 = asyncio.ensure_future(watch_1()) 340 | t2 = asyncio.ensure_future(watch_2()) 341 | await f1 342 | await f3 343 | await self.client.put('/foo', 'foo') 344 | await f2 345 | await f4 346 | fake_endpoints = 'ipv4:///127.0.0.1:49999' 347 | self.client.update_server_list(fake_endpoints) 348 | await asyncio.sleep(2) 349 | self.client.update_server_list(self.endpoints) 350 | await self.client.put('/foo', 'foo1') 351 | await self.client.delete('/foo') 352 | await t1 353 | await t2 354 | 355 | @asynctest 356 | async def test_watch_exception(self): 357 | await self.watch_exception() 358 | 359 | async def _run_test_with_auth(self, test): 360 | default_client = self.client 361 | await switch_auth_on(default_client) 362 | root_client = client(endpoint=self.endpoints, username="root", password="root") 363 | await root_client.role_grant_permission(name='client', key_range=range_prefix('/foo'), permission=PER_RW) 364 | self.client = client(endpoint=self.endpoints, username="client", password="client") 365 | try: 366 | await test() 367 | finally: 368 | await switch_auth_off( 369 | root_client, 370 | default_client 371 | ) 372 | await root_client.close() 373 | await self.client.close() 374 | self.client = default_client 375 | 376 | @asynctest 377 | async def test_watch1_with_auth(self): 378 | await self._run_test_with_auth(self.common_watch1) 379 | 380 | @asynctest 381 | async def test_watch_reconnect_with_auth(self): 382 | await self._run_test_with_auth(self.watch_reconnect) 383 | 384 | @asynctest 385 | async def test_watch_create_cancel_with_auth(self): 386 | await self._run_test_with_auth(self.watch_create_cancel) 387 | 388 | @asynctest 389 | async def test_batch_events_with_auth(self): 390 | await self._run_test_with_auth(self.batch_events) 391 | 392 | @asynctest 393 | async def test_compact_revision_with_auth(self): 394 | await self._run_test_with_auth(self.compact_revision) 395 | 396 | @asynctest 397 | async def test_watch_exception_with_auth(self): 398 | await self._run_test_with_auth(self.watch_exception) 399 | 400 | @asynctest 401 | async def tearDown(self): 402 | await self.cleanUp() 403 | await self.client.close() 404 | 405 | async def cleanUp(self): 406 | await self.client.delete(range_all()) 407 | 408 | 409 | if __name__ == '__main__': 410 | unittest.main() 411 | -------------------------------------------------------------------------------- /aioetcd3/watch.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from asyncio import CancelledError 4 | from aioetcd3.base import StubMixin 5 | from aioetcd3._etcdv3 import rpc_pb2 as rpc 6 | from aioetcd3._etcdv3 import kv_pb2 as kv 7 | from asyncio.queues import Queue, QueueEmpty, QueueFull 8 | from aioetcd3.utils import put_key_range 9 | from aioetcd3.kv import KVMetadata 10 | import aioetcd3._etcdv3.rpc_pb2_grpc as stub 11 | 12 | EVENT_TYPE_MODIFY = "MODIFY" 13 | EVENT_TYPE_DELETE = "DELETE" 14 | EVENT_TYPE_CREATE = "CREATE" 15 | 16 | 17 | class Event(object): 18 | def __init__(self, event, revision): 19 | if event.type == kv.Event.PUT: 20 | if event.kv.version == 1: 21 | self.type = EVENT_TYPE_CREATE 22 | else: 23 | self.type = EVENT_TYPE_MODIFY 24 | else: 25 | self.type = EVENT_TYPE_DELETE 26 | 27 | self.key = event.kv.key 28 | self.value = event.kv.value 29 | self.meta = KVMetadata(event.kv) 30 | 31 | self.pre_value = event.prev_kv.value 32 | self.pre_meta = KVMetadata(event.prev_kv) 33 | 34 | self.revision = revision 35 | 36 | def is_put(self): 37 | return self.type == EVENT_TYPE_CREATE or self.type == EVENT_TYPE_MODIFY 38 | 39 | def __str__(self): 40 | return f'{self.type} {self.key},{self.value}' 41 | 42 | 43 | class WatchScope(object): 44 | def __init__(self, _iter): 45 | self._iter = _iter 46 | 47 | async def __aenter__(self): 48 | await self._iter.__anext__() 49 | return self._iter 50 | 51 | async def __aexit__(self, exc_type, exc_val, exc_tb): 52 | try: 53 | await self._iter.aclose() 54 | except GeneratorExit: 55 | pass 56 | 57 | 58 | class _Pipe(object): 59 | """ 60 | Selectable asyncio channel 61 | """ 62 | def __init__(self, maxsize=None, *, loop=None): 63 | self._loop = loop 64 | self._notify = asyncio.Event(loop=loop) 65 | self._full_notify = asyncio.Event(loop=loop) 66 | self._queue = [] 67 | self._full_notify.set() 68 | if maxsize is None or maxsize <= 0: 69 | self._maxsize = None 70 | else: 71 | self._maxsize = maxsize 72 | self._last_watch_version = None 73 | 74 | def is_empty(self): 75 | return not self._notify.is_set() 76 | 77 | def is_full(self): 78 | return not self._full_notify.is_set() 79 | 80 | async def put(self, value): 81 | await self.wait_full() 82 | self.put_nowait(value) 83 | 84 | def put_nowait(self, value): 85 | if self.is_full(): 86 | raise QueueFull 87 | self._queue.append(value) 88 | if self.is_empty(): 89 | self._notify.set() 90 | if self._maxsize and len(self._queue) >= self._maxsize: 91 | self._full_notify.clear() 92 | 93 | async def get(self, value): 94 | await self.wait_empty() 95 | return self.get_nowait() 96 | 97 | def get_nowait(self): 98 | if self.is_empty(): 99 | raise QueueEmpty 100 | if self._maxsize or len(self._queue) <= self._maxsize: 101 | self._full_notify.set() 102 | if len(self._queue) == 1: 103 | self._notify.clear() 104 | return self._queue.pop(0) 105 | 106 | async def read(self, limit=None): 107 | await self.wait_empty() 108 | return self.read_nowait(limit) 109 | 110 | def read_nowait(self, limit=None): 111 | if self.is_empty(): 112 | raise QueueEmpty 113 | if limit is None or limit <= 0: 114 | read_size = len(self._queue) 115 | else: 116 | read_size = min(len(self._queue), limit) 117 | result = self._queue[:read_size] 118 | del self._queue[:read_size] 119 | if not self._maxsize or len(self._queue) < self._maxsize: 120 | self._full_notify.set() 121 | if len(self._queue) == 0: 122 | self._notify.clear() 123 | return result 124 | 125 | async def write(self, values): 126 | await self.wait_full() 127 | return self.write_nowait(values) 128 | 129 | def write_nowait(self, values): 130 | if self.is_full(): 131 | raise QueueFull 132 | if self._maxsize is None: 133 | write_size = len(values) 134 | else: 135 | write_size = min(len(values), self._maxsize - len(self._queue)) 136 | self._queue.extend(values[:write_size]) 137 | if len(self._queue) > 0: 138 | self._notify.set() 139 | if self._maxsize and len(self._queue) >= self._maxsize: 140 | self._full_notify.clear() 141 | return write_size 142 | 143 | async def wait_full(self): 144 | while self.is_full(): 145 | await self._full_notify.wait() 146 | 147 | async def wait_empty(self): 148 | while self.is_empty(): 149 | await self._notify.wait() 150 | 151 | 152 | async def _select(pipes, futures, *, loop=None): 153 | futures = [asyncio.ensure_future(f, loop=loop) for f in futures] 154 | _, pending = await asyncio.wait([p.wait_empty() for p in pipes] + list(futures), 155 | loop=loop, return_when=asyncio.FIRST_COMPLETED) 156 | for p in pending: 157 | if p not in futures: 158 | p.cancel() 159 | try: 160 | await p 161 | except asyncio.CancelledError: 162 | pass 163 | except Exception: 164 | pass 165 | return [p for p in pipes if not p.is_empty()], [f for f in futures if f.done()] 166 | 167 | 168 | class WatchException(Exception): 169 | def _clone(self): 170 | return type(self)(*self.args) 171 | 172 | class CompactRevisonException(WatchException): 173 | def __init__(self, revision): 174 | super().__init__(f"Watch on compact revision. Min revision is {revision}") 175 | self.revision = revision 176 | 177 | def _clone(self): 178 | return CompactRevisonException(self.revision) 179 | 180 | 181 | class ServerCancelException(WatchException): 182 | def __init__(self, cancel_reason): 183 | super().__init__(f"Watch cancelled: {cancel_reason}") 184 | self.cancel_reason = cancel_reason 185 | 186 | def _clone(self): 187 | return ServerCancelException(self.cancel_reason) 188 | 189 | 190 | class Watch(StubMixin): 191 | def __init__(self, channel, timeout, username=None, password=None): 192 | # Put (WatchCreateRequest, output_queue, done_future) to create a watch 193 | self._create_request_queue = _Pipe(5, loop=self._loop) 194 | # Put (output_queue, done_future) to cancel a watch 195 | self._cancel_request_queue = _Pipe(loop=self._loop) 196 | self._reconnect_event = asyncio.Event(loop=self._loop) 197 | self._watch_task_running = None 198 | super().__init__(channel, timeout, username=username, password=password) 199 | 200 | async def _watch_task(self, reconnect_event): 201 | # Queue for WatchRequest 202 | async def input_iterator(input_queue): 203 | while True: 204 | n = await input_queue.get() 205 | if n is None: 206 | break 207 | yield n 208 | 209 | async def watch_call(input_queue, watch_stub, output_pipe): 210 | await self._authenticate_if_needed() 211 | async with watch_stub.Watch.with_scope( 212 | input_iterator(input_queue), credentials=self._call_credentials, metadata=self._metadata 213 | ) as response_iter: 214 | async for r in response_iter: 215 | await output_pipe.put(r) 216 | last_received_revision = None 217 | # watch_id -> revision 218 | last_watches_revision = {} 219 | # watch_id -> (WatchCreateRequest, output_queue) 220 | registered_watches = {} 221 | # output_queue -> watch_id 222 | registered_queues = {} 223 | # A tuple (WatchCreateRequest, output_queue, done_future, cancel_future) 224 | pending_create_request = None 225 | # watch_id -> done_future 226 | pending_cancel_requests = {} 227 | # output_queue -> (WatchCreateRequest, done_future) 228 | restore_creates = {} 229 | quitting = False 230 | def _reconnect_revision(watch_id): 231 | if last_received_revision is None: 232 | return None 233 | else: 234 | if watch_id in last_watches_revision: 235 | last_revision = last_watches_revision[watch_id] 236 | return max(last_revision + 1, last_received_revision) 237 | else: 238 | return None 239 | try: 240 | while not quitting: # Auto reconnect when failed or channel updated 241 | reconnect_event.clear() 242 | output_pipe = _Pipe(loop=self._loop) 243 | input_queue = asyncio.Queue(loop=self._loop) 244 | call_task = asyncio.ensure_future(watch_call(input_queue, self._watch_stub, output_pipe), loop=self._loop) 245 | try: 246 | # Restore registered watches 247 | for watch_id, (create_request, output_queue) in registered_watches.items(): 248 | if watch_id in pending_cancel_requests: 249 | # Already cancelling 250 | fut = pending_cancel_requests.pop(watch_id) 251 | if not fut.done(): 252 | fut.set_result(True) 253 | continue 254 | r = rpc.WatchCreateRequest() 255 | r.CopyFrom(create_request) 256 | restore_revision = _reconnect_revision(watch_id) 257 | if restore_revision is not None: 258 | r.start_revision = restore_revision 259 | restore_creates[output_queue] = (r, None) 260 | registered_watches.clear() 261 | registered_queues.clear() 262 | # Restore pending cancels - should already be processed though 263 | for watch_id, fut in pending_cancel_requests.items(): 264 | fut.set_result(True) 265 | pending_cancel_requests.clear() 266 | # Restore pending create request 267 | if pending_create_request is not None: 268 | if pending_create_request[3] is not None: # Cancelled 269 | pending_create_request[1].put_nowait((False, None, None)) 270 | if pending_create_request[2] is not None and not pending_create_request[2].done(): 271 | pending_create_request[2].set_result(True) 272 | if pending_create_request[3] is not None and not pending_create_request[3].done(): 273 | pending_create_request[3].set_result(True) 274 | else: 275 | restore_creates[pending_create_request[1]] = (pending_create_request[0], pending_create_request[2]) 276 | pending_create_request = None 277 | while True: 278 | if pending_create_request is None: 279 | if restore_creates: 280 | q, (req, fut) = restore_creates.popitem() 281 | # Send create request 282 | pending_create_request = (req, q, fut, None) 283 | input_queue.put_nowait(rpc.WatchRequest(create_request=req)) 284 | if pending_create_request is None: 285 | select_pipes = [output_pipe, self._create_request_queue, self._cancel_request_queue] 286 | else: 287 | select_pipes = [output_pipe, self._cancel_request_queue] 288 | reconn_wait = asyncio.ensure_future(reconnect_event.wait(), loop=self._loop) 289 | select_futs = [reconn_wait, call_task] 290 | if not pending_create_request and not registered_watches and \ 291 | not restore_creates: 292 | select_futs.append(asyncio.sleep(2, loop=self._loop)) 293 | pipes, _ = await _select(select_pipes, 294 | select_futs, 295 | loop=self._loop) 296 | reconn_wait.cancel() 297 | if not pipes and not reconnect_event.is_set() and not call_task.done(): 298 | # No watch, stop the task 299 | quitting = True 300 | break 301 | # Process cancel requests first 302 | if self._cancel_request_queue in pipes: 303 | cancel_requests = self._cancel_request_queue.read_nowait() 304 | for output_queue, done_fut in cancel_requests: 305 | if output_queue in pending_cancel_requests: 306 | # Chain this future 307 | pending_cancel_requests[output_queue].add_done_callback( 308 | lambda f, done_fut=done_fut: done_fut.set_result(True) 309 | ) 310 | elif output_queue in restore_creates: 311 | # Cancel a request which is not started 312 | _, fut = restore_creates.pop(output_queue) 313 | output_queue.put_nowait((False, None, None)) 314 | if fut is not None and not fut.done(): 315 | fut.set_result(True) 316 | if done_fut is not None and not done_fut.done(): 317 | done_fut.set_result(True) 318 | elif pending_create_request is not None and \ 319 | pending_create_request[1] == output_queue: 320 | # Cancel the pending create watch 321 | if pending_create_request[3] is None: 322 | pending_create_request = pending_create_request[:3] + (done_fut,) 323 | else: 324 | pending_create_request[3].add_done_callback( 325 | lambda f, done_fut=done_fut: done_fut.set_result(True)) 326 | else: 327 | watch_id = registered_queues.get(output_queue) 328 | if watch_id is None: 329 | done_fut.set_result(True) 330 | else: 331 | # Send cancel request and save it to pending requests 332 | input_queue.put_nowait( 333 | rpc.WatchRequest( 334 | cancel_request= 335 | rpc.WatchCancelRequest(watch_id=watch_id) 336 | ) 337 | ) 338 | pending_cancel_requests[watch_id] = done_fut 339 | # Process received events 340 | if output_pipe in pipes: 341 | outputs = output_pipe.read_nowait() 342 | for response in outputs: 343 | if response.created: 344 | assert pending_create_request is not None 345 | if response.compact_revision > 0: 346 | # Cancelled (Is it possible?) 347 | exc = CompactRevisonException(response.compact_revision) 348 | pending_create_request[1].put_nowait((False, exc, response.compact_revision)) 349 | if pending_create_request[2] is not None and not \ 350 | pending_create_request[2].done(): 351 | pending_create_request[2].set_exception(exc) 352 | if pending_create_request[3] is not None and not \ 353 | pending_create_request[3].done(): 354 | pending_create_request[3].set_result(True) 355 | else: 356 | registered_watches[response.watch_id] = pending_create_request[0:2] 357 | registered_queues[pending_create_request[1]] = response.watch_id 358 | if pending_create_request[2] is not None and not \ 359 | pending_create_request[2].done(): 360 | pending_create_request[2].set_result(True) 361 | if pending_create_request[3] is not None: 362 | # Immediately cancel the watch 363 | input_queue.put_nowait( 364 | rpc.WatchRequest( 365 | cancel_request= 366 | rpc.WatchCancelRequest(watch_id=response.watch_id) 367 | ) 368 | ) 369 | pending_cancel_requests[response.watch_id] = pending_create_request[3] 370 | pending_create_request = None 371 | if response.events: 372 | last_received_revision = response.header.revision 373 | last_watches_revision[response.watch_id] = last_received_revision 374 | if response.watch_id in registered_watches: 375 | _, output_queue = registered_watches[response.watch_id] 376 | output_queue.put_nowait((True, 377 | [Event(e, last_received_revision) for e in response.events], 378 | last_received_revision)) 379 | if response.compact_revision > 0: 380 | if response.watch_id in registered_watches: 381 | _, output_queue = registered_watches.pop(response.watch_id) 382 | exc = CompactRevisonException(response.compact_revision) 383 | output_queue.put_nowait((False, exc, response.compact_revision)) 384 | del registered_queues[output_queue] 385 | if response.watch_id in pending_cancel_requests: 386 | if not pending_cancel_requests[response.watch_id].done(): 387 | pending_cancel_requests[response.watch_id].set_result(True) 388 | del pending_cancel_requests[response.watch_id] 389 | if response.canceled: 390 | # Cancel response 391 | if response.watch_id in registered_watches: 392 | _, output_queue = registered_watches.pop(response.watch_id) 393 | if response.watch_id in pending_cancel_requests: 394 | # Normal cancel 395 | output_queue.put_nowait((False, None, None)) 396 | else: 397 | output_queue.put_nowait((False, ServerCancelException(response.cancel_reason), _reconnect_revision(response.watch_id))) 398 | del registered_queues[output_queue] 399 | if response.watch_id in pending_cancel_requests: 400 | if not pending_cancel_requests[response.watch_id].done(): 401 | pending_cancel_requests[response.watch_id].set_result(True) 402 | del pending_cancel_requests[response.watch_id] 403 | if self._create_request_queue in pipes: 404 | while pending_create_request is None and not self._create_request_queue.is_empty(): 405 | create_req, output_queue, done_fut = self._create_request_queue.get_nowait() 406 | if done_fut.done(): 407 | # Ignore cancelled create requests 408 | output_queue.put_nowait((False, None, None)) 409 | continue 410 | # Send create request 411 | pending_create_request = (create_req, output_queue, done_fut, None) 412 | input_queue.put_nowait(rpc.WatchRequest(create_request=create_req)) 413 | if reconnect_event.is_set(): 414 | # Reconnected 415 | break 416 | if call_task.done(): 417 | # Maybe not available 418 | if call_task.exception() is not None: 419 | await call_task 420 | else: 421 | break 422 | finally: 423 | input_queue.put_nowait(None) 424 | call_task.cancel() 425 | if quitting: 426 | self._watch_task_running = None 427 | try: 428 | await call_task 429 | except asyncio.CancelledError: 430 | pass 431 | except Exception: 432 | pass 433 | except Exception as exc: 434 | if registered_queues: 435 | for q, watch_id in registered_queues.items(): 436 | 437 | q.put_nowait((False, exc, _reconnect_revision(watch_id))) 438 | if pending_create_request is not None: 439 | pending_create_request[1].put_nowait((False, exc, None)) 440 | if pending_create_request[2] is not None and not pending_create_request[2].done(): 441 | pending_create_request[2].set_exception(exc) 442 | if pending_create_request[3] is not None and not pending_create_request[3].done(): 443 | pending_create_request[3].set_result(True) 444 | if pending_cancel_requests: 445 | for _, fut in pending_cancel_requests.items(): 446 | if not fut.done(): 447 | fut.set_result(True) 448 | if restore_creates: 449 | for q, (_, fut) in restore_creates.items(): 450 | if fut is not None and not fut.done(): 451 | fut.set_result(exc) 452 | q.put_nowait((False, exc, _reconnect_revision(watch_id))) 453 | if not self._create_request_queue.is_empty(): 454 | create_requests = self._create_request_queue.read_nowait() 455 | for r in create_requests: 456 | r[1].put_nowait((False, exc, None)) 457 | if r[2] is not None and not r[2].done(): 458 | r[2].set_exception(exc) 459 | if not self._cancel_request_queue.is_empty(): 460 | cancel_requests = self._cancel_request_queue.read_nowait() 461 | for _, fut in cancel_requests: 462 | if fut is not None and not fut.done(): 463 | fut.set_result(True) 464 | 465 | if exc is CancelledError: 466 | raise 467 | except asyncio.CancelledError: 468 | raise 469 | 470 | finally: 471 | self._watch_task_running = None 472 | 473 | def _update_channel(self, channel): 474 | super()._update_channel(channel) 475 | self._watch_stub = stub.WatchStub(channel) 476 | self._reconnect_event.set() 477 | 478 | def _ensure_watch_task(self): 479 | if self._watch_task_running is None: 480 | self._watch_task_running = asyncio.ensure_future(self._watch_task(self._reconnect_event)) 481 | 482 | async def watch(self, key_range, start_revision=None, noput=False, nodelete=False, prev_kv=False, 483 | always_reconnect=False, ignore_compact=False, batch_events=False, create_event=False): 484 | 485 | filters = [] 486 | if noput: 487 | filters.append(rpc.WatchCreateRequest.NOPUT) 488 | if nodelete: 489 | filters.append(rpc.WatchCreateRequest.NODELETE) 490 | reconnect_revision = start_revision 491 | done_future = None 492 | try: 493 | while True: 494 | watch_request = rpc.WatchCreateRequest(start_revision=reconnect_revision, 495 | filters=filters, 496 | prev_kv=prev_kv) 497 | 498 | put_key_range(watch_request, key_range) 499 | self._ensure_watch_task() 500 | output_queue = asyncio.Queue(loop=self._loop) 501 | done_future = self._loop.create_future() 502 | await self._create_request_queue.put((watch_request, output_queue, done_future)) 503 | try: 504 | await done_future 505 | if create_event: 506 | yield None 507 | create_event = False 508 | while True: 509 | is_event, result, revision = await output_queue.get() 510 | if not is_event: 511 | if revision is not None: 512 | reconnect_revision = revision 513 | if result is None: 514 | break 515 | else: 516 | # When an exception is raised in multiple positions 517 | # the traceback will mix up, so clone the exception 518 | # for each raise 519 | if isinstance(result, WatchException): 520 | raise result._clone() from result 521 | else: 522 | raise WatchException("Watch failed with server exception") from result 523 | else: 524 | reconnect_revision = revision + 1 525 | if batch_events: 526 | yield tuple(result) 527 | else: 528 | for e in result: 529 | yield e 530 | except CompactRevisonException: 531 | if ignore_compact: 532 | continue 533 | else: 534 | raise 535 | except CancelledError: 536 | raise 537 | except Exception: 538 | if always_reconnect: 539 | continue 540 | else: 541 | raise 542 | else: 543 | break 544 | finally: 545 | if done_future is not None and not done_future.done(): 546 | done_future.cancel() 547 | if self._watch_task_running: 548 | done_future = self._loop.create_future() 549 | await self._cancel_request_queue.put((output_queue, done_future)) 550 | if self._watch_task_running: 551 | try: 552 | await done_future 553 | except Exception: 554 | pass 555 | 556 | def watch_scope(self, key_range, start_revision=None, noput=False, nodelete=False, prev_kv=False, 557 | always_reconnect=False, ignore_compact=False, batch_events=False): 558 | return WatchScope(self.watch(key_range, start_revision=start_revision, 559 | noput=noput, nodelete=nodelete, prev_kv=prev_kv, create_event=True, 560 | always_reconnect=always_reconnect, ignore_compact=ignore_compact, 561 | batch_events=batch_events)) 562 | -------------------------------------------------------------------------------- /aioetcd3/protos/rpc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package etcdserverpb; 3 | 4 | import "kv.proto"; 5 | import "auth.proto"; 6 | 7 | // for grpc-gateway 8 | 9 | 10 | service KV { 11 | // Range gets the keys in the range from the key-value store. 12 | rpc Range(RangeRequest) returns (RangeResponse) { 13 | } 14 | 15 | // Put puts the given key into the key-value store. 16 | // A put request increments the revision of the key-value store 17 | // and generates one event in the event history. 18 | rpc Put(PutRequest) returns (PutResponse) { 19 | } 20 | 21 | // DeleteRange deletes the given range from the key-value store. 22 | // A delete request increments the revision of the key-value store 23 | // and generates a delete event in the event history for every deleted key. 24 | rpc DeleteRange(DeleteRangeRequest) returns (DeleteRangeResponse) { 25 | } 26 | 27 | // Txn processes multiple requests in a single transaction. 28 | // A txn request increments the revision of the key-value store 29 | // and generates events with the same revision for every completed request. 30 | // It is not allowed to modify the same key several times within one txn. 31 | rpc Txn(TxnRequest) returns (TxnResponse) { 32 | } 33 | 34 | // Compact compacts the event history in the etcd key-value store. The key-value 35 | // store should be periodically compacted or the event history will continue to grow 36 | // indefinitely. 37 | rpc Compact(CompactionRequest) returns (CompactionResponse) { 38 | } 39 | } 40 | 41 | service Watch { 42 | // Watch watches for events happening or that have happened. Both input and output 43 | // are streams; the input stream is for creating and canceling watchers and the output 44 | // stream sends events. One watch RPC can watch on multiple key ranges, streaming events 45 | // for several watches at once. The entire event history can be watched starting from the 46 | // last compaction revision. 47 | rpc Watch(stream WatchRequest) returns (stream WatchResponse) { 48 | } 49 | } 50 | 51 | service Lease { 52 | // LeaseGrant creates a lease which expires if the server does not receive a keepAlive 53 | // within a given time to live period. All keys attached to the lease will be expired and 54 | // deleted if the lease expires. Each expired key generates a delete event in the event history. 55 | rpc LeaseGrant(LeaseGrantRequest) returns (LeaseGrantResponse) { 56 | } 57 | 58 | // LeaseRevoke revokes a lease. All keys attached to the lease will expire and be deleted. 59 | rpc LeaseRevoke(LeaseRevokeRequest) returns (LeaseRevokeResponse) { 60 | } 61 | 62 | // LeaseKeepAlive keeps the lease alive by streaming keep alive requests from the client 63 | // to the server and streaming keep alive responses from the server to the client. 64 | rpc LeaseKeepAlive(stream LeaseKeepAliveRequest) returns (stream LeaseKeepAliveResponse) { 65 | } 66 | 67 | // LeaseTimeToLive retrieves lease information. 68 | rpc LeaseTimeToLive(LeaseTimeToLiveRequest) returns (LeaseTimeToLiveResponse) { 69 | } 70 | 71 | // TODO(xiangli) List all existing Leases? 72 | } 73 | 74 | service Cluster { 75 | // MemberAdd adds a member into the cluster. 76 | rpc MemberAdd(MemberAddRequest) returns (MemberAddResponse) { 77 | } 78 | 79 | // MemberRemove removes an existing member from the cluster. 80 | rpc MemberRemove(MemberRemoveRequest) returns (MemberRemoveResponse) { 81 | } 82 | 83 | // MemberUpdate updates the member configuration. 84 | rpc MemberUpdate(MemberUpdateRequest) returns (MemberUpdateResponse) { 85 | } 86 | 87 | // MemberList lists all the members in the cluster. 88 | rpc MemberList(MemberListRequest) returns (MemberListResponse) { 89 | } 90 | } 91 | 92 | service Maintenance { 93 | // Alarm activates, deactivates, and queries alarms regarding cluster health. 94 | rpc Alarm(AlarmRequest) returns (AlarmResponse) { 95 | } 96 | 97 | // Status gets the status of the member. 98 | rpc Status(StatusRequest) returns (StatusResponse) { 99 | } 100 | 101 | // Defragment defragments a member's backend database to recover storage space. 102 | rpc Defragment(DefragmentRequest) returns (DefragmentResponse) { 103 | } 104 | 105 | // Hash computes the hash of the KV's backend. 106 | // This is designed for testing; do not use this in production when there 107 | // are ongoing transactions. 108 | rpc Hash(HashRequest) returns (HashResponse) { 109 | } 110 | 111 | // HashKV computes the hash of all MVCC keys up to a given revision. 112 | rpc HashKV(HashKVRequest) returns (HashKVResponse) { 113 | } 114 | 115 | // Snapshot sends a snapshot of the entire backend from a member over a stream to a client. 116 | rpc Snapshot(SnapshotRequest) returns (stream SnapshotResponse) { 117 | } 118 | 119 | // MoveLeader requests current leader node to transfer its leadership to transferee. 120 | rpc MoveLeader(MoveLeaderRequest) returns (MoveLeaderResponse) { 121 | } 122 | } 123 | 124 | service Auth { 125 | // AuthEnable enables authentication. 126 | rpc AuthEnable(AuthEnableRequest) returns (AuthEnableResponse) { 127 | } 128 | 129 | // AuthDisable disables authentication. 130 | rpc AuthDisable(AuthDisableRequest) returns (AuthDisableResponse) { 131 | } 132 | 133 | // Authenticate processes an authenticate request. 134 | rpc Authenticate(AuthenticateRequest) returns (AuthenticateResponse) { 135 | } 136 | 137 | // UserAdd adds a new user. 138 | rpc UserAdd(AuthUserAddRequest) returns (AuthUserAddResponse) { 139 | } 140 | 141 | // UserGet gets detailed user information. 142 | rpc UserGet(AuthUserGetRequest) returns (AuthUserGetResponse) { 143 | } 144 | 145 | // UserList gets a list of all users. 146 | rpc UserList(AuthUserListRequest) returns (AuthUserListResponse) { 147 | } 148 | 149 | // UserDelete deletes a specified user. 150 | rpc UserDelete(AuthUserDeleteRequest) returns (AuthUserDeleteResponse) { 151 | } 152 | 153 | // UserChangePassword changes the password of a specified user. 154 | rpc UserChangePassword(AuthUserChangePasswordRequest) returns (AuthUserChangePasswordResponse) { 155 | } 156 | 157 | // UserGrant grants a role to a specified user. 158 | rpc UserGrantRole(AuthUserGrantRoleRequest) returns (AuthUserGrantRoleResponse) { 159 | } 160 | 161 | // UserRevokeRole revokes a role of specified user. 162 | rpc UserRevokeRole(AuthUserRevokeRoleRequest) returns (AuthUserRevokeRoleResponse) { 163 | } 164 | 165 | // RoleAdd adds a new role. 166 | rpc RoleAdd(AuthRoleAddRequest) returns (AuthRoleAddResponse) { 167 | } 168 | 169 | // RoleGet gets detailed role information. 170 | rpc RoleGet(AuthRoleGetRequest) returns (AuthRoleGetResponse) { 171 | } 172 | 173 | // RoleList gets lists of all roles. 174 | rpc RoleList(AuthRoleListRequest) returns (AuthRoleListResponse) { 175 | } 176 | 177 | // RoleDelete deletes a specified role. 178 | rpc RoleDelete(AuthRoleDeleteRequest) returns (AuthRoleDeleteResponse) { 179 | } 180 | 181 | // RoleGrantPermission grants a permission of a specified key or range to a specified role. 182 | rpc RoleGrantPermission(AuthRoleGrantPermissionRequest) returns (AuthRoleGrantPermissionResponse) { 183 | } 184 | 185 | // RoleRevokePermission revokes a key or range permission of a specified role. 186 | rpc RoleRevokePermission(AuthRoleRevokePermissionRequest) returns (AuthRoleRevokePermissionResponse) { 187 | } 188 | } 189 | 190 | message ResponseHeader { 191 | // cluster_id is the ID of the cluster which sent the response. 192 | uint64 cluster_id = 1; 193 | // member_id is the ID of the member which sent the response. 194 | uint64 member_id = 2; 195 | // revision is the key-value store revision when the request was applied. 196 | int64 revision = 3; 197 | // raft_term is the raft term when the request was applied. 198 | uint64 raft_term = 4; 199 | } 200 | 201 | message RangeRequest { 202 | enum SortOrder { 203 | NONE = 0; // default, no sorting 204 | ASCEND = 1; // lowest target value first 205 | DESCEND = 2; // highest target value first 206 | } 207 | enum SortTarget { 208 | KEY = 0; 209 | VERSION = 1; 210 | CREATE = 2; 211 | MOD = 3; 212 | VALUE = 4; 213 | } 214 | 215 | // key is the first key for the range. If range_end is not given, the request only looks up key. 216 | bytes key = 1; 217 | // range_end is the upper bound on the requested range [key, range_end). 218 | // If range_end is '\0', the range is all keys >= key. 219 | // If range_end is key plus one (e.g., "aa"+1 == "ab", "a\xff"+1 == "b"), 220 | // then the range request gets all keys prefixed with key. 221 | // If both key and range_end are '\0', then the range request returns all keys. 222 | bytes range_end = 2; 223 | // limit is a limit on the number of keys returned for the request. When limit is set to 0, 224 | // it is treated as no limit. 225 | int64 limit = 3; 226 | // revision is the point-in-time of the key-value store to use for the range. 227 | // If revision is less or equal to zero, the range is over the newest key-value store. 228 | // If the revision has been compacted, ErrCompacted is returned as a response. 229 | int64 revision = 4; 230 | 231 | // sort_order is the order for returned sorted results. 232 | SortOrder sort_order = 5; 233 | 234 | // sort_target is the key-value field to use for sorting. 235 | SortTarget sort_target = 6; 236 | 237 | // serializable sets the range request to use serializable member-local reads. 238 | // Range requests are linearizable by default; linearizable requests have higher 239 | // latency and lower throughput than serializable requests but reflect the current 240 | // consensus of the cluster. For better performance, in exchange for possible stale reads, 241 | // a serializable range request is served locally without needing to reach consensus 242 | // with other nodes in the cluster. 243 | bool serializable = 7; 244 | 245 | // keys_only when set returns only the keys and not the values. 246 | bool keys_only = 8; 247 | 248 | // count_only when set returns only the count of the keys in the range. 249 | bool count_only = 9; 250 | 251 | // min_mod_revision is the lower bound for returned key mod revisions; all keys with 252 | // lesser mod revisions will be filtered away. 253 | int64 min_mod_revision = 10; 254 | 255 | // max_mod_revision is the upper bound for returned key mod revisions; all keys with 256 | // greater mod revisions will be filtered away. 257 | int64 max_mod_revision = 11; 258 | 259 | // min_create_revision is the lower bound for returned key create revisions; all keys with 260 | // lesser create trevisions will be filtered away. 261 | int64 min_create_revision = 12; 262 | 263 | // max_create_revision is the upper bound for returned key create revisions; all keys with 264 | // greater create revisions will be filtered away. 265 | int64 max_create_revision = 13; 266 | } 267 | 268 | message RangeResponse { 269 | ResponseHeader header = 1; 270 | // kvs is the list of key-value pairs matched by the range request. 271 | // kvs is empty when count is requested. 272 | repeated mvccpb.KeyValue kvs = 2; 273 | // more indicates if there are more keys to return in the requested range. 274 | bool more = 3; 275 | // count is set to the number of keys within the range when requested. 276 | int64 count = 4; 277 | } 278 | 279 | message PutRequest { 280 | // key is the key, in bytes, to put into the key-value store. 281 | bytes key = 1; 282 | // value is the value, in bytes, to associate with the key in the key-value store. 283 | bytes value = 2; 284 | // lease is the lease ID to associate with the key in the key-value store. A lease 285 | // value of 0 indicates no lease. 286 | int64 lease = 3; 287 | 288 | // If prev_kv is set, etcd gets the previous key-value pair before changing it. 289 | // The previous key-value pair will be returned in the put response. 290 | bool prev_kv = 4; 291 | 292 | // If ignore_value is set, etcd updates the key using its current value. 293 | // Returns an error if the key does not exist. 294 | bool ignore_value = 5; 295 | 296 | // If ignore_lease is set, etcd updates the key using its current lease. 297 | // Returns an error if the key does not exist. 298 | bool ignore_lease = 6; 299 | } 300 | 301 | message PutResponse { 302 | ResponseHeader header = 1; 303 | // if prev_kv is set in the request, the previous key-value pair will be returned. 304 | mvccpb.KeyValue prev_kv = 2; 305 | } 306 | 307 | message DeleteRangeRequest { 308 | // key is the first key to delete in the range. 309 | bytes key = 1; 310 | // range_end is the key following the last key to delete for the range [key, range_end). 311 | // If range_end is not given, the range is defined to contain only the key argument. 312 | // If range_end is one bit larger than the given key, then the range is all the keys 313 | // with the prefix (the given key). 314 | // If range_end is '\0', the range is all keys greater than or equal to the key argument. 315 | bytes range_end = 2; 316 | 317 | // If prev_kv is set, etcd gets the previous key-value pairs before deleting it. 318 | // The previous key-value pairs will be returned in the delete response. 319 | bool prev_kv = 3; 320 | } 321 | 322 | message DeleteRangeResponse { 323 | ResponseHeader header = 1; 324 | // deleted is the number of keys deleted by the delete range request. 325 | int64 deleted = 2; 326 | // if prev_kv is set in the request, the previous key-value pairs will be returned. 327 | repeated mvccpb.KeyValue prev_kvs = 3; 328 | } 329 | 330 | message RequestOp { 331 | // request is a union of request types accepted by a transaction. 332 | oneof request { 333 | RangeRequest request_range = 1; 334 | PutRequest request_put = 2; 335 | DeleteRangeRequest request_delete_range = 3; 336 | TxnRequest request_txn = 4; 337 | } 338 | } 339 | 340 | message ResponseOp { 341 | // response is a union of response types returned by a transaction. 342 | oneof response { 343 | RangeResponse response_range = 1; 344 | PutResponse response_put = 2; 345 | DeleteRangeResponse response_delete_range = 3; 346 | TxnResponse response_txn = 4; 347 | } 348 | } 349 | 350 | message Compare { 351 | enum CompareResult { 352 | EQUAL = 0; 353 | GREATER = 1; 354 | LESS = 2; 355 | NOT_EQUAL = 3; 356 | } 357 | enum CompareTarget { 358 | VERSION = 0; 359 | CREATE = 1; 360 | MOD = 2; 361 | VALUE= 3; 362 | LEASE = 4; 363 | } 364 | // result is logical comparison operation for this comparison. 365 | CompareResult result = 1; 366 | // target is the key-value field to inspect for the comparison. 367 | CompareTarget target = 2; 368 | // key is the subject key for the comparison operation. 369 | bytes key = 3; 370 | oneof target_union { 371 | // version is the version of the given key 372 | int64 version = 4; 373 | // create_revision is the creation revision of the given key 374 | int64 create_revision = 5; 375 | // mod_revision is the last modified revision of the given key. 376 | int64 mod_revision = 6; 377 | // value is the value of the given key, in bytes. 378 | bytes value = 7; 379 | // lease is the lease id of the given key. 380 | int64 lease = 8; 381 | // leave room for more target_union field tags, jump to 64 382 | } 383 | 384 | // range_end compares the given target to all keys in the range [key, range_end). 385 | // See RangeRequest for more details on key ranges. 386 | bytes range_end = 64; 387 | // TODO: fill out with most of the rest of RangeRequest fields when needed. 388 | } 389 | 390 | // From google paxosdb paper: 391 | // Our implementation hinges around a powerful primitive which we call MultiOp. All other database 392 | // operations except for iteration are implemented as a single call to MultiOp. A MultiOp is applied atomically 393 | // and consists of three components: 394 | // 1. A list of tests called guard. Each test in guard checks a single entry in the database. It may check 395 | // for the absence or presence of a value, or compare with a given value. Two different tests in the guard 396 | // may apply to the same or different entries in the database. All tests in the guard are applied and 397 | // MultiOp returns the results. If all tests are true, MultiOp executes t op (see item 2 below), otherwise 398 | // it executes f op (see item 3 below). 399 | // 2. A list of database operations called t op. Each operation in the list is either an insert, delete, or 400 | // lookup operation, and applies to a single database entry. Two different operations in the list may apply 401 | // to the same or different entries in the database. These operations are executed 402 | // if guard evaluates to 403 | // true. 404 | // 3. A list of database operations called f op. Like t op, but executed if guard evaluates to false. 405 | message TxnRequest { 406 | // compare is a list of predicates representing a conjunction of terms. 407 | // If the comparisons succeed, then the success requests will be processed in order, 408 | // and the response will contain their respective responses in order. 409 | // If the comparisons fail, then the failure requests will be processed in order, 410 | // and the response will contain their respective responses in order. 411 | repeated Compare compare = 1; 412 | // success is a list of requests which will be applied when compare evaluates to true. 413 | repeated RequestOp success = 2; 414 | // failure is a list of requests which will be applied when compare evaluates to false. 415 | repeated RequestOp failure = 3; 416 | } 417 | 418 | message TxnResponse { 419 | ResponseHeader header = 1; 420 | // succeeded is set to true if the compare evaluated to true or false otherwise. 421 | bool succeeded = 2; 422 | // responses is a list of responses corresponding to the results from applying 423 | // success if succeeded is true or failure if succeeded is false. 424 | repeated ResponseOp responses = 3; 425 | } 426 | 427 | // CompactionRequest compacts the key-value store up to a given revision. All superseded keys 428 | // with a revision less than the compaction revision will be removed. 429 | message CompactionRequest { 430 | // revision is the key-value store revision for the compaction operation. 431 | int64 revision = 1; 432 | // physical is set so the RPC will wait until the compaction is physically 433 | // applied to the local database such that compacted entries are totally 434 | // removed from the backend database. 435 | bool physical = 2; 436 | } 437 | 438 | message CompactionResponse { 439 | ResponseHeader header = 1; 440 | } 441 | 442 | message HashRequest { 443 | } 444 | 445 | message HashKVRequest { 446 | // revision is the key-value store revision for the hash operation. 447 | int64 revision = 1; 448 | } 449 | 450 | message HashKVResponse { 451 | ResponseHeader header = 1; 452 | // hash is the hash value computed from the responding member's MVCC keys up to a given revision. 453 | uint32 hash = 2; 454 | // compact_revision is the compacted revision of key-value store when hash begins. 455 | int64 compact_revision = 3; 456 | } 457 | 458 | message HashResponse { 459 | ResponseHeader header = 1; 460 | // hash is the hash value computed from the responding member's KV's backend. 461 | uint32 hash = 2; 462 | } 463 | 464 | message SnapshotRequest { 465 | } 466 | 467 | message SnapshotResponse { 468 | // header has the current key-value store information. The first header in the snapshot 469 | // stream indicates the point in time of the snapshot. 470 | ResponseHeader header = 1; 471 | 472 | // remaining_bytes is the number of blob bytes to be sent after this message 473 | uint64 remaining_bytes = 2; 474 | 475 | // blob contains the next chunk of the snapshot in the snapshot stream. 476 | bytes blob = 3; 477 | } 478 | 479 | message WatchRequest { 480 | // request_union is a request to either create a new watcher or cancel an existing watcher. 481 | oneof request_union { 482 | WatchCreateRequest create_request = 1; 483 | WatchCancelRequest cancel_request = 2; 484 | } 485 | } 486 | 487 | message WatchCreateRequest { 488 | // key is the key to register for watching. 489 | bytes key = 1; 490 | // range_end is the end of the range [key, range_end) to watch. If range_end is not given, 491 | // only the key argument is watched. If range_end is equal to '\0', all keys greater than 492 | // or equal to the key argument are watched. 493 | // If the range_end is one bit larger than the given key, 494 | // then all keys with the prefix (the given key) will be watched. 495 | bytes range_end = 2; 496 | // start_revision is an optional revision to watch from (inclusive). No start_revision is "now". 497 | int64 start_revision = 3; 498 | // progress_notify is set so that the etcd server will periodically send a WatchResponse with 499 | // no events to the new watcher if there are no recent events. It is useful when clients 500 | // wish to recover a disconnected watcher starting from a recent known revision. 501 | // The etcd server may decide how often it will send notifications based on current load. 502 | bool progress_notify = 4; 503 | 504 | enum FilterType { 505 | // filter out put event. 506 | NOPUT = 0; 507 | // filter out delete event. 508 | NODELETE = 1; 509 | } 510 | // filters filter the events at server side before it sends back to the watcher. 511 | repeated FilterType filters = 5; 512 | 513 | // If prev_kv is set, created watcher gets the previous KV before the event happens. 514 | // If the previous KV is already compacted, nothing will be returned. 515 | bool prev_kv = 6; 516 | } 517 | 518 | message WatchCancelRequest { 519 | // watch_id is the watcher id to cancel so that no more events are transmitted. 520 | int64 watch_id = 1; 521 | } 522 | 523 | message WatchResponse { 524 | ResponseHeader header = 1; 525 | // watch_id is the ID of the watcher that corresponds to the response. 526 | int64 watch_id = 2; 527 | // created is set to true if the response is for a create watch request. 528 | // The client should record the watch_id and expect to receive events for 529 | // the created watcher from the same stream. 530 | // All events sent to the created watcher will attach with the same watch_id. 531 | bool created = 3; 532 | // canceled is set to true if the response is for a cancel watch request. 533 | // No further events will be sent to the canceled watcher. 534 | bool canceled = 4; 535 | // compact_revision is set to the minimum index if a watcher tries to watch 536 | // at a compacted index. 537 | // 538 | // This happens when creating a watcher at a compacted revision or the watcher cannot 539 | // catch up with the progress of the key-value store. 540 | // 541 | // The client should treat the watcher as canceled and should not try to create any 542 | // watcher with the same start_revision again. 543 | int64 compact_revision = 5; 544 | 545 | // cancel_reason indicates the reason for canceling the watcher. 546 | string cancel_reason = 6; 547 | 548 | repeated mvccpb.Event events = 11; 549 | } 550 | 551 | message LeaseGrantRequest { 552 | // TTL is the advisory time-to-live in seconds. 553 | int64 TTL = 1; 554 | // ID is the requested ID for the lease. If ID is set to 0, the lessor chooses an ID. 555 | int64 ID = 2; 556 | } 557 | 558 | message LeaseGrantResponse { 559 | ResponseHeader header = 1; 560 | // ID is the lease ID for the granted lease. 561 | int64 ID = 2; 562 | // TTL is the server chosen lease time-to-live in seconds. 563 | int64 TTL = 3; 564 | string error = 4; 565 | } 566 | 567 | message LeaseRevokeRequest { 568 | // ID is the lease ID to revoke. When the ID is revoked, all associated keys will be deleted. 569 | int64 ID = 1; 570 | } 571 | 572 | message LeaseRevokeResponse { 573 | ResponseHeader header = 1; 574 | } 575 | 576 | message LeaseKeepAliveRequest { 577 | // ID is the lease ID for the lease to keep alive. 578 | int64 ID = 1; 579 | } 580 | 581 | message LeaseKeepAliveResponse { 582 | ResponseHeader header = 1; 583 | // ID is the lease ID from the keep alive request. 584 | int64 ID = 2; 585 | // TTL is the new time-to-live for the lease. 586 | int64 TTL = 3; 587 | } 588 | 589 | message LeaseTimeToLiveRequest { 590 | // ID is the lease ID for the lease. 591 | int64 ID = 1; 592 | // keys is true to query all the keys attached to this lease. 593 | bool keys = 2; 594 | } 595 | 596 | message LeaseTimeToLiveResponse { 597 | ResponseHeader header = 1; 598 | // ID is the lease ID from the keep alive request. 599 | int64 ID = 2; 600 | // TTL is the remaining TTL in seconds for the lease; the lease will expire in under TTL+1 seconds. 601 | int64 TTL = 3; 602 | // GrantedTTL is the initial granted time in seconds upon lease creation/renewal. 603 | int64 grantedTTL = 4; 604 | // Keys is the list of keys attached to this lease. 605 | repeated bytes keys = 5; 606 | } 607 | 608 | message Member { 609 | // ID is the member ID for this member. 610 | uint64 ID = 1; 611 | // name is the human-readable name of the member. If the member is not started, the name will be an empty string. 612 | string name = 2; 613 | // peerURLs is the list of URLs the member exposes to the cluster for communication. 614 | repeated string peerURLs = 3; 615 | // clientURLs is the list of URLs the member exposes to clients for communication. If the member is not started, clientURLs will be empty. 616 | repeated string clientURLs = 4; 617 | } 618 | 619 | message MemberAddRequest { 620 | // peerURLs is the list of URLs the added member will use to communicate with the cluster. 621 | repeated string peerURLs = 1; 622 | } 623 | 624 | message MemberAddResponse { 625 | ResponseHeader header = 1; 626 | // member is the member information for the added member. 627 | Member member = 2; 628 | // members is a list of all members after adding the new member. 629 | repeated Member members = 3; 630 | } 631 | 632 | message MemberRemoveRequest { 633 | // ID is the member ID of the member to remove. 634 | uint64 ID = 1; 635 | } 636 | 637 | message MemberRemoveResponse { 638 | ResponseHeader header = 1; 639 | // members is a list of all members after removing the member. 640 | repeated Member members = 2; 641 | } 642 | 643 | message MemberUpdateRequest { 644 | // ID is the member ID of the member to update. 645 | uint64 ID = 1; 646 | // peerURLs is the new list of URLs the member will use to communicate with the cluster. 647 | repeated string peerURLs = 2; 648 | } 649 | 650 | message MemberUpdateResponse{ 651 | ResponseHeader header = 1; 652 | // members is a list of all members after updating the member. 653 | repeated Member members = 2; 654 | } 655 | 656 | message MemberListRequest { 657 | } 658 | 659 | message MemberListResponse { 660 | ResponseHeader header = 1; 661 | // members is a list of all members associated with the cluster. 662 | repeated Member members = 2; 663 | } 664 | 665 | message DefragmentRequest { 666 | } 667 | 668 | message DefragmentResponse { 669 | ResponseHeader header = 1; 670 | } 671 | 672 | message MoveLeaderRequest { 673 | // targetID is the node ID for the new leader. 674 | uint64 targetID = 1; 675 | } 676 | 677 | message MoveLeaderResponse { 678 | ResponseHeader header = 1; 679 | } 680 | 681 | enum AlarmType { 682 | NONE = 0; // default, used to query if any alarm is active 683 | NOSPACE = 1; // space quota is exhausted 684 | } 685 | 686 | message AlarmRequest { 687 | enum AlarmAction { 688 | GET = 0; 689 | ACTIVATE = 1; 690 | DEACTIVATE = 2; 691 | } 692 | // action is the kind of alarm request to issue. The action 693 | // may GET alarm statuses, ACTIVATE an alarm, or DEACTIVATE a 694 | // raised alarm. 695 | AlarmAction action = 1; 696 | // memberID is the ID of the member associated with the alarm. If memberID is 0, the 697 | // alarm request covers all members. 698 | uint64 memberID = 2; 699 | // alarm is the type of alarm to consider for this request. 700 | AlarmType alarm = 3; 701 | } 702 | 703 | message AlarmMember { 704 | // memberID is the ID of the member associated with the raised alarm. 705 | uint64 memberID = 1; 706 | // alarm is the type of alarm which has been raised. 707 | AlarmType alarm = 2; 708 | } 709 | 710 | message AlarmResponse { 711 | ResponseHeader header = 1; 712 | // alarms is a list of alarms associated with the alarm request. 713 | repeated AlarmMember alarms = 2; 714 | } 715 | 716 | message StatusRequest { 717 | } 718 | 719 | message StatusResponse { 720 | ResponseHeader header = 1; 721 | // version is the cluster protocol version used by the responding member. 722 | string version = 2; 723 | // dbSize is the size of the backend database, in bytes, of the responding member. 724 | int64 dbSize = 3; 725 | // leader is the member ID which the responding member believes is the current leader. 726 | uint64 leader = 4; 727 | // raftIndex is the current raft index of the responding member. 728 | uint64 raftIndex = 5; 729 | // raftTerm is the current raft term of the responding member. 730 | uint64 raftTerm = 6; 731 | } 732 | 733 | message AuthEnableRequest { 734 | } 735 | 736 | message AuthDisableRequest { 737 | } 738 | 739 | message AuthenticateRequest { 740 | string name = 1; 741 | string password = 2; 742 | } 743 | 744 | message AuthUserAddRequest { 745 | string name = 1; 746 | string password = 2; 747 | } 748 | 749 | message AuthUserGetRequest { 750 | string name = 1; 751 | } 752 | 753 | message AuthUserDeleteRequest { 754 | // name is the name of the user to delete. 755 | string name = 1; 756 | } 757 | 758 | message AuthUserChangePasswordRequest { 759 | // name is the name of the user whose password is being changed. 760 | string name = 1; 761 | // password is the new password for the user. 762 | string password = 2; 763 | } 764 | 765 | message AuthUserGrantRoleRequest { 766 | // user is the name of the user which should be granted a given role. 767 | string user = 1; 768 | // role is the name of the role to grant to the user. 769 | string role = 2; 770 | } 771 | 772 | message AuthUserRevokeRoleRequest { 773 | string name = 1; 774 | string role = 2; 775 | } 776 | 777 | message AuthRoleAddRequest { 778 | // name is the name of the role to add to the authentication system. 779 | string name = 1; 780 | } 781 | 782 | message AuthRoleGetRequest { 783 | string role = 1; 784 | } 785 | 786 | message AuthUserListRequest { 787 | } 788 | 789 | message AuthRoleListRequest { 790 | } 791 | 792 | message AuthRoleDeleteRequest { 793 | string role = 1; 794 | } 795 | 796 | message AuthRoleGrantPermissionRequest { 797 | // name is the name of the role which will be granted the permission. 798 | string name = 1; 799 | // perm is the permission to grant to the role. 800 | authpb.Permission perm = 2; 801 | } 802 | 803 | message AuthRoleRevokePermissionRequest { 804 | string role = 1; 805 | string key = 2; 806 | string range_end = 3; 807 | } 808 | 809 | message AuthEnableResponse { 810 | ResponseHeader header = 1; 811 | } 812 | 813 | message AuthDisableResponse { 814 | ResponseHeader header = 1; 815 | } 816 | 817 | message AuthenticateResponse { 818 | ResponseHeader header = 1; 819 | // token is an authorized token that can be used in succeeding RPCs 820 | string token = 2; 821 | } 822 | 823 | message AuthUserAddResponse { 824 | ResponseHeader header = 1; 825 | } 826 | 827 | message AuthUserGetResponse { 828 | ResponseHeader header = 1; 829 | 830 | repeated string roles = 2; 831 | } 832 | 833 | message AuthUserDeleteResponse { 834 | ResponseHeader header = 1; 835 | } 836 | 837 | message AuthUserChangePasswordResponse { 838 | ResponseHeader header = 1; 839 | } 840 | 841 | message AuthUserGrantRoleResponse { 842 | ResponseHeader header = 1; 843 | } 844 | 845 | message AuthUserRevokeRoleResponse { 846 | ResponseHeader header = 1; 847 | } 848 | 849 | message AuthRoleAddResponse { 850 | ResponseHeader header = 1; 851 | } 852 | 853 | message AuthRoleGetResponse { 854 | ResponseHeader header = 1; 855 | 856 | repeated authpb.Permission perm = 2; 857 | } 858 | 859 | message AuthRoleListResponse { 860 | ResponseHeader header = 1; 861 | 862 | repeated string roles = 2; 863 | } 864 | 865 | message AuthUserListResponse { 866 | ResponseHeader header = 1; 867 | 868 | repeated string users = 2; 869 | } 870 | 871 | message AuthRoleDeleteResponse { 872 | ResponseHeader header = 1; 873 | } 874 | 875 | message AuthRoleGrantPermissionResponse { 876 | ResponseHeader header = 1; 877 | } 878 | 879 | message AuthRoleRevokePermissionResponse { 880 | ResponseHeader header = 1; 881 | } 882 | -------------------------------------------------------------------------------- /aioetcd3/_etcdv3/rpc_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | import grpc 3 | 4 | from aioetcd3._etcdv3 import rpc_pb2 as rpc__pb2 5 | 6 | 7 | class KVStub(object): 8 | """for grpc-gateway 9 | 10 | """ 11 | 12 | def __init__(self, channel): 13 | """Constructor. 14 | 15 | Args: 16 | channel: A grpc.Channel. 17 | """ 18 | self.Range = channel.unary_unary( 19 | '/etcdserverpb.KV/Range', 20 | request_serializer=rpc__pb2.RangeRequest.SerializeToString, 21 | response_deserializer=rpc__pb2.RangeResponse.FromString, 22 | ) 23 | self.Put = channel.unary_unary( 24 | '/etcdserverpb.KV/Put', 25 | request_serializer=rpc__pb2.PutRequest.SerializeToString, 26 | response_deserializer=rpc__pb2.PutResponse.FromString, 27 | ) 28 | self.DeleteRange = channel.unary_unary( 29 | '/etcdserverpb.KV/DeleteRange', 30 | request_serializer=rpc__pb2.DeleteRangeRequest.SerializeToString, 31 | response_deserializer=rpc__pb2.DeleteRangeResponse.FromString, 32 | ) 33 | self.Txn = channel.unary_unary( 34 | '/etcdserverpb.KV/Txn', 35 | request_serializer=rpc__pb2.TxnRequest.SerializeToString, 36 | response_deserializer=rpc__pb2.TxnResponse.FromString, 37 | ) 38 | self.Compact = channel.unary_unary( 39 | '/etcdserverpb.KV/Compact', 40 | request_serializer=rpc__pb2.CompactionRequest.SerializeToString, 41 | response_deserializer=rpc__pb2.CompactionResponse.FromString, 42 | ) 43 | 44 | 45 | class KVServicer(object): 46 | """for grpc-gateway 47 | 48 | """ 49 | 50 | def Range(self, request, context): 51 | """Range gets the keys in the range from the key-value store. 52 | """ 53 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 54 | context.set_details('Method not implemented!') 55 | raise NotImplementedError('Method not implemented!') 56 | 57 | def Put(self, request, context): 58 | """Put puts the given key into the key-value store. 59 | A put request increments the revision of the key-value store 60 | and generates one event in the event history. 61 | """ 62 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 63 | context.set_details('Method not implemented!') 64 | raise NotImplementedError('Method not implemented!') 65 | 66 | def DeleteRange(self, request, context): 67 | """DeleteRange deletes the given range from the key-value store. 68 | A delete request increments the revision of the key-value store 69 | and generates a delete event in the event history for every deleted key. 70 | """ 71 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 72 | context.set_details('Method not implemented!') 73 | raise NotImplementedError('Method not implemented!') 74 | 75 | def Txn(self, request, context): 76 | """Txn processes multiple requests in a single transaction. 77 | A txn request increments the revision of the key-value store 78 | and generates events with the same revision for every completed request. 79 | It is not allowed to modify the same key several times within one txn. 80 | """ 81 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 82 | context.set_details('Method not implemented!') 83 | raise NotImplementedError('Method not implemented!') 84 | 85 | def Compact(self, request, context): 86 | """Compact compacts the event history in the etcd key-value store. The key-value 87 | store should be periodically compacted or the event history will continue to grow 88 | indefinitely. 89 | """ 90 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 91 | context.set_details('Method not implemented!') 92 | raise NotImplementedError('Method not implemented!') 93 | 94 | 95 | def add_KVServicer_to_server(servicer, server): 96 | rpc_method_handlers = { 97 | 'Range': grpc.unary_unary_rpc_method_handler( 98 | servicer.Range, 99 | request_deserializer=rpc__pb2.RangeRequest.FromString, 100 | response_serializer=rpc__pb2.RangeResponse.SerializeToString, 101 | ), 102 | 'Put': grpc.unary_unary_rpc_method_handler( 103 | servicer.Put, 104 | request_deserializer=rpc__pb2.PutRequest.FromString, 105 | response_serializer=rpc__pb2.PutResponse.SerializeToString, 106 | ), 107 | 'DeleteRange': grpc.unary_unary_rpc_method_handler( 108 | servicer.DeleteRange, 109 | request_deserializer=rpc__pb2.DeleteRangeRequest.FromString, 110 | response_serializer=rpc__pb2.DeleteRangeResponse.SerializeToString, 111 | ), 112 | 'Txn': grpc.unary_unary_rpc_method_handler( 113 | servicer.Txn, 114 | request_deserializer=rpc__pb2.TxnRequest.FromString, 115 | response_serializer=rpc__pb2.TxnResponse.SerializeToString, 116 | ), 117 | 'Compact': grpc.unary_unary_rpc_method_handler( 118 | servicer.Compact, 119 | request_deserializer=rpc__pb2.CompactionRequest.FromString, 120 | response_serializer=rpc__pb2.CompactionResponse.SerializeToString, 121 | ), 122 | } 123 | generic_handler = grpc.method_handlers_generic_handler( 124 | 'etcdserverpb.KV', rpc_method_handlers) 125 | server.add_generic_rpc_handlers((generic_handler,)) 126 | 127 | 128 | class WatchStub(object): 129 | # missing associated documentation comment in .proto file 130 | pass 131 | 132 | def __init__(self, channel): 133 | """Constructor. 134 | 135 | Args: 136 | channel: A grpc.Channel. 137 | """ 138 | self.Watch = channel.stream_stream( 139 | '/etcdserverpb.Watch/Watch', 140 | request_serializer=rpc__pb2.WatchRequest.SerializeToString, 141 | response_deserializer=rpc__pb2.WatchResponse.FromString, 142 | ) 143 | 144 | 145 | class WatchServicer(object): 146 | # missing associated documentation comment in .proto file 147 | pass 148 | 149 | def Watch(self, request_iterator, context): 150 | """Watch watches for events happening or that have happened. Both input and output 151 | are streams; the input stream is for creating and canceling watchers and the output 152 | stream sends events. One watch RPC can watch on multiple key ranges, streaming events 153 | for several watches at once. The entire event history can be watched starting from the 154 | last compaction revision. 155 | """ 156 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 157 | context.set_details('Method not implemented!') 158 | raise NotImplementedError('Method not implemented!') 159 | 160 | 161 | def add_WatchServicer_to_server(servicer, server): 162 | rpc_method_handlers = { 163 | 'Watch': grpc.stream_stream_rpc_method_handler( 164 | servicer.Watch, 165 | request_deserializer=rpc__pb2.WatchRequest.FromString, 166 | response_serializer=rpc__pb2.WatchResponse.SerializeToString, 167 | ), 168 | } 169 | generic_handler = grpc.method_handlers_generic_handler( 170 | 'etcdserverpb.Watch', rpc_method_handlers) 171 | server.add_generic_rpc_handlers((generic_handler,)) 172 | 173 | 174 | class LeaseStub(object): 175 | # missing associated documentation comment in .proto file 176 | pass 177 | 178 | def __init__(self, channel): 179 | """Constructor. 180 | 181 | Args: 182 | channel: A grpc.Channel. 183 | """ 184 | self.LeaseGrant = channel.unary_unary( 185 | '/etcdserverpb.Lease/LeaseGrant', 186 | request_serializer=rpc__pb2.LeaseGrantRequest.SerializeToString, 187 | response_deserializer=rpc__pb2.LeaseGrantResponse.FromString, 188 | ) 189 | self.LeaseRevoke = channel.unary_unary( 190 | '/etcdserverpb.Lease/LeaseRevoke', 191 | request_serializer=rpc__pb2.LeaseRevokeRequest.SerializeToString, 192 | response_deserializer=rpc__pb2.LeaseRevokeResponse.FromString, 193 | ) 194 | self.LeaseKeepAlive = channel.stream_stream( 195 | '/etcdserverpb.Lease/LeaseKeepAlive', 196 | request_serializer=rpc__pb2.LeaseKeepAliveRequest.SerializeToString, 197 | response_deserializer=rpc__pb2.LeaseKeepAliveResponse.FromString, 198 | ) 199 | self.LeaseTimeToLive = channel.unary_unary( 200 | '/etcdserverpb.Lease/LeaseTimeToLive', 201 | request_serializer=rpc__pb2.LeaseTimeToLiveRequest.SerializeToString, 202 | response_deserializer=rpc__pb2.LeaseTimeToLiveResponse.FromString, 203 | ) 204 | 205 | 206 | class LeaseServicer(object): 207 | # missing associated documentation comment in .proto file 208 | pass 209 | 210 | def LeaseGrant(self, request, context): 211 | """LeaseGrant creates a lease which expires if the server does not receive a keepAlive 212 | within a given time to live period. All keys attached to the lease will be expired and 213 | deleted if the lease expires. Each expired key generates a delete event in the event history. 214 | """ 215 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 216 | context.set_details('Method not implemented!') 217 | raise NotImplementedError('Method not implemented!') 218 | 219 | def LeaseRevoke(self, request, context): 220 | """LeaseRevoke revokes a lease. All keys attached to the lease will expire and be deleted. 221 | """ 222 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 223 | context.set_details('Method not implemented!') 224 | raise NotImplementedError('Method not implemented!') 225 | 226 | def LeaseKeepAlive(self, request_iterator, context): 227 | """LeaseKeepAlive keeps the lease alive by streaming keep alive requests from the client 228 | to the server and streaming keep alive responses from the server to the client. 229 | """ 230 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 231 | context.set_details('Method not implemented!') 232 | raise NotImplementedError('Method not implemented!') 233 | 234 | def LeaseTimeToLive(self, request, context): 235 | """LeaseTimeToLive retrieves lease information. 236 | """ 237 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 238 | context.set_details('Method not implemented!') 239 | raise NotImplementedError('Method not implemented!') 240 | 241 | 242 | def add_LeaseServicer_to_server(servicer, server): 243 | rpc_method_handlers = { 244 | 'LeaseGrant': grpc.unary_unary_rpc_method_handler( 245 | servicer.LeaseGrant, 246 | request_deserializer=rpc__pb2.LeaseGrantRequest.FromString, 247 | response_serializer=rpc__pb2.LeaseGrantResponse.SerializeToString, 248 | ), 249 | 'LeaseRevoke': grpc.unary_unary_rpc_method_handler( 250 | servicer.LeaseRevoke, 251 | request_deserializer=rpc__pb2.LeaseRevokeRequest.FromString, 252 | response_serializer=rpc__pb2.LeaseRevokeResponse.SerializeToString, 253 | ), 254 | 'LeaseKeepAlive': grpc.stream_stream_rpc_method_handler( 255 | servicer.LeaseKeepAlive, 256 | request_deserializer=rpc__pb2.LeaseKeepAliveRequest.FromString, 257 | response_serializer=rpc__pb2.LeaseKeepAliveResponse.SerializeToString, 258 | ), 259 | 'LeaseTimeToLive': grpc.unary_unary_rpc_method_handler( 260 | servicer.LeaseTimeToLive, 261 | request_deserializer=rpc__pb2.LeaseTimeToLiveRequest.FromString, 262 | response_serializer=rpc__pb2.LeaseTimeToLiveResponse.SerializeToString, 263 | ), 264 | } 265 | generic_handler = grpc.method_handlers_generic_handler( 266 | 'etcdserverpb.Lease', rpc_method_handlers) 267 | server.add_generic_rpc_handlers((generic_handler,)) 268 | 269 | 270 | class ClusterStub(object): 271 | # missing associated documentation comment in .proto file 272 | pass 273 | 274 | def __init__(self, channel): 275 | """Constructor. 276 | 277 | Args: 278 | channel: A grpc.Channel. 279 | """ 280 | self.MemberAdd = channel.unary_unary( 281 | '/etcdserverpb.Cluster/MemberAdd', 282 | request_serializer=rpc__pb2.MemberAddRequest.SerializeToString, 283 | response_deserializer=rpc__pb2.MemberAddResponse.FromString, 284 | ) 285 | self.MemberRemove = channel.unary_unary( 286 | '/etcdserverpb.Cluster/MemberRemove', 287 | request_serializer=rpc__pb2.MemberRemoveRequest.SerializeToString, 288 | response_deserializer=rpc__pb2.MemberRemoveResponse.FromString, 289 | ) 290 | self.MemberUpdate = channel.unary_unary( 291 | '/etcdserverpb.Cluster/MemberUpdate', 292 | request_serializer=rpc__pb2.MemberUpdateRequest.SerializeToString, 293 | response_deserializer=rpc__pb2.MemberUpdateResponse.FromString, 294 | ) 295 | self.MemberList = channel.unary_unary( 296 | '/etcdserverpb.Cluster/MemberList', 297 | request_serializer=rpc__pb2.MemberListRequest.SerializeToString, 298 | response_deserializer=rpc__pb2.MemberListResponse.FromString, 299 | ) 300 | 301 | 302 | class ClusterServicer(object): 303 | # missing associated documentation comment in .proto file 304 | pass 305 | 306 | def MemberAdd(self, request, context): 307 | """MemberAdd adds a member into the cluster. 308 | """ 309 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 310 | context.set_details('Method not implemented!') 311 | raise NotImplementedError('Method not implemented!') 312 | 313 | def MemberRemove(self, request, context): 314 | """MemberRemove removes an existing member from the cluster. 315 | """ 316 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 317 | context.set_details('Method not implemented!') 318 | raise NotImplementedError('Method not implemented!') 319 | 320 | def MemberUpdate(self, request, context): 321 | """MemberUpdate updates the member configuration. 322 | """ 323 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 324 | context.set_details('Method not implemented!') 325 | raise NotImplementedError('Method not implemented!') 326 | 327 | def MemberList(self, request, context): 328 | """MemberList lists all the members in the cluster. 329 | """ 330 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 331 | context.set_details('Method not implemented!') 332 | raise NotImplementedError('Method not implemented!') 333 | 334 | 335 | def add_ClusterServicer_to_server(servicer, server): 336 | rpc_method_handlers = { 337 | 'MemberAdd': grpc.unary_unary_rpc_method_handler( 338 | servicer.MemberAdd, 339 | request_deserializer=rpc__pb2.MemberAddRequest.FromString, 340 | response_serializer=rpc__pb2.MemberAddResponse.SerializeToString, 341 | ), 342 | 'MemberRemove': grpc.unary_unary_rpc_method_handler( 343 | servicer.MemberRemove, 344 | request_deserializer=rpc__pb2.MemberRemoveRequest.FromString, 345 | response_serializer=rpc__pb2.MemberRemoveResponse.SerializeToString, 346 | ), 347 | 'MemberUpdate': grpc.unary_unary_rpc_method_handler( 348 | servicer.MemberUpdate, 349 | request_deserializer=rpc__pb2.MemberUpdateRequest.FromString, 350 | response_serializer=rpc__pb2.MemberUpdateResponse.SerializeToString, 351 | ), 352 | 'MemberList': grpc.unary_unary_rpc_method_handler( 353 | servicer.MemberList, 354 | request_deserializer=rpc__pb2.MemberListRequest.FromString, 355 | response_serializer=rpc__pb2.MemberListResponse.SerializeToString, 356 | ), 357 | } 358 | generic_handler = grpc.method_handlers_generic_handler( 359 | 'etcdserverpb.Cluster', rpc_method_handlers) 360 | server.add_generic_rpc_handlers((generic_handler,)) 361 | 362 | 363 | class MaintenanceStub(object): 364 | # missing associated documentation comment in .proto file 365 | pass 366 | 367 | def __init__(self, channel): 368 | """Constructor. 369 | 370 | Args: 371 | channel: A grpc.Channel. 372 | """ 373 | self.Alarm = channel.unary_unary( 374 | '/etcdserverpb.Maintenance/Alarm', 375 | request_serializer=rpc__pb2.AlarmRequest.SerializeToString, 376 | response_deserializer=rpc__pb2.AlarmResponse.FromString, 377 | ) 378 | self.Status = channel.unary_unary( 379 | '/etcdserverpb.Maintenance/Status', 380 | request_serializer=rpc__pb2.StatusRequest.SerializeToString, 381 | response_deserializer=rpc__pb2.StatusResponse.FromString, 382 | ) 383 | self.Defragment = channel.unary_unary( 384 | '/etcdserverpb.Maintenance/Defragment', 385 | request_serializer=rpc__pb2.DefragmentRequest.SerializeToString, 386 | response_deserializer=rpc__pb2.DefragmentResponse.FromString, 387 | ) 388 | self.Hash = channel.unary_unary( 389 | '/etcdserverpb.Maintenance/Hash', 390 | request_serializer=rpc__pb2.HashRequest.SerializeToString, 391 | response_deserializer=rpc__pb2.HashResponse.FromString, 392 | ) 393 | self.HashKV = channel.unary_unary( 394 | '/etcdserverpb.Maintenance/HashKV', 395 | request_serializer=rpc__pb2.HashKVRequest.SerializeToString, 396 | response_deserializer=rpc__pb2.HashKVResponse.FromString, 397 | ) 398 | self.Snapshot = channel.unary_stream( 399 | '/etcdserverpb.Maintenance/Snapshot', 400 | request_serializer=rpc__pb2.SnapshotRequest.SerializeToString, 401 | response_deserializer=rpc__pb2.SnapshotResponse.FromString, 402 | ) 403 | self.MoveLeader = channel.unary_unary( 404 | '/etcdserverpb.Maintenance/MoveLeader', 405 | request_serializer=rpc__pb2.MoveLeaderRequest.SerializeToString, 406 | response_deserializer=rpc__pb2.MoveLeaderResponse.FromString, 407 | ) 408 | 409 | 410 | class MaintenanceServicer(object): 411 | # missing associated documentation comment in .proto file 412 | pass 413 | 414 | def Alarm(self, request, context): 415 | """Alarm activates, deactivates, and queries alarms regarding cluster health. 416 | """ 417 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 418 | context.set_details('Method not implemented!') 419 | raise NotImplementedError('Method not implemented!') 420 | 421 | def Status(self, request, context): 422 | """Status gets the status of the member. 423 | """ 424 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 425 | context.set_details('Method not implemented!') 426 | raise NotImplementedError('Method not implemented!') 427 | 428 | def Defragment(self, request, context): 429 | """Defragment defragments a member's backend database to recover storage space. 430 | """ 431 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 432 | context.set_details('Method not implemented!') 433 | raise NotImplementedError('Method not implemented!') 434 | 435 | def Hash(self, request, context): 436 | """Hash computes the hash of the KV's backend. 437 | This is designed for testing; do not use this in production when there 438 | are ongoing transactions. 439 | """ 440 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 441 | context.set_details('Method not implemented!') 442 | raise NotImplementedError('Method not implemented!') 443 | 444 | def HashKV(self, request, context): 445 | """HashKV computes the hash of all MVCC keys up to a given revision. 446 | """ 447 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 448 | context.set_details('Method not implemented!') 449 | raise NotImplementedError('Method not implemented!') 450 | 451 | def Snapshot(self, request, context): 452 | """Snapshot sends a snapshot of the entire backend from a member over a stream to a client. 453 | """ 454 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 455 | context.set_details('Method not implemented!') 456 | raise NotImplementedError('Method not implemented!') 457 | 458 | def MoveLeader(self, request, context): 459 | """MoveLeader requests current leader node to transfer its leadership to transferee. 460 | """ 461 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 462 | context.set_details('Method not implemented!') 463 | raise NotImplementedError('Method not implemented!') 464 | 465 | 466 | def add_MaintenanceServicer_to_server(servicer, server): 467 | rpc_method_handlers = { 468 | 'Alarm': grpc.unary_unary_rpc_method_handler( 469 | servicer.Alarm, 470 | request_deserializer=rpc__pb2.AlarmRequest.FromString, 471 | response_serializer=rpc__pb2.AlarmResponse.SerializeToString, 472 | ), 473 | 'Status': grpc.unary_unary_rpc_method_handler( 474 | servicer.Status, 475 | request_deserializer=rpc__pb2.StatusRequest.FromString, 476 | response_serializer=rpc__pb2.StatusResponse.SerializeToString, 477 | ), 478 | 'Defragment': grpc.unary_unary_rpc_method_handler( 479 | servicer.Defragment, 480 | request_deserializer=rpc__pb2.DefragmentRequest.FromString, 481 | response_serializer=rpc__pb2.DefragmentResponse.SerializeToString, 482 | ), 483 | 'Hash': grpc.unary_unary_rpc_method_handler( 484 | servicer.Hash, 485 | request_deserializer=rpc__pb2.HashRequest.FromString, 486 | response_serializer=rpc__pb2.HashResponse.SerializeToString, 487 | ), 488 | 'HashKV': grpc.unary_unary_rpc_method_handler( 489 | servicer.HashKV, 490 | request_deserializer=rpc__pb2.HashKVRequest.FromString, 491 | response_serializer=rpc__pb2.HashKVResponse.SerializeToString, 492 | ), 493 | 'Snapshot': grpc.unary_stream_rpc_method_handler( 494 | servicer.Snapshot, 495 | request_deserializer=rpc__pb2.SnapshotRequest.FromString, 496 | response_serializer=rpc__pb2.SnapshotResponse.SerializeToString, 497 | ), 498 | 'MoveLeader': grpc.unary_unary_rpc_method_handler( 499 | servicer.MoveLeader, 500 | request_deserializer=rpc__pb2.MoveLeaderRequest.FromString, 501 | response_serializer=rpc__pb2.MoveLeaderResponse.SerializeToString, 502 | ), 503 | } 504 | generic_handler = grpc.method_handlers_generic_handler( 505 | 'etcdserverpb.Maintenance', rpc_method_handlers) 506 | server.add_generic_rpc_handlers((generic_handler,)) 507 | 508 | 509 | class AuthStub(object): 510 | # missing associated documentation comment in .proto file 511 | pass 512 | 513 | def __init__(self, channel): 514 | """Constructor. 515 | 516 | Args: 517 | channel: A grpc.Channel. 518 | """ 519 | self.AuthEnable = channel.unary_unary( 520 | '/etcdserverpb.Auth/AuthEnable', 521 | request_serializer=rpc__pb2.AuthEnableRequest.SerializeToString, 522 | response_deserializer=rpc__pb2.AuthEnableResponse.FromString, 523 | ) 524 | self.AuthDisable = channel.unary_unary( 525 | '/etcdserverpb.Auth/AuthDisable', 526 | request_serializer=rpc__pb2.AuthDisableRequest.SerializeToString, 527 | response_deserializer=rpc__pb2.AuthDisableResponse.FromString, 528 | ) 529 | self.Authenticate = channel.unary_unary( 530 | '/etcdserverpb.Auth/Authenticate', 531 | request_serializer=rpc__pb2.AuthenticateRequest.SerializeToString, 532 | response_deserializer=rpc__pb2.AuthenticateResponse.FromString, 533 | ) 534 | self.UserAdd = channel.unary_unary( 535 | '/etcdserverpb.Auth/UserAdd', 536 | request_serializer=rpc__pb2.AuthUserAddRequest.SerializeToString, 537 | response_deserializer=rpc__pb2.AuthUserAddResponse.FromString, 538 | ) 539 | self.UserGet = channel.unary_unary( 540 | '/etcdserverpb.Auth/UserGet', 541 | request_serializer=rpc__pb2.AuthUserGetRequest.SerializeToString, 542 | response_deserializer=rpc__pb2.AuthUserGetResponse.FromString, 543 | ) 544 | self.UserList = channel.unary_unary( 545 | '/etcdserverpb.Auth/UserList', 546 | request_serializer=rpc__pb2.AuthUserListRequest.SerializeToString, 547 | response_deserializer=rpc__pb2.AuthUserListResponse.FromString, 548 | ) 549 | self.UserDelete = channel.unary_unary( 550 | '/etcdserverpb.Auth/UserDelete', 551 | request_serializer=rpc__pb2.AuthUserDeleteRequest.SerializeToString, 552 | response_deserializer=rpc__pb2.AuthUserDeleteResponse.FromString, 553 | ) 554 | self.UserChangePassword = channel.unary_unary( 555 | '/etcdserverpb.Auth/UserChangePassword', 556 | request_serializer=rpc__pb2.AuthUserChangePasswordRequest.SerializeToString, 557 | response_deserializer=rpc__pb2.AuthUserChangePasswordResponse.FromString, 558 | ) 559 | self.UserGrantRole = channel.unary_unary( 560 | '/etcdserverpb.Auth/UserGrantRole', 561 | request_serializer=rpc__pb2.AuthUserGrantRoleRequest.SerializeToString, 562 | response_deserializer=rpc__pb2.AuthUserGrantRoleResponse.FromString, 563 | ) 564 | self.UserRevokeRole = channel.unary_unary( 565 | '/etcdserverpb.Auth/UserRevokeRole', 566 | request_serializer=rpc__pb2.AuthUserRevokeRoleRequest.SerializeToString, 567 | response_deserializer=rpc__pb2.AuthUserRevokeRoleResponse.FromString, 568 | ) 569 | self.RoleAdd = channel.unary_unary( 570 | '/etcdserverpb.Auth/RoleAdd', 571 | request_serializer=rpc__pb2.AuthRoleAddRequest.SerializeToString, 572 | response_deserializer=rpc__pb2.AuthRoleAddResponse.FromString, 573 | ) 574 | self.RoleGet = channel.unary_unary( 575 | '/etcdserverpb.Auth/RoleGet', 576 | request_serializer=rpc__pb2.AuthRoleGetRequest.SerializeToString, 577 | response_deserializer=rpc__pb2.AuthRoleGetResponse.FromString, 578 | ) 579 | self.RoleList = channel.unary_unary( 580 | '/etcdserverpb.Auth/RoleList', 581 | request_serializer=rpc__pb2.AuthRoleListRequest.SerializeToString, 582 | response_deserializer=rpc__pb2.AuthRoleListResponse.FromString, 583 | ) 584 | self.RoleDelete = channel.unary_unary( 585 | '/etcdserverpb.Auth/RoleDelete', 586 | request_serializer=rpc__pb2.AuthRoleDeleteRequest.SerializeToString, 587 | response_deserializer=rpc__pb2.AuthRoleDeleteResponse.FromString, 588 | ) 589 | self.RoleGrantPermission = channel.unary_unary( 590 | '/etcdserverpb.Auth/RoleGrantPermission', 591 | request_serializer=rpc__pb2.AuthRoleGrantPermissionRequest.SerializeToString, 592 | response_deserializer=rpc__pb2.AuthRoleGrantPermissionResponse.FromString, 593 | ) 594 | self.RoleRevokePermission = channel.unary_unary( 595 | '/etcdserverpb.Auth/RoleRevokePermission', 596 | request_serializer=rpc__pb2.AuthRoleRevokePermissionRequest.SerializeToString, 597 | response_deserializer=rpc__pb2.AuthRoleRevokePermissionResponse.FromString, 598 | ) 599 | 600 | 601 | class AuthServicer(object): 602 | # missing associated documentation comment in .proto file 603 | pass 604 | 605 | def AuthEnable(self, request, context): 606 | """AuthEnable enables authentication. 607 | """ 608 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 609 | context.set_details('Method not implemented!') 610 | raise NotImplementedError('Method not implemented!') 611 | 612 | def AuthDisable(self, request, context): 613 | """AuthDisable disables authentication. 614 | """ 615 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 616 | context.set_details('Method not implemented!') 617 | raise NotImplementedError('Method not implemented!') 618 | 619 | def Authenticate(self, request, context): 620 | """Authenticate processes an authenticate request. 621 | """ 622 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 623 | context.set_details('Method not implemented!') 624 | raise NotImplementedError('Method not implemented!') 625 | 626 | def UserAdd(self, request, context): 627 | """UserAdd adds a new user. 628 | """ 629 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 630 | context.set_details('Method not implemented!') 631 | raise NotImplementedError('Method not implemented!') 632 | 633 | def UserGet(self, request, context): 634 | """UserGet gets detailed user information. 635 | """ 636 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 637 | context.set_details('Method not implemented!') 638 | raise NotImplementedError('Method not implemented!') 639 | 640 | def UserList(self, request, context): 641 | """UserList gets a list of all users. 642 | """ 643 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 644 | context.set_details('Method not implemented!') 645 | raise NotImplementedError('Method not implemented!') 646 | 647 | def UserDelete(self, request, context): 648 | """UserDelete deletes a specified user. 649 | """ 650 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 651 | context.set_details('Method not implemented!') 652 | raise NotImplementedError('Method not implemented!') 653 | 654 | def UserChangePassword(self, request, context): 655 | """UserChangePassword changes the password of a specified user. 656 | """ 657 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 658 | context.set_details('Method not implemented!') 659 | raise NotImplementedError('Method not implemented!') 660 | 661 | def UserGrantRole(self, request, context): 662 | """UserGrant grants a role to a specified user. 663 | """ 664 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 665 | context.set_details('Method not implemented!') 666 | raise NotImplementedError('Method not implemented!') 667 | 668 | def UserRevokeRole(self, request, context): 669 | """UserRevokeRole revokes a role of specified user. 670 | """ 671 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 672 | context.set_details('Method not implemented!') 673 | raise NotImplementedError('Method not implemented!') 674 | 675 | def RoleAdd(self, request, context): 676 | """RoleAdd adds a new role. 677 | """ 678 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 679 | context.set_details('Method not implemented!') 680 | raise NotImplementedError('Method not implemented!') 681 | 682 | def RoleGet(self, request, context): 683 | """RoleGet gets detailed role information. 684 | """ 685 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 686 | context.set_details('Method not implemented!') 687 | raise NotImplementedError('Method not implemented!') 688 | 689 | def RoleList(self, request, context): 690 | """RoleList gets lists of all roles. 691 | """ 692 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 693 | context.set_details('Method not implemented!') 694 | raise NotImplementedError('Method not implemented!') 695 | 696 | def RoleDelete(self, request, context): 697 | """RoleDelete deletes a specified role. 698 | """ 699 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 700 | context.set_details('Method not implemented!') 701 | raise NotImplementedError('Method not implemented!') 702 | 703 | def RoleGrantPermission(self, request, context): 704 | """RoleGrantPermission grants a permission of a specified key or range to a specified role. 705 | """ 706 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 707 | context.set_details('Method not implemented!') 708 | raise NotImplementedError('Method not implemented!') 709 | 710 | def RoleRevokePermission(self, request, context): 711 | """RoleRevokePermission revokes a key or range permission of a specified role. 712 | """ 713 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 714 | context.set_details('Method not implemented!') 715 | raise NotImplementedError('Method not implemented!') 716 | 717 | 718 | def add_AuthServicer_to_server(servicer, server): 719 | rpc_method_handlers = { 720 | 'AuthEnable': grpc.unary_unary_rpc_method_handler( 721 | servicer.AuthEnable, 722 | request_deserializer=rpc__pb2.AuthEnableRequest.FromString, 723 | response_serializer=rpc__pb2.AuthEnableResponse.SerializeToString, 724 | ), 725 | 'AuthDisable': grpc.unary_unary_rpc_method_handler( 726 | servicer.AuthDisable, 727 | request_deserializer=rpc__pb2.AuthDisableRequest.FromString, 728 | response_serializer=rpc__pb2.AuthDisableResponse.SerializeToString, 729 | ), 730 | 'Authenticate': grpc.unary_unary_rpc_method_handler( 731 | servicer.Authenticate, 732 | request_deserializer=rpc__pb2.AuthenticateRequest.FromString, 733 | response_serializer=rpc__pb2.AuthenticateResponse.SerializeToString, 734 | ), 735 | 'UserAdd': grpc.unary_unary_rpc_method_handler( 736 | servicer.UserAdd, 737 | request_deserializer=rpc__pb2.AuthUserAddRequest.FromString, 738 | response_serializer=rpc__pb2.AuthUserAddResponse.SerializeToString, 739 | ), 740 | 'UserGet': grpc.unary_unary_rpc_method_handler( 741 | servicer.UserGet, 742 | request_deserializer=rpc__pb2.AuthUserGetRequest.FromString, 743 | response_serializer=rpc__pb2.AuthUserGetResponse.SerializeToString, 744 | ), 745 | 'UserList': grpc.unary_unary_rpc_method_handler( 746 | servicer.UserList, 747 | request_deserializer=rpc__pb2.AuthUserListRequest.FromString, 748 | response_serializer=rpc__pb2.AuthUserListResponse.SerializeToString, 749 | ), 750 | 'UserDelete': grpc.unary_unary_rpc_method_handler( 751 | servicer.UserDelete, 752 | request_deserializer=rpc__pb2.AuthUserDeleteRequest.FromString, 753 | response_serializer=rpc__pb2.AuthUserDeleteResponse.SerializeToString, 754 | ), 755 | 'UserChangePassword': grpc.unary_unary_rpc_method_handler( 756 | servicer.UserChangePassword, 757 | request_deserializer=rpc__pb2.AuthUserChangePasswordRequest.FromString, 758 | response_serializer=rpc__pb2.AuthUserChangePasswordResponse.SerializeToString, 759 | ), 760 | 'UserGrantRole': grpc.unary_unary_rpc_method_handler( 761 | servicer.UserGrantRole, 762 | request_deserializer=rpc__pb2.AuthUserGrantRoleRequest.FromString, 763 | response_serializer=rpc__pb2.AuthUserGrantRoleResponse.SerializeToString, 764 | ), 765 | 'UserRevokeRole': grpc.unary_unary_rpc_method_handler( 766 | servicer.UserRevokeRole, 767 | request_deserializer=rpc__pb2.AuthUserRevokeRoleRequest.FromString, 768 | response_serializer=rpc__pb2.AuthUserRevokeRoleResponse.SerializeToString, 769 | ), 770 | 'RoleAdd': grpc.unary_unary_rpc_method_handler( 771 | servicer.RoleAdd, 772 | request_deserializer=rpc__pb2.AuthRoleAddRequest.FromString, 773 | response_serializer=rpc__pb2.AuthRoleAddResponse.SerializeToString, 774 | ), 775 | 'RoleGet': grpc.unary_unary_rpc_method_handler( 776 | servicer.RoleGet, 777 | request_deserializer=rpc__pb2.AuthRoleGetRequest.FromString, 778 | response_serializer=rpc__pb2.AuthRoleGetResponse.SerializeToString, 779 | ), 780 | 'RoleList': grpc.unary_unary_rpc_method_handler( 781 | servicer.RoleList, 782 | request_deserializer=rpc__pb2.AuthRoleListRequest.FromString, 783 | response_serializer=rpc__pb2.AuthRoleListResponse.SerializeToString, 784 | ), 785 | 'RoleDelete': grpc.unary_unary_rpc_method_handler( 786 | servicer.RoleDelete, 787 | request_deserializer=rpc__pb2.AuthRoleDeleteRequest.FromString, 788 | response_serializer=rpc__pb2.AuthRoleDeleteResponse.SerializeToString, 789 | ), 790 | 'RoleGrantPermission': grpc.unary_unary_rpc_method_handler( 791 | servicer.RoleGrantPermission, 792 | request_deserializer=rpc__pb2.AuthRoleGrantPermissionRequest.FromString, 793 | response_serializer=rpc__pb2.AuthRoleGrantPermissionResponse.SerializeToString, 794 | ), 795 | 'RoleRevokePermission': grpc.unary_unary_rpc_method_handler( 796 | servicer.RoleRevokePermission, 797 | request_deserializer=rpc__pb2.AuthRoleRevokePermissionRequest.FromString, 798 | response_serializer=rpc__pb2.AuthRoleRevokePermissionResponse.SerializeToString, 799 | ), 800 | } 801 | generic_handler = grpc.method_handlers_generic_handler( 802 | 'etcdserverpb.Auth', rpc_method_handlers) 803 | server.add_generic_rpc_handlers((generic_handler,)) 804 | --------------------------------------------------------------------------------