gnocchiclient-7.0.1/0000775000372000037200000000000013225150244015170 5ustar travistravis00000000000000gnocchiclient-7.0.1/.mailmap0000664000372000037200000000022613225150105016605 0ustar travistravis00000000000000Mehdi Abaakouk Mehdi Abaakouk Mehdi ABAAKOUK gord chung gnocchiclient-7.0.1/doc/0000775000372000037200000000000013225150244015735 5ustar travistravis00000000000000gnocchiclient-7.0.1/doc/source/0000775000372000037200000000000013225150244017235 5ustar travistravis00000000000000gnocchiclient-7.0.1/doc/source/index.rst0000664000372000037200000000171413225150105021075 0ustar travistravis00000000000000.. gnocchiclient documentation master file, created by sphinx-quickstart on Tue Jul 9 22:26:36 2013. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Python bindings and command line tool to the Gnocchi API ======================================================== This is a client for `Gnocchi`_. There's :doc:`a Python API ` (the :mod:`gnocchiclient` module), and a :doc:`command-line script ` (installed as :program:`gnocchi`). Each implements the entire Gnocchi API. .. seealso:: You may want to read the `Gnocchi documentation`__ to get an idea of the concepts. By understanding the concepts this library and client should make more sense. __ http://gnocchi.xyz .. _Gnocchi: http://gnocchi.xyz Contents: .. toctree:: :maxdepth: 2 installation shell api Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` gnocchiclient-7.0.1/doc/source/api.rst0000664000372000037200000000217513225150105020541 0ustar travistravis00000000000000The :mod:`gnocchiclient` Python API =================================== .. module:: gnocchiclient :synopsis: A client for the Gnocchi API. .. currentmodule:: gnocchiclient Usage ----- To use gnocchiclient in a project:: >>> from gnocchiclient import auth >>> from gnocchiclient.v1 import client >>> >>> auth_plugin = auth.GnocchiBasicPlugin(user="admin", >>> endpoint="http://localhost:8041") >>> gnocchi = client.Client(session_options={'auth': auth_plugin}) >>> gnocchi.resource.list("generic") With authentication from a keystoneauth1 plugins:: >>> from keystoneauth1 import loading >>> from oslo_config import cfg >>> from gnocchiclient import auth >>> from gnocchiclient.v1 import client >>> >>> conf = cfg.ConfigOpts() >>> ... >>> auth_plugin = loading.load_auth_from_conf_options(conf, "gnocchi_credentials") >>> gnocchi = client.Client(session_options={'auth': auth_plugin}) >>> gnocchi.resource.list("generic") Reference --------- For more information, see the reference: .. toctree:: :maxdepth: 2 api/autoindex gnocchiclient-7.0.1/doc/source/shell.rst0000664000372000037200000000440413225150105021074 0ustar travistravis00000000000000The :program:`gnocchi` shell utility ========================================= .. program:: gnocchi .. highlight:: bash The :program:`gnocchi` shell utility interacts with Gnocchi from the command line. It supports the entirety of the Gnocchi API. Authentication method +++++++++++++++++++++ You'll need to provide the authentication method and your credentials to :program:`gnocchi`. Basic authentication ~~~~~~~~~~~~~~~~~~~~ If you're using Gnocchi with basic authentication, export the following variables in your environment:: export OS_AUTH_TYPE=gnocchi-basic export GNOCCHI_USER= export GNOCCHI_ENDPOINT=http://urlofgnocchi .. note:: OS_AUTH_TYPE is used globally by all clients supporting Keystone. Provide :option:`--os-auth-plugin` gnocchi-basic to the client instead if other clients are used in session. OpenStack Keystone authentication ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you're using Gnocchi with Keystone authentication, export the following variables in your environment with the appropriate values:: export OS_AUTH_TYPE=password export OS_USERNAME=user export OS_PASSWORD=pass export OS_TENANT_NAME=myproject export OS_AUTH_URL=http://auth.example.com:5000/v2.0 The command line tool will attempt to reauthenticate using your provided credentials for every request. You can override this behavior by manually supplying an auth token using :option:`--endpoint` and :option:`--os-auth-token`. You can alternatively set these environment variables:: export GNOCCHI_ENDPOINT=http://gnocchi.example.org:8041 export OS_AUTH_PLUGIN=token export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155 For more details, check the `keystoneauth documentation`_. .. _`keystoneauth documentation`: https://docs.openstack.org/developer/keystoneauth/ Timestamps ++++++++++ By default, timestamps are displayed in local time zone. If you prefer to see timestamps dispalyed in UTC time zones, you can pass the `--utc` option to the command. The `gnocchi` command line interface parses timestamps in the `ISO8601`_ format. If no time zone is specified, timestamps are assumed to be in the local client time zone. .. _`ISO8601`: https://en.wikipedia.org/wiki/ISO_8601 Commands descriptions +++++++++++++++++++++ .. include:: gnocchi.rst gnocchiclient-7.0.1/doc/source/_static/0000775000372000037200000000000013225150244020663 5ustar travistravis00000000000000gnocchiclient-7.0.1/doc/source/_static/gnocchi-icon.ico0000664000372000037200000132323613225150105023725 0ustar travistravis00000000000000 ( pu !E$x"!!! !"!#$y"R "K#" ""#n!.!$!!#!] "[!!"#H$V! ""b (! ""R"b !"$z # 0$z!-"K# "!"i"w!$"!!U$F#u##&"~" $#;""%o ##`  #e$ !5%o"" "%*" "$%'(**++,,,,,++*)('%$#!#u!)2) #'*-/0000000000000000000000000000/-*-( +8/##(,/00000000000000000000000000000000000000/0P -96*"(-0000000000000000000000000000000000000000000000+".::1%$*/0000000000000000000000000000000000000000000000000/.M#/:;8, "*/000000000000000000000000000000000000000000000000000000.!M 0:;;4(&.0000000000000000000000000000000000000000000000000000000000/0  !0:;;9/#)/0000000000000000000000000000000000000000000000000000000000000/0P#!0:;;;7+ *000000000000000000000000000000000000000000000000000000000000000000/#I!0:;;;:3')0000000000000000000000000000000000000000000000000000000000000000000000' !0:;;;;9/$&/00000000000000000000000000000000000000000000000000000000000000000000000/)#u /:;;;;;7, !-00000000000000000000000000000000000000000000000000000000000000000000000000/-9 /:;;;;;;4)(00000000000000000000000000000000000000000000000000000000000000000000000000000/.T$-9;;;;;;:1& -000000000000000000000000000000000000000000000000000000000000000000000000000000000`,9;;;;;;;9/+/000000000000000000000000000000000000000000000000000000000000000000000000000000000//l"S*8;;;;;;;;:62000000000000000000000000000000000000000000000000000000000000000000000000000000000.y!(6;;;;;;;;;9620000000000000000000000000000000000000000000000000000000000000000000000000000000/"*/38;;;;;;;;;;:6300000000000000000000000000000000000000000000000000000000000000000000000000000.z#%.0000027:;;;;;;;;;;:63000000000000000000000000000000000000000000000000000000000000000000000000000.o $.00000000015:;;;;;;;;;;;:73000000000000000000000000000000000000000000000000000000000000000000000000/.Y ,0000000000000048;;;;;;;;;;;;:740000000000000000000000000000000000000000000000000000000000000000000000/.B#%/0000000000000000026:;;;;;;;;;;;;:7400000000000000000000000000000000000000000000000000000000000000000000/.' )000000000000000000000049;;;;;;;;;;;;;:841000000000000000000000000000000000000000000000000000000000000000000++000000000000000000000000026:;;;;;;;;;;;;;;851000000000000000000000000000000000000000000000000000000000000000/"Y,0000000000000000000000000000037:;;;;;;;;;;;;;;9520000000000000000000000000000000000000000000000000000000000000/",00000000000000000000000000000000037:;;;;;;;;;;;;;;9520000000000000000000000000000000000000000000000000000000000/-D!+000000000000000000000000000000000000048:;;;;;;;;;;;;;;96200000000000000000000000000000000000000000000000000000000/.)0000000000000000000000000000000000000000047:;;;;;;;;;;;;;;:630000000000000000000000000000000000000000000000000000000!/%00000000000000000000000000000000000000000000037:;;;;;;;;;;;;;;:730000000000000000000000000000000000000000000000000000/]$z /000000000000000000000000000000000000000000000000369;;;;;;;;;;;;;;:740000000000000000000000000000000000000000000000000/$"-0000000000000000000000000000000000000000000000000000158:;;;;;;;;;;;;;:8410000000000000000000000000000000000000000000000/ )000000000000000000000000000000000000000000000000000000000369;;;;;;;;;;;;;:84100000000000000000000000000000000000000000000->$"0000000000000000000000000000000000000000000000000000000000000147:;;;;;;;;;;;;;85100000000000000000000000000000000000000000/".00000000000000000000000000000000000000000000000000000000000000000147:;;;;;;;;;;;;952000000000000000000000000000000000000000/]"(00000000000000000000000000000000000000000000000000000000000000000000001479;;;;;;;;;;;9620000000000000000000000000000000000000#"%'$  /000000000000000000000000000000000000000000000000000000000000000000000000001368:;;;;;;;;;:630000000000000000000000000000000000.c%o#)043/+'#+000000000000000000000000000000000000000000000000000000000000000000000000000000002469:;;;;;;;:730000000000000000000000000000000/#A&-4:;951-)$ #000000000000000000000000000000000000000000000000000000000000000000000000000000000000002468:;;;;;:7400000000000000000000000000000-O #*18;;;;:62.)% -000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000013568:;;:74000000000000000000000000000" '.5:;;;;;;:62-)$%000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000245687410000000000000000000000/,##$+29;;;;;;;;;950+'".00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"")07;;;;;;;;;;;;7564200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/ &-5:;;;;;;;;;;;;;:8642000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/+*#n$+29;;;;;;;;;;;;;;;9864200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/}")59;;;;;;;;;;;;;;;;:975300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,#*001469;;;;;;;;;;;;;;;;;;97531000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-(*00000000368:;;;;;;;;;;;;;;;;;;:753100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.'")0000000000000258:;;;;;;;;;;;;;;;;;;;97520000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/+!&000000000000000000257:;;;;;;;;;;;;;;;;;;;:8630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/+ #$00000000000000000000000247:;;;;;;;;;;;;;;;;;;;;9630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/ /000000000000000000000000000147:;;;;;;;;;;;;;;;;;;;:9620000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.N$z.00000000000000000000000000000000147:;;;;;;;;;;;;;;;;;;;:741000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/( +0000000000000000000000000000000000000147:;;;;;;;;;;;;;;;;;;;85200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"&000000000000000000000000000000000000000000257:;;;;;;;;;;;;;;;;;;9620000000000000000000000000000000000000000000000000000000000000000000000000000000000000/0 "!/0000000000000000000000000000000000000000000000258:;;;;;;;;;;;;;;;;;:6300000000000000000000000000000000000000000000000000000000000000000000000000000000000/-000000000000000000000000000000000000000000000000000369;;;;;;;;;;;;;;;;;:63000000000000000000000000000000000000000000000000000000000000000000000000000000000/(0000000000000000000000000000000000000000000000000000000147:;;;;;;;;;;;;;;;;:63000000000000000000000000000000000000000000000000000000000000000000000000000000/$!00000000000000000000000000000000000000000000000000000000000036:;;;;;;;;;;;;;;;;:62000000000000000000000000000000000000000000000000000000000000000000000000000/+"-000000000000000000000000000000000000000000000000000000000000000259;;;;;;;;;;;;;;;;9620000000000000000000000000000000000000000000000000000000000000000000000000.M'0000000000000000000000000000000000000000000000000000000000000000000148:;;;;;;;;;;;;;;;9510000000000000000000000000000000000000000000000000000000000000000000000/! /0000000000000000000000000000000000000000000000000000000000000000000000036:;;;;;;;;;;;;;;:84000000000000000000000000000000000000000000000000000000000000000000000+#f+000000000000000000000000000000000000000000000000000000000000000000000000000258;;;;;;;;;;;;;;:73000000000000000000000000000000000000000000000000000000000000000000-D"#0000000000000000000000000000000000000000000000000000000000000000000000000000000147:;;;;;;;;;;;;;:620000000000000000000000000000000000000000000000000000000000000000"-00000000000000000000000000000000000000000000000000000000000000000000000000000000000369;;;;;;;;;;;;;9510000000000000000000000000000000000000000000000000000000000000 %000000000000000000000000000000000000000000000000000000000000000000000000000000000000000148:;;;;;;;;;;;:7300000000000000000000000000000000000000000000000000000000000 .0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000369;;;;;;;;;;;:6200000000000000000000000000000000000000000000000000000000/6&00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000158:;;;;;;;;;;840000000000000000000000000000000000000000000000000000000f/000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000369;;;;;;;;;:720000000000000000000000000000000000000000000000000000&0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000158:;;;;;;;;9510000000000000000000000000000000000000000000000000.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000369;;;;;;;:730000000000000000000000000000000000000000000000/%000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000158:;;;;;;95100000000000000000000000000000000000000000000.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000369;;;;;:73000000000000000000000000000000000000000000)#00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000147:;;;;951000000000000000000000000000000000000000+/+000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000369;;;:730000000000000000000000000000000000000,E 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000147:;;9510000000000000000000000000000000000.S!(00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000258;:7200000000000000000000000000000000.^"/00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000147:94000000000000000000000000000000/g##000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000025862000000000000000000000000000.j ?*000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003420000000000000000000000000.j/0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f $0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/\#*00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000P#H/0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.=!00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-"!'00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000+#;-00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/$q %(&000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/. #!,30)""0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.i!!,693,%&00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"",6;;6/(!+00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-9"!+6;;;92,%/00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/"!+5;;;;;6/(#0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 " )3:;;;;;:863100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/V"h(2:;;;;;;;97520000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/&&4:;;;;;;;;:9642000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -0269;;;;;;;;;;:86410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/R#-00000158:;;;;;;;;;;;:863100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/#3)000000000036:;;;;;;;;;;;;;:86310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 "00000000000000258:;;;;;;;;;;;;;;:864200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,.$i-00000000000000000036:;;;;;;;;;;;;;;;;:9652000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.n$0000000000000000000000258:;;;;;;;;;;;;;;;;;;:8642000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/$k-00000000000000000000000000258:;;;;;;;;;;;;;;;;;;;;:8653100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/ #0000000000000000000000000000000369;;;;;;;;;;;;;;;;;;;;;;;;:8754210000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000+&,00000000000000000000000000000000000369;;;;;;;;;;;;;;;;;;;;;;;;;;;:987543200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.T" 0000000000000000000000000000000000000000368:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:986543100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/(00000000000000000000000000000000000000000000257:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:9765431000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 (.0000000000000000000000000000000000000000000000001358:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:9865432000000000000000000000000000000000000000000000000000000000000000000000000000000000000000##00000000000000000000000000000000000000000000000000000013579:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:987643210000000000000000000000000000000000000000000000000000000000000000000000000000/")00000000000000000000000000000000000000000000000000000000000024578:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:976543210000000000000000000000000000000000000000000000000000000000000000000'/000000000000000000000000000000000000000000000000000000000000000000013456789:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:987654210000000000000000000000000000000000000000000000000000000000-9"0000000000000000000000000000000000000000000000000000000000000000000000000000001123455677899:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:98765321000000000000000000000000000000000000000000000000.Y )00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001122334455667788899:::;;;;;;;;;;;;;;;;;;;;;;;;;;;:98764321000000000000000000000000000000000000000v#3.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011112223333444445555566666666777777643210000000000000000000000000000000/#P 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"Z&00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/"[+00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/"K/00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/"4!0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 &000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/*000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000!#00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"K'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/+0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/ .0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/r#00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.X!00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.8!$00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000)#'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/)00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000!,0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/&.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/|!00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-O"400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000)! 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 (!00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000!#0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/q#}$0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,4#}%0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000#f#'+.,($ &000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/#;")18:730,($ &000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/a!(/7;;;;:730,.31000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/.!&-5:;;;;;;;:98754310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/#v$+29;;;;;;;;;;;;:87543100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/q -469:;;;;;;;;;;;;;;;:87643200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/,"/0000358:;;;;;;;;;;;;;;;;;;;:9765321000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000!0000000001468:;;;;;;;;;;;;;;;;;;;;;;:9865421000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.d!'000000000000002479:;;;;;;;;;;;;;;;;;;;;;;;;;::8764320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" ,00000000000000000002579;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:98654310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000$00000000000000000000000002579:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:98766543322100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000; "0000000000000000000000000000002468:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;::999887776665554443333222211111000000000000000000000000000000000000000000000000000000000000000000000000000000000'0000000000000000000000000000000000013579:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;::::::99999999988888888888877777777777777775432100000000000000000000000000.c#,0000000000000000000000000000000000000000024679:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:98765421000000000000000000000000000000000-""00000000000000000000000000000000000000000000000023578:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:988765432100000000000000000000000000000000000000000000!0000000000000000000000000000000000000000000000000000000235689:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;::9877654332100000000000000000000000000000000000000000000000000000000%0000000000000000000000000000000000000000000000000000000000000002345667899::;;;;;;;;;;;;;;;;;;;;;;;;;;;:::998877665544322110000000000000000000000000000000000000000000000000000000000000000000000-O!=)0000000000000000000000000000000000000000000000000000000000000000000000000000001112222222222222111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000%o-000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.7#!00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/r!$000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"'00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/!)000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000+",000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000+$#/000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.B$x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000[$N0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.n !0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/~"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/ #0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/"$00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"b%00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000%0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000. %0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.y#%00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000p"%00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000[ $0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-D$#0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000--"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000."!000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/  000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000#.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e+000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000;"b(00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000112333200000000000000000000000000000000000000. "&00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011233455677889::975420000000000000000000000000000000000000000/ #000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000112334456678899:;;;;;;;;;;;:8753100000000000000000000000000000000000000000000000"C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000112334455677889::;;;;;;;;;;;;;;;;;;;;;;:9764210000000000000000000000000000000000000000000000000000/m$z/000000000000000000000000000000000000000000000000000000000000000000000000112233445566778899::;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;98653100000000000000000000000000000000000000000000000000000000000-3".0000000000000000000000000000000001112222333444455555566666777888999::;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:98653100000000000000000000000000000000000000000000000000000000000000000/$@0001112233344455666777888999:::;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:976431000000000000000000000000000000000000000000000000000000000000000000000000/"!$%&')*+,-/012356789:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:97653200000000000000000000000000000000000000000000000000000000000000000000000000000000/q&"$&)+-02469:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:9876432000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000! "$0455677899:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:9865432100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000//0000000000001123344566778899:::;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;::9876654321000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/$r0000000000000000000000000000000000111222333444455555555555555555544433221100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000K!#000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 '000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000#l+00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/Q".0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/$00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/!00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-?"K#00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000#&0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.t#)0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-!+000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/"-00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000//&"/00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/!00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/.,!00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/)"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"R000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/$000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.i"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/:/000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/] ,000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.^#*000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002354200000000000000000000000000._'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000013579:7300000000000000000000000000000/Q#"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000013468:;;96300000000000000000000000000000000-> 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002468:;;;;;95200000000000000000000000000000000000/+"[-000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002468:;;;;;;;:84100000000000000000000000000000000000000+ #'000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012468:;;;;;;;;;;:6300000000000000000000000000000000000000000/!000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000013468:;;;;;;;;;;;;:85200000000000000000000000000000000000000000000/!6-00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000013578:;;;;;;;;;;;;;;;:73000000000000000000000000000000000000000000000000/!^&00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024579:;;;;;;;;;;;;;;;;;:852000000000000000000000000000000000000000000000000000/\$/000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000013568:;;;;;;;;;;;;;;;;;;;;;9630000000000000000000000000000000000000000000000000000000/&#'000000000000000000000000000000000000000000000000000000000000000000000000000000000000000134679:;;;;;;;;;;;;;;;;;;;;;;;97410000000000000000000000000000000000000000000000000000000000"/0000000000000000000000000000000000000000000000000000000000000000000000000000000134679:;;;;;;;;;;;;;;;;;;;;;;;;;;:74100000000000000000000000000000000000000000000000000000000000000"%000000000000000000000000000000000000000000000000000000000000000000000000235689:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:742000000000000000000000000000000000000000000000000000000000000000000["h,000000000000000000000000000000000000000000000000000000000000001235679:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;9741000000000000000000000000000000000000000000000000000000000000000000000-"D /00000000000000000000000000000000000000000000000001123456789:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:8631000000000000000000000000000000000000000000000000000000000000000000000000/ $00000000000000000000000000000000112334455677899:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;975200000000000000000000000000000000000000000000000000000000000000000000000000000/b")000001112223344455666778899::;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:8531000000000000000000000000000000000000000000000000000000000000000000000000000000000$"b"&),/0123345678::;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:9753100000000000000000000000000000000000000000000000000000000000000000000000000000000000000"!#&(*-/1469:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:865310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/<!*00112344556778899::;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;::98765320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/!*0000000000000000000001122333444555556666666666666665555443322100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-Z )000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/!)000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/h"(00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/#'00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/a#&0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/#;$0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.="0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000! 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000$"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/\!//0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/-#*000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000;'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/h"#000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/#I000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000!/0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 !U*00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/. %0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/ $F 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.".00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000(0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.#!0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000., .0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000330000000000000000000001'000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000147610000000000000000000000#000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000269:5100000000000000000000000-( *0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000047:;9500000000000000000000000000!0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000258:;;94000000000000000000000000000/b#H+000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000369;;;;830000000000000000000000000000/+ #!00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000158:;;;;:720000000000000000000000000000000#*0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000369;;;;;;:510000000000000000000000000000000/)%!/00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000158:;;;;;;;940000000000000000000000000000000000/ &00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000369;;;;;;;;:720000000000000000000000000000000000000; -000000000000000000000000000000000000000000000000000000000000000000000000000000000000000258:;;;;;;;;;:510000000000000000000000000000000000000/  /0000000000000000000000000000000000000000000000000000000000000000000000000000000000147:;;;;;;;;;;;8300000000000000000000000000000000000000000@%0000000000000000000000000000000000000000000000000000000000000000000000000000000369;;;;;;;;;;;;:61000000000000000000000000000000000000000000 !)00000000000000000000000000000000000000000000000000000000000000000000000000258:;;;;;;;;;;;;;8400000000000000000000000000000000000000000000/6",000000000000000000000000000000000000000000000000000000000000000000000258:;;;;;;;;;;;;;;951000000000000000000000000000000000000000000000/#-0000000000000000000000000000000000000000000000000000000000000000257:;;;;;;;;;;;;;;;:730000000000000000000000000000000000000000000000000 $q.00000000000000000000000000000000000000000000000000000000000258:;;;;;;;;;;;;;;;;:8400000000000000000000000000000000000000000000000000/~1 .000000000000000000000000000000000000000000000000000001468:;;;;;;;;;;;;;;;;;:841000000000000000000000000000000000000000000000000000/  ".000000000000000000000000000000000000000000000013468:;;;;;;;;;;;;;;;;;;;:841000000000000000000000000000000000000000000000000000000-J"Z-00000000000000000000000000000000000012345789:;;;;;;;;;;;;;;;;;;;;;;:73000000000000000000000000000000000000000000000000000000000/ "+000000000000000000000001123456789::;;;;;;;;;;;;;;;;;;;;;;;;;;;:85200000000000000000000000000000000000000000000000000000000000/02!(00000112234455677899:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;9752000000000000000000000000000000000000000000000000000000000000000-I$N  $'.6789::;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:9875420000000000000000000000000000000000000000000000000000000000000000000/$F! 0000000122334455566677777888888877776665544321000000000000000000000000000000000000000000000000000000000000000000000000000+ 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-"!#0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000//Q$*%0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/}!&000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/A'00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/ !'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/$!6'000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/+!&000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,)$$00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000+/$!0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-- 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000//+%0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,#")000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000."$0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/+ #/00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/$O+000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/._##000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.7#.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/+!%00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/s!.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/00 $000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000//$ ,0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/.8 ! /00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/x"'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-"#,000000000000000000000000000000000000000000000000000000000000000000000000000000000000/,E#.0000000000000000000000000000000000000000000000000000000000000000000000000000000000.i#H"/000000000000000000000000000000000000000000000000000000000000000000000000000000//x #!$0000000000000000000000000000000000000000000000000000000000000000000000000000/.0"'0000000000000000000000000000000000000000000000000000000000000000000000000//x' +!(00000000000000000000000000000000000000000000000000000000000000000000000.d #{(000000000000000000000000000000000000000000000000000000000000000000///G #'0000000000000000000000000000000000000000000000000000000000000000/+ "%00000000000000000000000000000000000000000000000000000000000//-I#!#/000000000000000000000000000000000000000000000000000000000`"# .00000000000000000000000000000000000000000000000000//0`-"Z"+0000000000000000000000000000000000000000000000//-J #v"'00000000000000000000000000000000000000000/0/b+"R#&.00000000000000000000000000000000000//.S/.'.^.//00000000000000000000000///0{.N0 + /+/G.d..///0/////00//|.d/L,."?????????????||||||||||||~????????????????????????gnocchiclient-7.0.1/doc/source/_static/gnocchi-logo.png0000664000372000037200000026523613225150105023753 0ustar travistravis00000000000000PNG  IHDR d*tEXtSoftwareAdobe ImageReadyqe<j@IDATx]V/zcCRIZ-ÖՕQs.2cNh qEmE L=1x$g(QIzKc.[4QC_B.\=цө01/Ux( e|>*귞Z_~RVRQzWcq>\k=ZVb$zޛVA*ދZ\k]1!˴yp{LZCwXh[sauB\2?m+  P9LGx<-X~qqZǐm;(B尴. .A<=wP`-)f`Yt/AnX r$B3(>Zxwpp{LZGIcWA(&1Վ?;KfZR }li[Xz*`Mɲ. &[fp9X x,X "T@1W ^Zme&.op 2vkWj7)͆fw%T@O1W w>`*S8qy-/(0cMؒ9 w:LhS|:BO*:|iIox(o J𸓎/}.??͠}t 9BV+ėj\$$'gjqyW5Z!>'T@0ZhoJt-V,=_ :b~:grZ }m ٵR<.DЍ|PfB3dC* s'jKsq l\^J,qJ#Al ]!T@S 卅fP<-zdljJܫ @Sb9B M嫡ٍx8r1OI7.* sʻ# H%X9B3L^S V K.* sʻK'U+1lxZ ǽ39kw",\.T@] Mfn\5f| ^P qo<}of|?P9#T:1P7Wr .c*=3!kǾêAߍ[E 9B+#Lq!4J;ca>d&4('T@/_ M P0Å+WqW kE{ZP9#Tpuo`k}9c^jB5?4;* sO2io P`\Ăf Af*c_54QU 2l:4o0ЋtON١|LE sqe|('jںX LyVR2}ՠ;95rصorD @݄W1H8 T\V rn6.o[yȜuh\$U p<z`ubn2:ȫTXѱcU5>Q^r֤ch*{@!nx A  eE[5)c<Rnև˜{ܫթeq!._LJ}cBdNB.gRp e{}JPR,}SBdNYBPx314ie{)L>ފLjײ9e CiCS@x\r܃L+81;oD)z\Je6.B`xܫf|L53,0o!&P~*%>M{cKO#weh?NIQ; Ci)x;W*ZҚw:2cd(A =%T")^E{O8|.<$D2>'~l/(|r:iֺs~^4i/@{*94M/zoϴ@ %gXmӺa2]͓?wnX6' *O`\emPrvUyjL5`fCBk(ZoV[Epk3)}@%K猵k+98ƍABXcGl p'<'oRگůmgwPڹf[>ZJ:m؞<4CJ*W`jΓ!sZr!qIOy>KߔN@V3֒:Íqg@K:%%s9f O!AY%+OOquVV!{z^1.k*q"W/P9XhsU w@%{yD\^ NUM!soo蛼?i]~? R<1Y=ƥpL2ҿ$TqR ˅P zP@9_gs~5Fri8;@(T>Wm1` t2x|;<lo8fpֺ*lh]S|FY~ =K2=$g5b~A9]ʁ.y-SzKm"Nlh޴taAڀt!x=C3d4$P @>9_h5]3+PTХX^e #Ƕj\ r[P3qپB! ?Z+7:h#OΗ |>:W&Wzt07g3)Ot)(7z^ȱ-ʫ;xԂ9l2͠@q %f7򯚓2@iø07,ɸ:nK9iLmB<:P,ok$rj"7f~AI8ZMc*B̄fAAb,4/ҷ;@C6͆fGL+ P @>9_җ9dhɫAso.Y>zJY[ ]gǵj\W (ٸl LB@qy1Py&9P @>9_ҳ9x\Mf|LEȩ|7.u22xTn^RpL @j@]ǚCr`49N!t2\j'K>OMӯfB\~qn6P&Y= xgJq͍RP.okKP9IqvA92*?\C/5x{^l<1mL5tf}Rʁ AX@c:.?nMtgG /Yu2C qa\N@dIBv6x (=O*ɁLvC3`9P @>9_d&9.J 2*?WGmy'q<+sP9w`0Ɂ DzXzC /Yu2jao捾a yP;#BX6Wx6PzqtZP9k@qy@thv1nj'K: Э9"e y' +Bxv6j*5_IoC9$T k k OΗw[ 0yM5Xb YPd\e^*DzquT%EǠir(W_ 9p:.?̀r %cClM5|q9^oȺ OOzHBc,2Ν^zOqc `"OΗ5Ml\Nfr.oȤ `I۶m W\ sssB8(Aވ򩮏ʡZӕh.fӺ҄ZۏL1.%Y:zZy[۱cGزeKcIA:ٷo_a4EZrzxYnD3ЗXhSWq iq `9m-OقLKݜ7|kZIQSbӦMiIA͛77rWd#4q8ZO W `5n^*B3Dy7jGdzno)P>yݍ:䓡y}\5l"-? 3ЗmNnu.M@CWx:8yF`+Jcq9~!aE/߁s@S@:%SQϗN2˧^P Gx x{)H7:NtU*l\N尼xk9\h藴mw/nhv/Usu'rI?J.J9%`~v}G*A93'\,~r97]x1\z5\|\t))vyfuZNRWIȄqn ?\P JD|*8ם@- _֮%@[: ngRkwOouTv:ӥV:fUJLoRJ8O!BZ ևM42k:4姕jȧ"/sݱv2iވ[E}27OB&#RK]a*K-7C𘓞tY}EOOOw}WȼxNӪ t&ȏwB) |֓Rw ,a5zX-rO`7o|{K|1t)/V5iϓFPV.TAfPzP @>|I 3q^5m TΟn?ok[ xqFx<4* ǭSq5iٵkWa~v̙3SjL/TN̵ LP$.?;ׄZ)Kw,4O؂ Mf<*o蟌kAhSxq1P9%=(09@Ƿ(Y"Oy;_ʕU[X7BΟe #Tjx ;Ӛ*_;rxJ'U"MM{it&/Թرc92SS+ Sqګ ˉ \NSΗĹd\˘-tAKT*aR / *g ǫKyn]tx?a6>ҿ4n@&4x,8SJ]4Sq2{ͩ*`mڴek (~(֓G* ) }ȑ"Pv醴cǎCgL$%TN0yz,ȘATh˧̅a<.~锹<.n:sLcMw'B|81ԄR&`RI[wjw-߿bdòoh*'wRՑ-u'=0I24>VM`s)]`V`L%tN}5j.?psm9(TN)L~P5Ǡ*.z4 'uUUȁмI< ^Tjɓa۶mg;vgΜQr>I\qI#T>t,J$='ԡ8<ꨮYlW?'m+{A(T@q9U wt\ @n͟(Vn'P_`9}S{ 3ȃD\Uηu˙#W@9PSzSM)( %=KnjjJ!cAy&r՝T|3-c*@QaXyu2Or)u,R 9%C ]P9}ѝ|B5( GTXbMS3 7PJ[)PCږg_B\54&@\1F0ODpc.9'{)|g(ԭ\9xruԝ)sZsj(>҅7r9%:N])cǎM6)DoՖP9=N~ҍUM z7:.m9/BDؓ2px< /P^U eiܘk@^sYuISrmǎaϞ= ;K}P^Bj} Y(r4 GjJo߾eVx<, e7k?dsJSSSP"6m{BtE3W)P^U Xt,ϛf]Vo5P 2ݷ~]K(uߵkB=7&)SmmLE`UA+TN:d{L:A[JOyqA'77a˧zo] ү+ͷn~ޯ4O9'X@/)Awˊ`3g;q*/֣_].رcsii۸qcx';鮺tT;wnEvŻrJ|q8O ČRUe`RXpwZڡ xgdcq(v,;wvȼ8O8׹_F~= yꈜ%=I i277@t[cWT\U#_ AxG s]_7s+3V;'Cn Czpyyȩx ,W '$)X:5210xn=WRUv'o}042tpy)Ǜ^:O?"@Fg@9ˑ/rصkW#XZƔ)\ɜbl޸PJ:N(Sy?;( O=oPdz.>S .I7ە<*r&u޳gO#LMIK Րd<Ȏ;±c;#P\ejjѵ<ȥP¹qs|ںV+Jݰ1]Zgj!z6=u `}]EЩ<#j\U KP9@йs!U%D OF@_{ed2TaT™_NѧcY ot*πVl\Tlw( qFM(n5P6P>42oPl2!u*[O aǓQ0xiP Xg|˖- dN7:tJkWJ0~?xBG.@/}+JXDǾTo)xl: Pŋ Aar oRԱܾ+ƕϫ#S\ɬ=Vgjպb1 >9w"P}*]z}=9%ɸ:o^O/=۟@h^^ɼԩȿSQt\ޛVShS'j׎;±c0B.Ϥ1% [«_/M NxNO ^5ҥK!T>wUʩ&OKz:ug@z97?GFo!ȟԵKx2'˗/+Bg"B=:p09P&)\ 7lN}X%i? {?+J ?䚮F(x9E`M\kwAZ&U\6m؛•eٹ\| cqUdz寿Aإ<&Z}yUmҥK1+f$^8 Ξ={!})@.q% sjw24K9BUu-ϩXVUºk#Z1; ^09-;v4Bjt*n y~V_@9E:0Wr~f`mY+7&tPy:МRrH#Gcǎ I,C J 1Uat*g5._XX?(/ԝ<S*% ʋća[+Cá20<ΆHSZuL0+FM*_V'I(={'ON0`sjį۸jrvG}=`yF2ެ0+F?5h]شiS8vX8tPmLL^'s鱸:s>hmhDcjo=zI<2?(ڥڧ#e;fWuJCV gΜ ;vP tIB{TͥU#ߞ2hC#!T*6,Ӟ?7dN:>V\QЄ'TzDL~2ɓt4.Ueȷԝwvݬ0(1Pgͥ/a|fJb@gNGӷ.AyEl!TLD~SU|j@y ]=qA  غV̵L}ODh_\b\+ʎ3nS,:Fov8vBpC]|շ;<;wmoٲE1rbzzZzceBƻ ?L#l|`miqT%լv)׽z"TS n吟v{xܹB':ktx|=s= (8wwƂ-̺ߜ'>9& ;5cE{5*7ôya愺ӦKyOu*did3jZjt* ?yE辟+Aaƥwa?jPԼl$j&sg.ݿ;is~8^z % Z|t+os4/xǜ:=W]Ɯ}yP}q/Nvh[Ep1Oo/i-tsAp<:gh{ӾZ?= wQ.ӭɓEq]| kWH]:vڥ9OC99;qdf54OwxmLsAA5 B̵F(,?w1~)X>@CwǬc[ xe~|: 999w_Exyb~x<ts7iT8ԔlIR#BK;n)Pv۶mS ΨͩztWG`Nǂ,_~ uvPKB GVJG r͛K7ԭV=FMjZ7{T[2ۚ< (t|AZ>aэ?Kt6n~}.b}gk]:T}ҶOrnzNr#ɏ9l٢y@ϕ`Zh^\;T>Dמ EmLA@Pt):`_W¥_ŀV˟ק\WV`霗AsXZp3W w:3}9@czh6 u/}IVdΝ;="'Agغuk_%|W <Ϥt)JX/-м4^S2upb]#΍G.=}}!CobcBa9~>A||Sư/n~B/­e"4_P&v̳XRPsR+ujt<(Bܳ/*j9s믇znܸFGGCLY&TÝυ;ǡHja頹i{;WUXӏt)n89¾ 9 Wn{9Ɇ 6pGsrq /. бNk]z_b,|c,|AnVhw޳gb@ҥK #K $T~gq0R wj#P7,.ȑ3g4$N`V wEIXJ{8퇏Ds}oXg۞ajo=Oο b5](X3o^ Bd_\4yk>>Dt_>zm!s w:O(`'6lwˢRÇ76mRnC)Dox 7鬖}m7P#\dw(T3y C_;6+o~-Q,/HC(И2kA\/'!NfVྸh_ Mi76P.ӘAo\t)9r$LMM)F@ySK}xJ{Gvmܸ1_UVSRq kΜ9. iqpDюiyJn.͟ot*ϋ[6 zJkq٫ x,YmA^t>Ks%^\hO{}n Ci;q,<]w&ﭔ;ٶm[سgbHYܹs [mXYPyaJa֭H#`.\==q"CcZh^ ֺf@;M;g)P> ̟^\s@lz47.`P}@u~ '",Z:7w*fO7i߹/66>y!g?ɯ=[l ZM1 eN<7}?({r2lz|͛7í[]S|rQDt݅A?[kϵ6^x4wQ;E}DpPG@yߞ/#TP =>2El{">z:CMo)97 /i99k;OP 'cr@͏~n<{SSSرctc[?6TOoB]TY?TǷ]ԛBP.2,Al' 9t[5b>Zys_ gpхмǀy9{knn.۷Oܵ}g(\ZW1/jk8"xzx8t:{ Ɔ㭱az v>2ң˳/M=A(jk9qPy_X?a:ǃ@=Y+\@o՜9sF!oo(Xxh5i}.1/(Gcqȟ/b|3 +-gKc~chyK5yy{MJ877ѧsg=&mi>.`^}xpS:Nlp8}t,ă*TA c^2@hvqWja&qҸ|?Z t!1s,*?0y ]caS]כ uywFw'?VCwe ?\k}g(}V_ x,) Kyi b@h/TN1̈́;ץf24KŪ?ٖQ9rD~h +e>̜ʓtō7P#\ta} Z? lD1M2XC{+<wuC /`/TfmFu+ ޅ?kjX =;pW|h-[sie||24*li씮kc4aF?4MI7:t(ڵKA2$ʧb0~O(M<$Ҥ5-Y wZXXׯ_ot,O"w*'XTЅȾ4/K᧙nн<+A~ld=³^i(=ԪXWrHc\Jc6I`y /_rS82"m*g :3g[]5Nf͛3(_Hc.X`V)gu.=WT"v?s-? 7 3 {$nhv+z:uޢ2 _p6'_}g(Oű^e` 0TJg&4;&cɌr l?<tcϸ~v>wBP6'€n㾳ރ6mRHTǝs,]>c^ّof.ɭ[|#X .dhvJ'N(.h똑?9^i.l̍B\K|}|-v+zN[S)X>ս 2njB5x*uהf|\EthO{FOײe24ϥo.uvolHݱ/^x㍰m6K.5/_Vr> z=dG [n~GFGGrϝ,l\z*4/la~z P#l^i[FV͐y0Ϫ_ GFs>=1o&UxtT^k}qM5,ҩ<\pse *u*OԹ;yd8rBd`_q^'!T~>^ِ@ߕuօ!!݆‰0GY+hcѹu xq.|l6.micȼ6hBVFB[rV~Gۍ wnU5'[qL<.[1FLjajj2}Ǚx#ۦNö:UH$ݐqƲ`<"q9vg[!#]SlںV@9=q;a? #Z|- GahFw,Ju֕Go6Fu፽ΛV+hHVDWcj`r4.KsU5븤ǃ@9@VhO/|W!X#ܣڦƓj)'TZ^(7<<FGG ^~p%`.fJѳt5&v~psNE#P/á:uzF)?C 1 ? J8ΙsKەɠ.3A`\׫ѩ)>/í;#ል7 væGաc֣)p>oou/*]2'x@Mv{9֭BF)D>NS9Xvsy?>m;v(r&Ӎ\PRN&}֧Wک\عsgIY֭͛[^@#An:ْ')l|x#Eە0ćo> c?=<>X-A~z:Qcݙ< ^c~~uKY'}@u̫[yF]tqMZ/ݔjt!uʶ7V *O'*G:fB ,u >(~O <i(\t-4Cdlt(b%FF膍6t0Dz20Gí.O!z 9W]k5FS݄Ɂ,Ϗm/&(]PlK]v)Eui="frzPNZg غukxW%oܸ!\.x.)NJxWG@ _ u~̆.{BCb|ta Op1`!,u損;ZUϪɁ w6 /yWޛ p̙F!Tan̮/URTۍu7m޼9?~QPyV` FrN(4L 8Xr>NXOTqcC#66+s [?啴n:^ g~YQqQ{ywz/T5裱|2v8ƚjiyOdV3}+4>0ϯNKX1`:R'-}rBt*;ቸ:u0Xzxb >1sY;1(| #3SK][7}@~ԃnԭ`<-[=h~ҥpk!{c-9N`*۷ @^ˍ( ϖb(OR|ht `PB 75B^f.2V>03q^OeSq.0PRxl~8Sg6NeS9[%ƤٔL y w퇙|;u |XS$ם;& ͛÷mv򑑑"\˗ JȓɸL:UZ+K|dr"i\͡sPyw+{9[yהbcl3@UCkyx־xfe>^Kqs]ZP pp6n|ƚ޺uV~zcYDț4<O Ւ_,p ?j0a?u- zHwϷˣaav]c:w{̹9غɌou ;:/ONt y< @ǽi}N ̧kV7PykK8p lݺU!$o޼)\dޙ3gfg'cRk>=ish.RʓG ^AZ2l$>Û5Cc7YpʣaitIn[9sc2|?>p?[Z$?:0פv|S՚ ]RS@޽; 1@\|Y!Ȼy"VПM<;?Q̵RãP\ O* ah05Gż0O08V o xpor"1`Y>lg.|?3dXh>I1mu>,*|7Cfr*UmRwW_}U!LȢ'O*E$^:x~H_?k%v'ydN.S4oX{J{0o%\k P[UIǧ8Ntc @[+0ک`lܸ1|W .dzzZ(ɸ,YYk[J*CC6<7#O|؝.Sh ?P>\nTrVuNko8 ͎5X4=`y:.)HU c4vM7V`V$wWZy ƷF.-"˩t!]؜war*Cad6<wya {jNXtI&?]m]#^ɾx"4qIN(L1P~_0mW^y%l߾]!2N(Er`h>~o}WmIAoO4DRb͡0/|^F7d=l{iDZ¸2y{6. =c,\8uwbUjuM@ЍޭyO(VR~_4ou)7o S@?R@ nky5lW6[? ~z~ mxXl.Z~ة|Ñ.4#~}z4' zwLEzxzF\S"P3qy_87VB@y o@/R@IU26W0+1T^&C#xFO^ot1~.ż0Ol4/,g+ut'7蟱\udإ|_8O=zsvo߮"\t˗ùs oouϪ6Ul|xJs>b UF]8v1W1d橋yz͡0/|^07f^$/sU6 db.~՝}1%SEm렿n^}U((r[BHֳ9uOWm۸=ƺL*Caxz/X$@yb.OV%,̮ l +L|˧BĀr[ՂYU5ew]^:词7o~ QZ9sFɌ}__|ðu˭#]?v.b^0t&_h_b<|=ZS7ܳAG\`pBd'ץ|0^yFrʣ.O򑑑0<<* إK\Nwx?]M6m۶Ϟ|vasw)ktL8}ۛifg?{=~Z~4oԥzl$v1k޴Rp_F;-|8Xkzh%]`|7gfqWk@6LotH~pQla QRpyZR|ttTΝ;XPիWv0x-ezzz_kǎ-[K:.I|520V{9zk'/}Pʟ}x/Աѵ|F oR<%ʇ>~3wM6g~Ԁ_){xj*Nd{r$\~x Ce U)dO6iyv@yvl'l̵37J !/Ȋz#4卐FT>X>v#ԩ|˧*omKCDZ[Co*5l<.}34pH*Yx gSp<-n@mo/O?]asKrXq\ zL{g'_|Ci cƗ[ؽ|2 ׶ɿ`Лl{4@'Ns3bYTKyK9,G;\BsyQ)Ȝ:u 9p;n[lQ*LJ{Q{ R VCS9#0fK;. fع|a.>xgF(E8T\69#nWe2TV'SVbaa!\~0vH9u!_V&_۰-u4o:-tǙ3g~`\l =&wv?s-l̍C FvFj o]Է|*>=6۠/q2.'Ɵ/Oy'57{Ͳک\>zg V;\:եpp.} qR9m훂O?5f^NƸRFN_5*¨o,YUۮ[yFs'qѸ:h9v0.Sk" R_;yRPÍ72.OAv6Z -fjm6ZvXX ֣J;A#X^V042Eb\2`h yPPyO8C7uGFF tvNA>tj~iӦF<:ϽΜ9cq9M7fxK]rv/PyjҶ۷^xA!.yfuV_ArݔYk'ۡԹ|׮]N溘!T]埋׺4 :nAk0< Z.Je:?DQ(xZ@:M#u'hӒ壣= wu$.]9x{˖-)d^˗u` ǐ.| \ ?1_ШP9__CFsy[7ޥ0D<ZмPd/[]ؖꫯ*_婃NѩKrBe _t?@l+?`y+0u\o'+7T Ү/ÓOþ xn㔙X_1f[Ν;I7n>^0Y®]o-PNfjvzz0?_jh˗5WI7U2@m{"c__U;o0rdщݬ\ Pcq<2o$~iL:,~'za_[~@t`.K ޅ?kjc{w_,~:ɑv 1ͻAŬƒy{: oi:vSs cPx>+ضs?γs3J^=PQf3߆\!_W撏e%tqǤ3w牱B\$_gc;OS[ʗ>%Xޙ_Vq>8/P]١|LIɢ4Nt-{=?omӺ~h' )= ~)zͣ}o狾 1ojr?oVWmI]Sg?YC9/ 9s&LMM)Ag<]hkw϶j@m|oV!HB8*a`y X('kڡv<=ΰXHS Һ0$;v<=e"߄# ~ލB˷iO+ mzW^Q7 ?Or(M&ShرcK˧ `cڏ3AM'ڝzɼ@v=a C#zJ=4C"er0o5?_<7oӒ<&h ϗt{}qh{_(F[7y U2Ogԥ| ]h}kWb@Atv.Ow4j5EehI':_u^H= ۓ~V|Ѱ+Pg~Y Su+1TwIAӼ~ ŝ; 7uCNvcGer|D{l P*oS侄Sg:eFz5R ҝ@':CS}h?f}}IȆ? uXdx#at @P8wQzh6^JB0KBtJvQ7s!rK}s T^qC X47(LʏդmJc^:|7TE$Wc0H]D^:Nu˭pX#*P(®qH!z+B|@9,|cr5\?}OQO<*aݘ|2-MFB¿]zCrg?YַVdV:.`'yi.jCG>P%EuL%wBއ}@9U;PsYFXts_,dZtӹٜ*/=|7{p { Oqۥ7v^}UgȓOSۺSD(@wxj5XMaxz g.ǡFrzb6^ă>An^oǎV}÷`#d^I}.!T^ k)D޿c,l, [+ :`N CG? :;t^HI r[R )PHC}V#c14^+ V@_wQv ]]twӾ9(=Ϝ9) T Kzک<7vo*=o @N7on SDZt߾OpxU}g(=V kjBXvxQ,rq(۳g 9GbVO8t=aaXw0[l\f+TPѸ:h߆\!:r ]hw jXvmOr]`n=2/V|zuq5<|A;H<yC5A{?,pH^gmʁ~*/v6.YLSqۤt)WrwQ p Sx@O0}AG>/X̴шJ7㗾ېg5U:g:"|ceʁn{XzA<ٸdͲ*'qu ]G{B5o ya<&T`B@)G7!d\W$pɺO&08rҾ6]ۄ˳KlE~#lH]鶟gII:u蠃 dO#]LKyDPf[T>C_3ԭ[1P>y4?ѕ2H:X52PAzo`Es>ץ򡗝S1%a Aפ@-`EtLr(t:uH'tlWt*BJ' X$KHԙN\ݕ<5 2K}qTZ$bI7xZ"D:tJ!0Py9/Q,HK :}qj¡@o#i{1\}Qi{c݊@W 7n P.'A;/ Cs )0BߕPcbtKw%uO˥K]t=I4.^'6oy+^o6[:A] iR#.Q\sحV -) YR{cZRVZJa-`eS[I"yRKׯS#rE˩6nܨK9]o_W PB@7/DPHh"\t[#pw{ɠB-#8:\^g.3+,y{8=Rm(OR .09w,-)\s.t&|7}KMk7UG/B}wna~ P`B@/}J;\~&~uyDjq;oPߪ]-/'WR<-zz  j1Tn+'Cp')TΕn_+\]rSOM՘vΝ;5Kɿ)@ I@?=SSSNGsR>* n[_*7G|cw e,Z|B+\WFc}ޮ7)T~*o3/&jSrapR4&rwX']4]v;RWիk_BD IsHNV Jpw!OةzJaΰ5A5ƒ^ FN|Ο͠yexr`]onp?-CPMCЇh|K_G}tBI^PF#X@y ` du4==9K\7E_i=|;=֭M%օ/,JKu^_*.I͇`y0oG EWB=BP:[zP ٦c1o1`_ kRgΜiV[kJ}wVVEc,hw9L'҉<]駭[n5,mBex?X=!tK54mn*OAvWĪS6f7uAw?"97@~;wN2$t-Xn]-Rv?իW] kRH%u+׵~/}0<. ߠ˴TH}EE!F . Սߊ5S΀yeB9<̶'?ߪ־ 4|d,и#ГnINa.5:JעR`y*ꩧ⪪ݵsNE`R< :ʁ\ST*atw}̝]A3`~=Yf mjSl:B~ Cٳ'۷O1.۲eKQZ~mxnt*PڸqcxU/~X R3g4:DGB1P>v>bv˗/+}t#T2v.VN@^n\Gi<~0aRwa[ãP\(N 0|3h^9|yG /hN)m{"YM=ٿxu';{"T)ni@~'0 ?mָaKU2v.V `9Mz;C3k"P|؝ѥri+na? Ï!7Z}C~1@9 c;v4n(byw ,͗=E5}.۹s"b?я•+WHS#G \K9ʃߊf] w:kb|#O~mmOpjA5S<ݼ  cNqnI,/]9~nٯ!TSOUU g 7nTV7M~ή]“O>8'XRWnhx$ [ J;~Í%4F*(B:B1 2C (5P<P9tX9sFAR[K g}VXo}[@a;D.I|umPAΠy=Q\o+ܛx:)F|íV|Qm<yyѸ;9"d\LeCD(yN'u(9|c"۲eKC^ܞC @ Uu)'Z6 C0Zy0}!d3RQhǓq Ku ;|%ГSN`9PF ?t*+,J'l~m((~BzEiu7'ljB JbuC.ż?|J !F(/Aضm[x7kb,F ХHa*۷o_ؼyy%u˭ps JC#2<y7ѳcw>fg[N\d'?0XBPnِnI#M>@\|P@SO=U1뎍7 "?t)t"ʕ+:AظA0rt)ީ.0jpvb{[ߪ!v%A ؿFwˁXMuϳ>,o~]`t!7]T?sbЁ6:zC##͎ws.C{Z`Yg6P9gϞp!{wa =i?%XjB)[RJ.'ˋegn\S5 B@V<,l~c(Jc]^tߎ_+u0C3ĞWov2OYs+ AǻB @c¶m|nn.9rD1Bҩ|6n(Tβ.?OC`s k4k^۞нJ]qgŦs$}1Kq= ("$u)߸qBPKP9y37rC!Bhzaʆ*H_b\ݱ}vE`Y~)t#G–-["önKyU*jPٕc7'?( GsC͆07M @9ӊQi W+ W;yEΟ?~_)tA:N]]442@5:ohS|xFмn28"\N !Ps}1cٲBO=TMGrg?"@уd ^WH]+j,a @9\!PĻᆱ,ێ;ȥv**UwRr\z5?U={Zg7tN]J2v@9sssҥK ۷xȥʟSХ(I!txFrDcQr`OX)O`9z;:P9Vt/#Rԩ' VNX)A(-[4:CCO=X\+m޼9lݺU!xχ+W(Ў;ž={" E"Сa 1==]"P0ZTVS 7өLݡK9LRw%zFrrYC ;n*\|ZMyPyMCzjO^:ǀʟs,ho]h~Rrd4(V=tCn8ȃ?L!TP;v  @nN^+KJr71%i?i}1ƤDxROg`Sz;4/} рD=S $78[yi; v8%vC42hK$QH?Ht[08lR.gUr"%^V'X)_Uk}weVB_1 ###]>}˾oCD7‰s'N"ȸ{ Rq?Al-Ȳ/i)^A@x?+@_xF>`9|ח 2N 2333 F^M_1*^Μ9c#˺1M(O(75"ȸ;{rrdսB'=ʹgP,Ow `Eo~S6;N@GūĢ#4Txcllt5{CUC/@䩢8qD../#gy& dݚʿbJr^]5 VN8FAƭ'>| po~L0T㏟0 ###A@i+v*O|E[y'$5I: @?VdfM_236駟VމD4jk 舉 C'br ;h||ؔP9䇶rQ@F~/Ç1q=@?m@l*f4s793KI Fx N8}Y:Jt|rASʁ^TA###A@h+oACzjqH#m,'≅A%xwN#ȂTB_1Tf rF[yk*,MZ(ʇ (A5="QrKƲ{ bybTk*o'ӱ! 7k9襍ʿd,9w!@=3fѴPyG5uCȐ(?myqa`@LLL}Y']133\bd#W6 XvoddtXXX0ȡ(#}f?0jk1O5w]"t+F5J^)Jк%7Tf r,ȣ=4wVV59z}.c7OU|-| ڢhlyۨ  :dG^Mz!tH@<,;zhxg A;}tr`-zaPƲ;CCCTgG^#z: sN gUl{W *A0yb@̤dq<6 XvGK9 9wrvjdΡ'%Cx@7 2!]駟6n ?_2*g3ӆ}g16TYF2V5AWMv76Fk1xe5nYT%#ٽC`CSSS}B;D+i+rL:yW 躉 C)^)"^1Z ;q!]#TٌrB=JSB|q@T~쎖r6BL^Czu2ꄎь տ ʕ+WA Nab$366flHSܑw@;7@CZۭQ3[XM}yI=}+}#~/[Qn`}/ϕkwTMd趣G] zf\gb\1??dzfbbȕxBSN*756455eЧ⎼^{ v|jmh4B^r9F^37LʡQf o%<7sCi@S4|\i5?hC>Gīfչ''> x6@?ȕ'NqM_11Py{c) זnTә{}|x^Wk8=~άkRj5`^w3_jjM_HBF*v+b@O=/z"=::]m褊ؘ!p9C>fG^{,,¹:dmVW B1]+oyf#xfS Ϗ{=WngZ#zסN|~B]wѣA!χ+Wl G=?Eo|#}@l̄m=+[:acQ@Oŝir 6:ౕϗ/_NFră'vsʷTOv4>S%F!(@ zXl| byZϛtȱÏ$af qGy| 5ߋ$4.Cǎ}~QO(z1>>\gum^ cpW?oqm͓_|BP95ot׹qhm]o{OrLcȪx, 4r6355eEj4Z.D뎷xyaa[!.q<<a,#=g*a{+wëZ~m`_눪9} |3|XO ?k=o " mCNZ%{^5~}1xlB̋~2{ȳ>Ӹ暼YFVnlﵟÕ {z<1kV_5Oi#׫JȊ۽:3Rf Rx?ؚ;Z/Fvh=~_G<w +vϙ*OK=}\s[*Gz~f< `歶szhR/̡ӯվozCp3دևn[a~86߻1tB6y[kq3Dޏﵹ6ozcV׸Y<1yRfOka\酧~:ktDkKƱ;L3Z-ݢրzwEETgs^#/6Btyqt=>vm< o@yJ˟sA9nCfyzl|r tH|9QbK˘e~ qjm5*'=o~ӓ\9\amg{kj~^6E9yntmLMM]Q-bqC);gS/G;&HB'Py{iFU ʍھ~ zòd:H±-ZඟߛmM~o6k:}> #'cw4C6j1o }3$ܡּ,ywW{ß0_P1׷az)4V7p0zc./j嵰.gǦM{^{%!R3|X Kʯ\^v7 Ɍ{OxZO.2/_|LkoZL{׸֙f5dݭuWͫf9N9ISyi*g#ӆ@5[̫jy3\oS4Qb7̋&yX ioxؒaI^Kz.$ï7Cۯv}ݭ90^Ѕ卞ӥJr@Ζ\Q8G͡ݚ tGVCE 2u8+W92wePM"gkZGœNRǢNHC?F{Ȣfj*om12(btKܑw9I;ؑO%RƮZrPmiC[ 5^ξm^|.T6n9f+ "8XЫwH\ɨև/ cd˟s爛lh Pes-/E ^yt{gNi6(vodd‚!iFV7tg9o+DP|⿣qkm1o>۽kk WsY1\g/*a╗[3T%؝1C*om12ٙG;_B7'-W*kfϡMʻ oVOCҝ_[TOC&K< $=[ARy GanK3͹q=9|ޢRN7}y?я0Pd<-qnOVnwekg5y*zvcQ&T~Cj-q~Һ3W^ZζvOv;j 퍂חLQz@n Zo͛_wA3.Õ+W 4sm7Q N'@"[6>qk㾷ȪxM\(EGlP9 #hMldaakz=ݢZ2bN;⁊x*Ԋ'"8u]㏮D+@!_(~$ݨ4#^٢{2ܠ q?k<n~9$7bWsRN`[ Ok̛pku7Ehre*F.^hpS^Oր9yl;˹4Fv ʠ!@ 1}&29͸S +Zs&^puNHNq k@0[Ø95=i^A7gq&T@Ojt=Z]탬&^?_Yfy o5#q흈l]bt|<;E5.) ڬ+Zo5=R( p خxyp٭܈ ago};D`#ʹ^7R676!jVVVRX^^^ kc3G1MaAtLM;[I~ vg1v/5a~% j؍*Mϼkm䮯E:dnnMbX u"MrZٍw}7 1:iv.^1vyf+pX`9q, h?~1@tH .M\Ȝ^;b#vm?RT'7r'; 1?`=؉Rl;aáChMl*1!HZ펐yZ5xˁ/6ⲃC?l+e#fDx잖rv*5UڿNe#ER6茩)Chc<̅n9rZ>5Ïx#3V<3@9=+tAv… abb H+ze#-dv-T@z %6:`VE 1 C{=쳆@C6y׼%G@;]Ӑy<~&b DG)R7y3*Wz ,s `K~2rh HXBm044dajj0yl.r!s%/ATv-y6*|: [7A@+F1P߻)M@;ؘ!9w"ˉb詰U+=T @>0RV(ΊwX>1TCȜ4DE7<`4w߇};~ :+F^{ "hMsss6 kZh44A܉711aCcK|N8}{ 1t3>nE3d/B 풃cF  zq`-倵މ-@or.E;P9@\pACSMK9ޑV_| #L[ވW΍ǣ(u݈/o!65Q\Ek8e:V$G 1@tV '@6bHoV%Cŝ&@?QPSSS[A011aEh;T <@[ie3^ZLo9!7%'W<)2`Fq'b;6o~iC@oi1$TFbx= Xmtt*";ȑ#㏮x1xr^ /z(t@9'Ėr ;*:C1`-zdd#ʕ+bz(lq,vJ|  6h~iC61L Kb0E}p r,=j А!pC`SvÇ>?*WQl}C.(g~dcQť riC`S.;X\Eؑh7AFӌ bzW^ 2{rb>B=`g^qeGjiԪ@ 2J3.X齸_K)dK[9SB=`Ghr5@^Ń|Rkiƅ.(gRdVP9@ؑW\ǎ&T #G{bz(^-bff Ο?o)r߮\bGW h+AFZ?4CJA@NPN+#G#B=!=ڷmP9111Or-<> *;'CF4jUC b\^{HŖS-?E*;_w\ΪFC|:q!z\Xl)ˆ H\+W 0vh._-?v')AFxÇ= qx ;b.~k-6-`-Z t֕+W TSSSc[y hn0kkq!0p'$sqņrAF[xwC׌kllAs(zjwjrFC\!9bbi+nb1@{ J<:o5B7qIK9| 2k1ʋ vU{AfؼU\Jb_,B0` t߉>J{k?LTY&bz… fP333}Wz@\1`WT1d.hN4=<4XmY絖ϓ%r纝z΃F( n~Ҟu}這&Zvq k1=P9 ,h 77<afSxon'FcҨmAR%y~O RSY=|'@Q`Z tp7BcccMOO}kkςtR}uf? 0_02?I\,+6ϜF?C}-'jk^G?/t|]oغ .dC fff (˗/B]BPQ|}y?nzf0?9?{U Jȱ]|u@hυύHiȼoj J>Rm1vr #dđ#G C@6`PaHCq:/RyLZ<?gIS9=4@\50zk>kAomDB~Q~1O:!@Fh0,.Wb'fbaw&֌i`[kqhqk67o4_ χq!ttkAJ&J7o5kk}Ay)6l?/ >kE[3C]'FGG= JX76U1݋Ǐ)r$tC o4ơk5TN'j$mv%ǰx}< k,Xn zl=_:/ o GkǡC< JVl*,C 3]hkZR򁁁[sAΙJ]1$mB[֝ş[;跂Rc-|TÇ #XP<ѣ= 2evv,meNZ7{~%Tz- ÝE xl _m~Nkf༖6Cӿ{(?<#:=puvڄ]nx艧ʋ>9Y\vr%tpQ#=6266flVh[[/i+ρV>go&@~Nmt+wK{Y RQ-T^L333X&I C n?*sPy xhWԭHZ6[[F>]m-1XϋufbRbdPy8{@lռ_~<jox˞s%@~OiP<[oɁz*zOS9P%cg#cccBڨ<~u>KOU&lux0l瓶#!\(PK;n1?:L]pɾow\x?n0;;kϡT>0kŶxcifxrơF*VNT` {T=APXoACȸZu{j)4BڞP;Pz0{C}~o1j(. ?*7B}+3@1PbW433 2رcP@/_6P1ccc7]Va3qi}|9i_Zk"_;ZƁ9FxO`- PF; h쫯. c- M}MkmO[ ㏮F5dRBcmV*Ir,'-5nc|r~r!p!fOS9f( 2Tۗl|o!Գ^ld+y-@;=${` ȿ5BAbHcjjr53pz@8yaJ" v_OMlnny:i+V.X>_paatAl _k"LoCo~{;1ߡrG;.*o9C`C,,«?\c耵"ߛc3yG{{NC#Iov\Ȗ#GB]|033 bܡl1;;klhllؖiCo]|J٥$0^/ʧ1?(?n_XJ#ӭyPo%~r:v?anik1b d؈r0rkaak]MآRr|o=kMԨ,,re0 \ 7Ȇy qzU#LzŋMMMw2u|.,, m&+χa ǥա-chȑ#\^s!T:k||։F&x{֦?_3G4/dal]pZuj=By8vgzz!=S m1C 18^^? b|{ah(/= oiJ`31X~_6}Zdz4ި<2l8gOGȞÇni qxNg]=ΜW)<4@;0:FSyi*g3###p @U&K` EViq쎦r6`kbP3Xa3 fff  Fr%mUqB;{1Cp MKA. X|/d;a,;7==mlHS9I(rQҀ;HWsll`lj||6$ȇ#X{AUհRX\\L?[zfb[*G#ٝ)C`S630xVK+++슦Vf4lLKykT~Q$@;vr _{ٍʺ/MOO ! i)9O`6CxQsM׌db6244FGG Lh1 ѕ+W : 6[[m<~=n ;`aaTl+5 hkGi+WKlomoඦ/^3ݛ26C|?nY`&@nz 6jJk5 6l'f M |&>!; 굪!fCoP#hYC`S###att q!dIl( FFk؎oݚEWkh,755elJ[9@{]1 V>o^3(^BZ ayy9,--6~Z}`9qܜ!)r-YpF8PLCq(48l]e]2ݛ56%T//B45Cȉf@18~5`,755eljdd$PXO$Q@N;] #T~e :~!}O<\/Ά`SFROBZjX]] ˎ#TAʹ'xB;T C5fY]S9dFѸ-0RX\\J('0T~ =redd$PO|Y+F5r3ȳmk``[Qx30lo 77,@?Ta1T`ՓO>i@{쐠r^ԫZyC@[f𝶋7ʻB[9266FGG > E\b344d`dž ȕ1 7vq4T~kF{SSS=?~6@r"oё#G Z MX<.^{F{ي'Oз]5W=fY7 ȷ=~1펦rbdd$9 Kj)ϛF^Ȱ#yWLry3%@_WS%#ڽAa'4/> T7褙C(GXV5rYV{?0x {zꩧ K#s^3;;kbzośA,oś-]C/^dD155eP8~A}gM%yhaCQ- c=rJ[9YQ saCQ[ _2݋3)6W]1T@Ȑ٘czzؒ'|9r:E9b!Vf4wԔ!%O=! 5h!XȨ{/^8aL199ilH8~AsP9@V]}~~X@bzСC[}j% -9y!s+WB 2B\|pPyMMM[2>>*>ok1ٵP?U{?!sC `uk1٥˄َ'|2 =Ui+\btaC0ahz~kVjK/^2 sss#T#||| ؒSO 5(wՕ0 Ց1[o~'w'b͐>aNm/{k!ңB[wQC m~.x@xlgܖCdlʅm@?S O|y r^] =Aۥ a_,([=畵5CnT`F!Dksyrgff<zdttB,TqeC(xr@;^ntrN_{ Fann. Vi+r>c~P_MD WKQ[ , '7%Cʘf J1Vm000*\OqBtk9bXX!Xv3ȈC(Ç)L[{LNN`[bk{ C}urd}xtK,8^-Vv4,~Y`<Óxrڭx`#6S1Xʇ ȕ_1>h4`̹>?ڗ6l/'vGxY83?ϚʋK[9XT><XO?,y;XK3J&s_CTJ,,zO> Q@_җ+#ʳܹs =pT(4jh$Z)mO[ȓ[- _(_sGCkmk aĕu.^A-(m N@Pyw"f|N'OIu,]"`N^Dakŵx /0mSL`-Z @vwg'}ٍ>  0>Q_]!{K _{Cm@ζWˍ$ihf@V`< ?X]3Iغ LOO{P JXȎGks;/A0=S 0>H"o"O>'_kTK}wwo'A֖荵7B-44f86W}z=vqC,O*/0::jh*ϊxI9mFIȋB)T^OB~mO_K͖n?|$L2*ꡛ._Vfgg=f6dܜ!|v*xP9ck9@֝9?,, 4굤|_3D>?T?LB |kچMkMo2^,ǦrOX1'wfs̙v*^7 b<i+oC$D}zg!r>x9im< qqȯ, kPyi* Ѱ4Vn0z3<ëkqwW ۓO>i`-Ǟx C(]/^Ars'6'/yoACYn+oC=mI+yz%4 gۛ[㥃UPp}d,0::j`-4Pn1av8y?@Ŷro$'F'ܖϵ5qO@X5C]G&.?Av:i+Dh$Yi#Oȓ&H^JcC9@;sKyZLo*k1@q+T7F^Ν A[Py d٩Dmu%Ԗw56lț%BeF<|C@ܓO>i`-Ǟx C(vQW ONNm}/mȪT4>t=4jj)C}z`z.) / ﯅PixpmW`X=|_ >)S[B/^\2:s!6qO d_&.?\dK<^'m"&Pii|8 ?,@dڅGɱ6ɓ=$TNSO@Ze555/Av3>h46 x㽡4LcSZ#I`Uȴ׸_b;~\ci瑺g>}h]ȲT,.^ Ph_^=ۭ֓4X$(18>@ /%tR0WC.1POX顱 bi[<,F^gΜ A[Ŷ3 NO~ rTJ<kI3yl(o#i/OZ?Y0;n4 _6 XN4ʡ:$$ W| yHWxi<~^:XME7w':~x50'pS +(r cmgw YvW{˳$O1H^NiKIxio \BZL ożriz P ?Pߙ3g41O>dx _ g3.+% ? Wli $mix"O1dbCyl*/"M4}ZLo98*xϒkF~O6:&^2 N}]p+H~}4 ,<K$A} Br#~j%Tv66VCin[P@y$H+aF[###!DCόov &Xk{gc y}ϧkCP;ߓPז>}Al{( 3Ҥ|S1[XXgϞ5:J: oB^P\F`!\\3JrbHŋ?Kno[@ Y'X3i< Wy ?w V[ȷ`K- #s@+w8t`9u[sG|$]7|U߄g^xZLŶrrP_ogh+[,Vn!ߊr{T 35HVǏazɓahh TB/^Hn>0;p ,N|]i g-1@W5{ YOC.X(rqg'?1& ,OR;IHch [uYC`ˡ-ѫP;FY\[9Y/A$ i75ȨTި غz)4B㽡6{ ˍP%K~P_@=[%v Cū&vCOB/^ w֫jɓ'OS;;<ʁKȓP@%h_-/,Ak}Q m^qWu%/bSyܙ]t2&TWMȯW64D &Rxp9TFC9 YP9;\5P<@F,T,CYredd$?H7r-3BkШik,mkm$XPNc#荵Vroxܹs?~<,Xbz']= =!(?}A;1P.\uCmP@Iy}~0>ڷF}յ֥դFJ^ۿ' ]Z|؆U:٩عT>77gHzo$uda謷zNrKحC%Ch,l#Ocy8~-7CkVB4 ;he7b1Om?%eJsyS[Y4ȺFcy"m nFT.rr?gͼoG4n1Ȩ^Wi'99966ܹsJhx/L^?O>dzA% DV/yH:뭷JwFyC?ͥ;1x` (Co󦶲*dLl#o,&7ҏ,﯅]\j;rifkdzX$8Cq1P~ҴU||v,Ms3?zsɌA9Cߋ.9u!N2lj^&%Kh$e^>w{Zd%n1X1+eڿL'4pqC[sYNkpl'\@'56Y*F(Wvd{y'پl{X:+A[6{C$[edWme1 ke夅|1ٖFޤ:Nϭmnxlm^yu+bNsYnj\WS?78ifb<1/հ+P _x?ʿa_=}]"iI;==\w<ŏȎzP tRl#OJr51@^A$Pl%C؅ONRbqI3\Od<$_/kP9C?ҟ ; _*~ B] xn(fy/w<;w.r=BNUo=ٮț!!9hUb;Ǐ B[şO"-/ʞIV<}mM|5_[beqoso 66p# b8Wό%/eɲ&ē'@+JNiόׇǷZpؿJƿדmSK? 3FX]_A>;Fޔ'Zɳd#3h*ms2bOo[ sְx3[3^!?')k Gpy\c0mI%yVd:T'K;$O:P{ e ߺFJ复K{go͠~nm]ٵ5ۭnl~5 ͐bvpl4ơkpkQ\ׯyCQbخJm]599Ξ={ێJȣ OToh+'N7yl$/%rvr:D[9Q5\lm 9f%6~,SpU[9(Z\k[+MdqQ[]e>T7^M7YH1HH<_]WIC^_A| :E[9E G[ eollҷ^$^qy k+?[5N@zrVhv@?VeK/~ȯ BS[] A{Ixz%> KP%T. ؅%Cf[9@c{Z ٗz/p<[X7@Ox=6-H޴<;00n5kkr  ###_}U`-M\[yKi ̠!T^[Y6d+zOCIIwțb CKkI;yl)?\ꤧnr~vpY `-+V1PG3CCi[yCK3헶o?+i#)\N1P ͠[Μ9y 3:@*T~oaeCCz-TW U[] Apz) דxi`.`=m"$䱙4ǦriBavO”ؒ g###: ;rU.C{+@~..r j)C}kA~{TijYLqxd%MryAF >r<|s.ԯ 5P]Y4>Ixڞ:{ 86L>^NZVN[ʡIb<l||<Ϧ@6Tܼ!"`y0=K7BV5{ȓ0yl'^*B$Hr(yĔZ,LWȫ߇xѕ#k1dK9w횇wN:e;7=S auCWRh\GB|NI<c<~w3CSyV꫆H+GbȘ\oa o[wQ[y߫WWCmy Dl 䱉:4'䱡JP%TFo& -(ώXh||/+GbȖr܇{*6 |[22m׃PQDNxڞP!$L?^-H>ë!Tپw.T%!wnn #Zrgފ-ve hyȋj)4WB}67ޖx .WmdG믻*?###}s_\9@vCSy |3gΤ.E DԫhXF4Wc#{ӆPN<0yl'$]gdxW >/y8{/9{AIV^QD&R>_=jF]Kum5#~!TUNJ[dlloK\Z `-PyV>LxH{/22/BA4( Ixz%ԓZDc3yFtۏ@y 3y422W'[<}@h+ψ^z)LOO2=S 0'M山k,P@%z,oT$WbK?*Ϻtȋ~lǍWDb葾 'm$7^lvַ%X@Lj!HuFWW P4 f_^=ӯgI ?'HNoŖyBl+X}x"ٮyh{/yW Sr!Ot v! 'G֚ȓF'iCy֤A$<^IB1L^OR/Œ@ [ O022b 'mג?fCl*dL%n W b;и^=D~mOh,&?;dKkaςI;}ʼnfr_rx WCT$7l, kNoS[] Ey㽡6{ T|y(7B`5 <*HoKdo[o%B?] @i+ϐf`B.]ADl`K<6 y9i& 1`Yj)ϫbf򠟃bl,@m((`9!(4L?rȺ K_h)ϳ?;6dH_߿3g΄I4`-.)n]0gG#X@&ޣr!P# \h^CZCK6~*ɏNŒ@}ԫ^!GB$@>?ԯm<JuOB^OMGŒ@[r\ٳpZ TPyVAr;{ӓ Vo|j澯!@u!rȢS9 ӟӧO9NJO@g<_In.yȳG^q*V^\F#ܘ!OoՃmB1(^>X . !r 땷Kesw|jj L)ַ<@!ϦfN -ʵ[^ +?^< ǰ!OpyfI VIy9 `y4<(~QJ7ᥗ^ sssdx޿%c K PPyV~) {6`vzA[9Zsy<^"O[ȓBi< [I1dE䱥c~l Kfvd: P.}n}ϦHnVNKwJ7f|"_4@_Yk!Z yyl!N~SN###ϱ|  pZr>mD}1(18[y('F5{ϩRJkI#yl&O[ȁ(grrRK.Em}7ٳg=k1Ic|" v`s=@7@;l3XXOoՃ6?ԯ Ł<[! GoBrrbK0#E2'bhrwO욝M[r_] H+? Z# x !$L^d0 gI Ǧȡ|O۝:uJ蹑xck1GaCI[9Olkn_Z[yj{Byu( ,~>}!fB$D>W3+X n5m4@c\oF/K+PΝVvx c7- ߷Jk4X@B]7_٘\ fkp\0\wcȾx7x 7v(,, F9By$DPCy{Ti \^ 7+g4Xw Œzahh(VPyV~-yS!N>utȹR4^N@6'%!}bx|xOcx|F&>pZ#yM^1,}C,k3g\˿?g*#ھP^/ C9 `yLfx|x5_Y ?|c-<յq 3f~+o7_NΎ4[rm||nk1lPMIIC1կ~5N9BV5Iu$DP6/wÆIx< x(4<́l{6i'rFGG `9`-*sv!;Ϟ=ktDl*+i,JJ('Iѵ&򤑼$DJhTB}'Zx7 C.M\*@^C]622bbI[&qS[oet_&.J}7[Z|PN$X^7 ixv࿇PځI~-@_2KqCXG "IK KvX2-P^z 1T>7[$I6*Wo. K_] 'ߦFFP^my1C@C=~mOq^Ć~aˁn5 b=l׌!_&''s=vM山M@<^Ic yl$_+U?Ru< 7[ǫC~:$8j$ʢBiB10lKՃ~W}<7qI,3Mb7 $mגo3;;?{aV'skەj[ZGVPIKO:ImIp|mzK> _=۟~^d0ݚ0R<|#\yυ,r .k1ܝP&`Dr]ȟSԩSv v~|_݀AEBc y WIn?x= $4ip< ]/KiZxѾ406'[aR,$ϩ+W64rM" \2|;{077gb)[|Bnȓ$P[c|kI}-8>px==ipʒry5<2iK|`4 ߿JRN7`W݆ҍkCt ###@n ۿOkƐOq׿099iL%Åz%m!//=BﵴHﲫ\-8Cip< xOY  2!m^ k{Ҷnkjx$4$DoAb@'h:Wp{H?Hn3;^zRvv g~/<3m!_Zxm~kgׂCoTns^]M7覴}|~0?޻ o'ǯ_o '䱝<$-Gˡۚr(@ 3n]3X$Z kʷ , ȷx);;hf(޾?<-7Y yƓOCɖ4 7V$v:$8cxy=z Zxp0AV7-NٳȈ!l`9`-oQ,I[!sυӧOvJ.ZȓB>T]kOZӶemzz-TURh,&-ٸ|}soN-jaMf~iG^O:x B;5$Z @ o$5cȿ>3 vla~ n kB'KiP|x=i$OƓrĖ}/fjŏ 6F(' -H^.ͿRNv"Z!ȸvOy뭷 صqC ʷ!i+{SSS_z4 v,_zsmVOӰxW 7h!On_oT; fP^ yr(VP5W_}I>X+mJ͟D;bvcz6g=XNgVWBj 'P--J$H>.HmiJةC؅3gΤ'8vX(H$GlW,'Z]<)"^ P"OB䫝 {+H>F +W|3Zɺb|ٳg ؑqCxI>"*߹1`9z-ToD7'ד5_J4<~[<w7xck4pԩꫯ_ l!A$ӧO`-w(i+ւL4[ `'b~b.Z M /=6?T?n>nm<'bP.%%7fijj*m-뭷 m3oUOʫs'iOCI`.[A CKt*HXK99~]Ki VB@y ={00}I `_$7D}kij׃P`եɶbF~-i#=?_ .6ZRxZɿӧO{NS.pO=!t@<ԩS>c͌! W'_25;;ĝDsssOn l|Z Δ6yq3Hc;?(4rc`3cccAI> k1DMk$5oq'׿ewC(FVDMȻTIë<@KUsv7xCS.)AkR9fXBm$X^q'}A}- f*/R-D7Kq ozFZ#a`4 ߿"HE3 ӿ~Sw1. cky<*o$X~)y$!6&B`G*'BhԪ!x}kA$P^O1`mAktk] 7́KP~j@+]~ߗ'r倵<*$X X^(SSS鎢W_}55?0vu$BڞXɓx% 0 yM\*r`=aZX3I$7D9s&|_Owi x ,, 5f'M嫋Ki< גy A,$OCʼnY_yKh-ZBh- B`yl+IKQwFӧOI[!uئ;I;yڛpi- <*'A/, gԋˡQCixk_ZzcZ y Ty1X~'xtgٳg ~-Tgˋl CKtY$Ϫw^ .4Q[J[s7MW_}$Z 'TaI[`yaŝEN.(cfuq>ԓrn y/' `g\'ϸ=~P!Kqj[ _%Yɓb;+o N@׿4h4CȰ5X X { Y'#cn|qGK/qtif 4s,o,{ PKn] y$!&/'Hޏo-3@c"g||rYBKU$k1Bz#ߦA=Sw2P(ܹsIb)i+]1 6 0go@7fF7ѥP_ I8*~\Jʁ6VIM@c7K?䓆}`llr$|[Jq-Dk1tPy%HCMK-0:yd8~})(!؆gS?rmuq> Be\|1D^_vTi={ӍaPB XΰR_v? w o 9@µé'7Lmܿ67:ȉ|1xw% MA/CpE&%\$T^~ӍSӍ}y.^N ُ¼x.>S(t/vʟ9o/S*g8[NX8v",=1DDy#o. ɏ;f\}{&+\C4;R=* p.Q*/r"}9E!|a?g[a3v._<~rb]˷υ닱3J؉\ C1S=g^{qjPɑ'D ͧI 9W^ѥR*/rsA߽i`ity /J^MS$R}`zkC=PVB%XΨa/_S#ax\`Dj?*pÃG-ǻ!cK ok&8Vc:u)Ouvc Y>M@f'0^fnIv~'9``92:,O$cN1T~p lmn<._<ۃ÷[w8Aؕ<_7ӊ|bg+\^[JT1F'pS1SwjK~_0 Lӽ80k NqHEW^y;̡yG:{dn[&x.S sG‘c6VH:o=_HWQIa?3iq&֬;;#~m5ֱ勦~wMu.$M?Aro}(OO5  9U T^΀yz^*+[n>L+?9.Tmmm{wc.c|nhغu(Wv|"O0'H@+ {s.:':6{ȃXy5Wr؛ 9T*ϟ${9 ^PB%!X$dw+C JP.] 9{w>§>d0[Cظ"bn 9"P^88>.Pձ%ƞkF'B-SWxI`:Cy8jKDIVH<( ONo~H6ݏч7鄬Cyk͙8^ aP&'TI /b&i;1_|yz$?k2;9$a$=?d?[oѧ%;^¹^mX $+H΄N'?Pj؝XNnVJ̙Uy7S̙Ey7#FMMbQ~%n u.Sx l. [7V|jpd?rd3vt1//5yմ2jyܼ,ȴSeɚy=@EKjt{lR)~7afQ*Z公$Oq7:Irٚ>r0ym3c9P_/):˙yz9Iؿy:7o?Г7t&T^U(cD RWPN_SOnq0⍰x;lEc|>ˏ.5u'_o9(¥i#a[xy0߸F[#p!q,vOw u_o5<;kB*qZKf3w(ɷu;/( N#GT‹W[ o(z~}!n^[Fb=ր'T?Lu8ۿۺ@]}{1w>wdо1<0܃RxppyCu8_Lq!Z͈ϙqsL@}ޛ7%UCʷb7-SoW~-%౞NMF*A\,_׾x3<g<Ƿn#׻r&#մn<' \կEu8].廉u+qbO͈ϙ;.M^1P'&_az$Pp'lux_(Tܸw'ܿ}=Ã1ԝ?gY '{97V]?ԩj{q47gmv&= =[a+u%)lg[.u'\wKYbz%nx.օҋfą+vG"y @qna$KPB}ݬ=;_Dek& 7f ǔaOyؼ67\vf,o~ Ojohx/?[iq&8T5:q<_*gNMXo,X40}ă𝯿NWy(c|nGwC0O͍7 o'gxӠUo>֬W&~ ,9)υPLY g &O]nJNwDx?>23|ipKqj ?@E'XA;}LS% ;^ҍj~[1H+9p? ӓڻ&cvϙLWCV^6k;(T>Py./͋fSx Zt&hɘmu阊GWS,*Ot-bI]˿śq kDx''L,P2fܼGl3ݚ3xO T#S<́|"|NtTx`4<^UC,o `~C;6ٛI@Fx>˟3G,|/v'ɨ=o;Ptw rt:/BH,OSs}$|=Q-xiؿXnJl%#B =6!+7 ((v槟gB$R3շMFtB}c*&֪!U/ R3xO1Ty)Z_ǀgO} zbvR:q7 Zb\ܼԪrylD; &W(y^~ԓ&b^LGLF7 VMjKf(v)O *ł}3d%{+SCOŎ槞܌:0>W>~ arK&y0V}!d]l7<;T*y(ש2*̓4K7L R 7v& (S/֩J(Nc=X*Ob+Wh9|4|NӯǙ*թSsf(V|ao *r<͐˗1((w+30ؙڻ&~8N O^N7PX+~:*g4;\,گ,\^ś8q^ɉn\]tB֡e*׵\:LSBf(T(@~~Zv/B|+?; ^Obuꥐu-_2zBfy*!Zl/@@7A *+Sgo/ 9obu qBtM`r:q<kƝ Bf(!Z޴7|R]Ysf`*:q'H0{Z!RBfX<υ`TJa=OȺs_i2("ώ7(/X^ Ykzw.^YI f8^֊LXoS7ˡ }&OAk&X7Θ=Y3u.o _SycݽB`k!O@Q={MrFk#GJ-}v=kfRwz]=~~ZZ~{&_-]JGõwM {[,rs |fO*g4;]|JQIj}jˡ.sqic.3<{x wM Y2tzȺ't.n͕Y0U `9 'Bf ZW85P*.\qPy}n8 kz$Oɡ_A\>y>_N\읋]l8P}.X L:@Dta͐WH!5P )7jP>)\}7<6ȩ.*R~)\35R,`|P9B]$6慐-EuZZ-`&x0{i{yB@9X 5p9P󇹉>sq3@ť^49`5^ 3.T~Ѕb=nCV}x wɶPR` F@ɭw9/ Y@5u@\k5dO/*g4;]|Foՠ{9@ѥY4T&q0͖c;[c:_5LJ\)hxN=aR`gk 3KuO9ˁY% r.nO"i>ݞ:Bo//*g4;]|\ Ǖ˛f:!Q8z_KaeS *! .~>{vTN`Ιj!u2ς^ςs_Ìzo}TY#f(N$C>kUIYe ٽ( {m<ҧ=N*t/vnRb̧ab.c0)#DcF(NӧlKO(-0r=7@Z/7C9 ҝ1Jܼ*g݅U(П µwM O+ѥ0kj\2B֕AC]/Sz5@] l%ThvPyqf+-R1 ]C)?w`v1d`:9_ @ttd,:.h`{ZȞ f:t.N0$}Bҽ]P)5jM*ThvPyQ+a;`PBzB|dxW\E1XOx*-ٍ0P:\C"9x4PFurнZ?cxdō5=HǮCΗ( P{4Bf }aAz(@1f >5^tÞ"p%vn\2 ˁX :s݋Z/|:׻r0[ҵ0'= 3.T^r0ʭݻi(h1 p Eg1@ AA߷K^z8}jT0v%3@ECaϿ{VCv?=P9Be^d6vanVjGCCT\:/-c^Ƨާ؅}2}HJCAb-\7̴N߹8PUq|;֋A붋EA 3.T>+ʹBPPqzoۚi:禂^nZq\JX_c@E/K/ڝ9A;HfRz?K)f߹nF`&{jߓ5:*ұ{O@Y/qBf swq/`.䭊wŽO3VCV^zxnŏ3 IWXQp49\+lwlR9y82D{c'˦ ٽ˳T>JZGp?ECtY_p{yYqA Yչf eZ:~Jrn-*>\s"s(zF6%3y.N`s(sq{:Wz%SK,f*v=}Ǻ,|-O).OOBf Wm.,C2o1Za yKך}oafâ^:)SBZ?^-Oф"H[! ܹ8fS 3SFMc{ү4%=.*SuRho}E;lUP9BU^x7fЄj/v|ݔ|>E:SQ[ntry RɏMrb-puXc50k?uv?[{5]Bu$w}uSJhz]oDtr߸. 3[wra*<[ YaBmJ~//Kf Xw?r#X~\Lt= >p!r~ {.nS֎sq`Rx^Ԓ]GEuL؏fx^k-ƭCMz*Fe5*g4;]߼}MPŐyϹE~~﵋&rq259֛[c=m/g ~`k&KGׁIQa;dZkq7£]%P.q^$cuz{q_j~WٯL 3.TͰ)GN(ޢ>Ve1Tssn~1E<Yk-ruTR7nq&X^{s0uZ3l̛fa /[O-8PLʫӅʡ+aC$0:ν? $Pf8ΪZ;foה֔c?׷{_5AEֹ>L m{G:vv`k7|mʫӅzj-w4{z( !L'l;D+qb&JRН(r)l׉}׍PzvNVZX8Ĺ8?R£U x.nʟ@ $>PhJܼ{CLAwr`֢K;֡.a0zaϭVJk$ܽL!Gߏs?\>ȟ@ $B <..!p` prRz\clJ+d[aoP9Lr.TP*1\7/`:q\a Կ`8B0P9@`y#n^clLzȺpH f`bqa0r'M`R\ 8.@y07 G`z1`7_ G+1Df:A#T0Px SPݍ|=;|Կ`8BON 7vaF[A|fp*0o,\BM3uB"id6p*0,XvɬhmJA#T0Py7vȼaViF t!6oP9B2y9 *@bмKA72, 2017-04-20T14:04:89 Pixelmator 3.6 1 5 2 72 72 257 1 348 0-IDATx} tdWyf-^dIU՛=dH Irqd``& &ɤ=[RKjڴTU31`lc/n"{TZ^>ݭzwp\{Dy"磿&#'E>Yq>?ӄE2!E?C>,?MF,GD>x~88N"w :' ?""!ѯ\Z,x7pp,:tb*tgya7~oS$q0^,D1㶿r&R*Bkuen(/!6(}x7qpXJ*u1%bmP[%Kxqp? }P!B/s ? [DjJ8&+TJ+շ~Hz>l'q)@x~?Z٧.k +d%5 })Q\VL o~ vBxDq:|wq'KX?lVd?VxxZq\)Y`M$Sc 9V@yxYxqڎͻ pU$㴑|5`g[yH&ZZApFB d4tGTHB@:GZPq<8;L5+myFAwf^w.b L5©ז -6!MNxq47 lX]܅D<~J}F +d> ^v!\[X0/xhß0EАnD /eJ(7 X0K܀~H !xL,! BEAtGX)%^vOyͶ  2( ch" ֨$3:a`UD Xyk`:0u{% NOYN>n &¿J5lfc"'d Ka9,GZ8* cbmX% iX$ĉ0>fRǔkח߇_| ו^'@gU.9𫉘v$0%V0sDq2X?!gDy(ar 赎f1a!AbBT>v9 sn{;ܖX<7!%rS7clH#cdHZٚӳBxDG;+܋܈*!1܇xxLaqR 1Z;GJw\?b%[{ZnQfE4uCZSF'i)$x}G9O[e!R(Ȳ"An 1@~V vv ~,O OYE~ @Cԅa섉V6 df ʊtkrj1ɀ&.'̲t 9e`yT4+Z%&9Ѯ(p ^!"b13}K`REb|QhR#1%qioN 5Q[/#ތ(2jL݅ ݅\tL! l3@ \ +([qs]ccS`k' GJ1ERvaxg.b֚bŢf]i0,Q0k)uѡR\H1Z`3ʀ&* 6Y ;ᨺ|N0m %Z6:iVB+1BtM,[/@X%.QXJЊLN!z l :K]s`KEbg"<@6oN@aN7R r XF,@)Q+.9ɚ bnR[8(i6 b[fm 1@y OfNe򱏂廊a ?{`n_8A~ƹ@ s _3`p- ZHk 6a `mSQf~ Hr!Ebas o:EP  sb2rc:I2 ؀g-tNJyqw3w; :<\ y:0%$,&ztx}k"5p=ha2,j@2u M1ƒ8 o+ ~8@0{TP#D/_'yL8zpOa/ 'p0K8=@T">%4Eh̉}?h?^ 9x0:=!c\&Ha`\Ks\HI#ŝR*,Ŧ:p(EFC\bբb*&gAKP@DotGp*BDAT˥bH\L89aId-@dBC.|VZߓ@u˃ӅXqpWb-uƐ9y]q-C )U&FmtA8P 78|v- a)ra@ W!Z_u^ 0݂2Y-VX#zz!)< `:1 X!3 Vr^Art3,T+N qv t˔V)I NFJxת{\l=J2eU 2HC䣟 4R-`' ע%q\h%IBq7^r-)C&;H+aP+qιB~l_-@:xCqwh^tr!<{E]hXvr+ƆdqnC6g-s$%L)H!; Z}E7 跐Ջ(($fⰨ5dɹnet>; ?6j 6o+\\Yn@07Y8.<8F(>x3=ON]xHA&KJW\Tstnt ܂{xY c7wrr+4%k`Edz{N8!š qpE.ȴ~DwUʈQctϿ^L\F`rk,@w+F\ W.cH'n49}+l5;d$d f qT Z \*%P!V"}O3a3q}6aXe&hCɌD 9 %%d_ ZtbWTk-j#e4l(L^s-"ձX o6Y)[,VR2qB,2܋knk0VLv# IٰL]#1PN[,ѭqYA\1UP$ງ|lULF.kz#1,h'zة&˄;iE&a0u[g =u]?k&8` lpc߿``F {mS $Ǝ@UpqapڅBAPL>` r v2X)HZw" ؇76㆝|Q%""P@@m@3|^ bP#ujP@1HET!V5:E%E A?3 JHwK_`0LV{Tc;T RͧLuRFvE`-ۏ7 ,k&Tե^}Ϭe L,x`bd SYP5KtU%zCSY&J:"JB4z:E/Q]!h٪ś ^Xh>YGVy-}KVpIf&ܢ9X-Wojyz{D_}攅8ͯ~UV'hqkUsϒ) LW7`5;/IAIy56du2qB#v tQ9㚐"bgYV\^%ycTDXH `IZբ&W4Mqdn(d[Y\p  d`$jnʒC,@p%{*(pRaSۈ) WFG5VaL;߂ ؄[&-Y IDBk"T:.`^NsoD:9U=FO:@^RS?.Md zFY@* Ox~evGtMkdՁ<N)çvw |ϡī @IÜ$8Z0K@"f֘5L".PLJ7Lzox >%Y<@C"pQW*vּ\/P %|^3ӷFt8`^7PsIqc|,i$RH6D|l!^Yr RZn]Sz6`h"Z [ wTX&Dq(j?nzp\%!.ב~G&؀(#ҟl 覧͌#JI6,"uŕ㢓M(;(g4||YKGcb8/3 A K&2)8UsH@>H}/DM( 2>gk0mdkr2o <"R|HC$ߵV"?fO9z F w>¡;5-ŤB9,r(c& fR0{@w4-TP@TH؃ @ xN BG$9&x_C&EapSyZJG򨶞1#~L1ʘe ՙ*L2p\dykq#הف^5X]ĸ1[H]։iRFB&A\LsO ʆ5ЅTa5\ 6]![\}Tr7偡z DyϹ@,N1?ei=fr^ؐ2%B?24 `% }ZXk4V'S]Dy 9|{/- N ^PP!<`끵UX};g^A7MaSjS3k,^(oy2enApw^DEI{ +'Ɉ>H&fDr}z7XCk"P1@h9}7 \[\O%׽R5ʈ,tL@yDu O*`{x?yЮB[y&.}sNэe0ف҃۰e7hħ\6̉)fπ@+@fc۞na}vv|u ,G^LAҷOy!_+jO{-kn,mշփu <`O<-8y{B-LRɟi6}WeS؎lz_ V4k`K& tSǺD9tmJWt¹x(fSEWUȽ*w." ~V)!D(O|#R~<P("[>N1s5D>Q9VAQJ0Y <)g`S`_:q5H9AI`[ae45H9׈Ϸ<; Y"ոC-,ԉ_dŸ@[m0Xrս78}\ךQ}e+3=4#𸘌\ꔓSVHX9uȢ9`r@c>H]q% Qnudcǃiȿ^ҡ`1l ƺ66cq"@MESo0K3QrIiSFݶ迳>( |/7IiԩF"o!3"o0:L|,~\ _=rtHVq!Wp[ηK|f oOɰǖbÉHs_ڞ6yEOm_uO9$o;9Dť\op@roEX^\c؀"|'e~{J='Xowkj}W$>&K huHlnM^moRAfm((WEmɇ߂+}" ի"7wk/}7 63k aa)A߾ OHA~'g<5E%5z5|7Z|v/$bjz,' G^exmcѦJ|)c4aSN!E@v*V[:(%_[6[ }{I8ěr> W|N:"K!=d3N;;{TijSP1iTs'*EDu~d]zr"V9ANnG$&s omF`U]B{ fccZf^s򡦚k֛W|^l͂ss9f$Ne@wbM7Dy>7r*8 o]qZHMs7/wy[XsW65'V/c&se}gsQ&Q 6n;^k==hNM r؂f="nD~ύG}(xdYiC%LEaLcE4^AK|h۽K.Q x(3CzЎ Wxhh>~[Vufր4Lv^O~ק)'CxѴc?0y@=,1qae6Y,٧)&v t >iW[,Ha׊B,1q ѳz1gl("kƁNGDE? //&/8_ Q:OB&IhOSILtRnZֲ` [8Vn ?yOM#:3I cٷtDrdҦ湁rJ}lWzoH?zn([45ݱe3cCW\+ʹ*gk@^yP_tD3Zu&m'9t`AR/nf>QryJBoM+6eNM=G9ry>v2EAs u9bHj4Au\M>AU0M1:/ʲCn):XK;8{; %.q#>pW; Mx; -i,u`J2z FT*kɣdE`a ()sϩv8%`uc&,*ubnQEی ;KLj~ |@Z9~7Ԅc LY;Odp/m;^S~,߭R,-i@,r 29wQN'G{XP\?n_PTxuX3b`w݅qϴ:emK~ 8c<ʥ!q EM'~⨾;8zǍvΑt;*t$yYM>b.YaXO ?djOV)^ӑL@`r;.-3Ekqȼp-vˀ!>؜{AƬ8J^gkn{r ׃-.IƦ׻A|\f[AFǨwӘ]7#\t@^?c;!@w(;*.޵PY+Xғv;WϷn֣b^"GQXńV-,EoޑX<[5lB29xD,  hj咕6coLoim+<[J\rA*SkԪ|SJݮfdS `L\Vp(~;тA\pW,`]U|{ŋ0CS&%ރ]7:ׂ8-'-=*6[h-ʙZ.Řs[~k%oWoA.I=sd S-W)ƝqBWiNG׎!Z_ixP[䳪YZT|j!=O3m YP$KDD9RGFύB@U}-8zX+uJ%ڼ-ߓ71 |*0㊉Ks:r2,Np'$I +łʣ%Ȫ8ɟǟ_[$\NcJ {NJSvr㷾sg:8YsT#X}V Mm&?(w[T 26?;<"Л,J˩<~(s|B)Wt)`94l/(%l%e KLn34Ws=,?þג]!UO,+S|h!\?VxEd &VZ-wA5?AjF?Û5]iG͕Tʂ$wmd"?]#xppڻXҐ)%_[wo`X(|g9 niiUZziS^#w{dE)C_^rY?~8Km~IIENDB`gnocchiclient-7.0.1/doc/source/installation.rst0000664000372000037200000000013713225150105022465 0ustar travistravis00000000000000============ Installation ============ At the command line:: $ pip install gnocchiclient gnocchiclient-7.0.1/doc/source/conf.py0000664000372000037200000001335013225150105020532 0ustar travistravis00000000000000# -*- coding: utf-8 -*- # # Gnocchi documentation build configuration file # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import datetime import subprocess # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'gnocchiclient.gendoc', 'sphinx.ext.autodoc', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Gnocchiclient' copyright = u'%s, The Gnocchi Developers' % datetime.date.today().year # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = subprocess.Popen(['sh', '-c', 'cd ../..; python setup.py --version'], stdout=subprocess.PIPE).stdout.read() version = version.strip() # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. import sphinx_rtd_theme html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = '_static/gnocchi-logo.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. html_favicon = '_static/gnocchi-icon.ico' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'gnocchidoc' html_theme_options = { 'logo_only': True, } gnocchiclient-7.0.1/README.rst0000664000372000037200000000142513225150105016655 0ustar travistravis00000000000000============= gnocchiclient ============= .. image:: https://travis-ci.org/gnocchixyz/python-gnocchiclient.png?branch=master :target: https://travis-ci.org/gnocchixyz/python-gnocchiclient :alt: Build Status .. image:: https://badge.fury.io/py/gnocchiclient.svg :target: https://badge.fury.io/py/gnocchiclient Python bindings to the Gnocchi API This is a client for Gnocchi API. There's :doc:`a Python API ` (the :mod:`gnocchiclient` module), and a :doc:`command-line script ` (installed as :program:`gnocchi`). Each implements the entire Gnocchi API. * Free software: Apache license * Documentation: http://gnocchi.xyz/gnocchiclient * Source: https://github.com/gnocchixyz/python-gnocchiclient * Bugs: https://github.com/gnocchixyz/python-gnocchiclient/issues gnocchiclient-7.0.1/setup.py0000664000372000037200000000177713225150105016712 0ustar travistravis00000000000000# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools # In python < 2.7.4, a lazy loading of package `pbr` will break # setuptools if some other modules registered functions in `atexit`. # solution from: http://bugs.python.org/issue15881#msg170215 try: import multiprocessing # noqa except ImportError: pass setuptools.setup( setup_requires=['pbr'], pbr=True) gnocchiclient-7.0.1/gnocchiclient/0000775000372000037200000000000013225150244020001 5ustar travistravis00000000000000gnocchiclient-7.0.1/gnocchiclient/client.py0000664000372000037200000000430413225150105021626 0ustar travistravis00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import sys from keystoneauth1 import adapter from keystoneauth1 import exceptions as k_exc from gnocchiclient import exceptions def Client(version, *args, **kwargs): module = 'gnocchiclient.v%s.client' % version __import__(module) client_class = getattr(sys.modules[module], 'Client') return client_class(*args, **kwargs) class SessionClient(adapter.Adapter): def request(self, url, method, **kwargs): kwargs.setdefault('headers', kwargs.get('headers', {})) # NOTE(sileht): The standard call raises errors from # keystoneauth, where we need to raise the gnocchiclient errors. raise_exc = kwargs.pop('raise_exc', True) try: resp = super(SessionClient, self).request(url, method, raise_exc=False, **kwargs) except k_exc.connection.ConnectFailure as e: raise exceptions.ConnectionFailure( message=str(e), url=url, method=method) except k_exc.connection.UnknownConnectionError as e: raise exceptions.UnknownConnectionError( message=str(e), url=url, method=method) except k_exc.connection.ConnectTimeout as e: raise exceptions.ConnectionTimeout( message=str(e), url=url, method=method) except k_exc.SSLError as e: raise exceptions.SSLError(message=str(e), url=url, method=method) if raise_exc and resp.status_code >= 400: raise exceptions.from_response(resp, method) return resp gnocchiclient-7.0.1/gnocchiclient/osc.py0000664000372000037200000000352213225150105021135 0ustar travistravis00000000000000# Copyright 2014 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from osc_lib import utils DEFAULT_METRICS_API_VERSION = '1' API_VERSION_OPTION = 'os_metrics_api_version' API_NAME = "metric" API_VERSIONS = { "1": "gnocchiclient.v1.client.Client", } def make_client(instance): """Returns a metrics service client.""" version = instance._api_version[API_NAME] try: version = int(version) except ValueError: version = float(version) gnocchi_client = utils.get_client_class( API_NAME, version, API_VERSIONS) # NOTE(sileht): ensure setup of the session is done instance.setup_auth() return gnocchi_client(session=instance.session, adapter_options={ 'interface': instance.interface, 'region_name': instance.region_name }) def build_option_parser(parser): """Hook to add global options.""" parser.add_argument( '--os-metrics-api-version', metavar='', default=utils.env( 'OS_METRICS_API_VERSION', default=DEFAULT_METRICS_API_VERSION), help=('Metrics API version, default=' + DEFAULT_METRICS_API_VERSION + ' (Env: OS_METRICS_API_VERSION)')) return parser gnocchiclient-7.0.1/gnocchiclient/exceptions.py0000664000372000037200000001710113225150105022530 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import re class ClientException(Exception): """The base exception class for all exceptions this library raises.""" message = 'Unknown Error' def __init__(self, code=None, message=None, request_id=None, url=None, method=None): self.code = code self.message = message or self.__class__.message self.request_id = request_id self.url = url self.method = method def __str__(self): formatted_string = "%s" % self.message if self.code: formatted_string += " (HTTP %s)" % self.code if self.request_id: formatted_string += " (Request-ID: %s)" % self.request_id return formatted_string class RetryAfterException(ClientException): """The base exception for ClientExceptions that use Retry-After header.""" def __init__(self, *args, **kwargs): try: self.retry_after = int(kwargs.pop('retry_after')) except (KeyError, ValueError): self.retry_after = 0 super(RetryAfterException, self).__init__(*args, **kwargs) class MutipleMeaningException(object): """An mixin for exception that can be enhanced by reading the details""" class ConnectionFailure(ClientException): """Connection failure""" class ConnectionTimeout(ClientException): """Connection timeout""" class UnknownConnectionError(ClientException): """Unknown connection error""" class SSLError(ClientException): """SSL connection error""" class BadRequest(ClientException): """HTTP 400 - Bad request: you sent some malformed data.""" http_status = 400 message = "Bad request" class Unauthorized(ClientException): """HTTP 401 - Unauthorized: bad credentials.""" http_status = 401 message = "Unauthorized" class Forbidden(ClientException): """HTTP 403 - Forbidden: your credentials don't give you access to this resource. """ http_status = 403 message = "Forbidden" class NotFound(ClientException): """HTTP 404 - Not found""" http_status = 404 message = "Not found" class MetricNotFound(NotFound, MutipleMeaningException): message = "Metric not found" match = re.compile("Metric .* does not exist") class ResourceNotFound(NotFound, MutipleMeaningException): message = "Resource not found" match = re.compile("Resource .* does not exist") class ResourceTypeNotFound(NotFound, MutipleMeaningException): message = "Resource type not found" match = re.compile("Resource type .* does not exist") class ArchivePolicyNotFound(NotFound, MutipleMeaningException): message = "Archive policy not found" match = re.compile("Archive policy .* does not exist") class ArchivePolicyRuleNotFound(NotFound, MutipleMeaningException): message = "Archive policy rule not found" match = re.compile("Archive policy rule .* does not exist") class MethodNotAllowed(ClientException): """HTTP 405 - Method Not Allowed""" http_status = 405 message = "Method Not Allowed" class NotAcceptable(ClientException): """HTTP 406 - Not Acceptable""" http_status = 406 message = "Not Acceptable" class Conflict(ClientException): """HTTP 409 - Conflict""" http_status = 409 message = "Conflict" class NamedMetricAlreadyExists(Conflict, MutipleMeaningException): message = "Named metric already exists" match = re.compile("Named metric .* already exist") class ResourceTypeAlreadyExists(Conflict, MutipleMeaningException): message = "Resource type already exists" match = re.compile("Resource type .* already exists") class ResourceAlreadyExists(Conflict, MutipleMeaningException): message = "Resource already exists" match = re.compile("Resource .* already exists") class ArchivePolicyAlreadyExists(Conflict, MutipleMeaningException): message = "Archive policy already exists" match = re.compile("Archive policy .* already exists") class ArchivePolicyRuleAlreadyExists(Conflict, MutipleMeaningException): message = "Archive policy rule already exists" match = re.compile("Archive policy rule .* already exists") class OverLimit(RetryAfterException): """HTTP 413 - Over limit: you're over the API limits for this time period. """ http_status = 413 message = "Over limit" class RateLimit(RetryAfterException): """HTTP 429 - Rate limit: you've sent too many requests for this time period. """ http_status = 429 message = "Rate limit" class NotImplemented(ClientException): """HTTP 501 - Not Implemented: the server does not support this operation. """ http_status = 501 message = "Not Implemented" _error_classes = [BadRequest, Unauthorized, Forbidden, NotFound, MethodNotAllowed, NotAcceptable, Conflict, OverLimit, RateLimit, NotImplemented] _error_classes_enhanced = { NotFound: [MetricNotFound, ResourceTypeNotFound, ResourceNotFound, ArchivePolicyRuleNotFound, ArchivePolicyNotFound], Conflict: [NamedMetricAlreadyExists, ResourceTypeAlreadyExists, ResourceAlreadyExists, ArchivePolicyAlreadyExists, ArchivePolicyRuleAlreadyExists] } _code_map = dict( (c.http_status, (c, _error_classes_enhanced.get(c, []))) for c in _error_classes) def from_response(response, method=None): """Return an instance of one of the ClientException on an requests response. Usage:: resp, body = requests.request(...) if resp.status_code != 200: raise from_response(resp) """ if response.status_code: cls, enhanced_classes = _code_map.get(response.status_code, (ClientException, [])) req_id = response.headers.get("x-openstack-request-id") content_type = response.headers.get("Content-Type", "").split(";")[0] kwargs = { 'code': response.status_code, 'method': method, 'url': response.url, 'request_id': req_id, } if "retry-after" in response.headers: kwargs['retry_after'] = response.headers.get('retry-after') if content_type == "application/json": try: body = response.json() except ValueError: pass else: if 'description' in body: # Gnocchi json desc = body.get('description') if desc: for enhanced_cls in enhanced_classes: if enhanced_cls.match.match(desc): cls = enhanced_cls break kwargs['message'] = desc elif isinstance(body, dict) and isinstance(body.get("error"), dict): # Keystone json kwargs['message'] = body["error"]["message"] else: kwargs['message'] = response.text elif content_type.startswith("text/"): kwargs['message'] = response.text if not kwargs['message']: del kwargs['message'] return cls(**kwargs) gnocchiclient-7.0.1/gnocchiclient/gendoc.py0000664000372000037200000000421613225150105021611 0ustar travistravis00000000000000# -*- encoding: utf-8 -*- # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from __future__ import absolute_import import sys from os_doc_tools import commands from gnocchiclient import shell # HACK(jd) Not sure why but Sphinx setup this multiple times, so we just avoid # doing several times the requests by using this global variable :( _RUN = False def get_clients(): return {'gnocchi': { 'name': 'A time series storage and resources index service (Gnocchi)', }} def discover_subcommands(os_command, subcommands, extra_params): return shell.GnocchiCommandManager.SHELL_COMMANDS.keys() def setup(app): global _RUN if _RUN: return output_dir = "doc/source" os_command = 'gnocchi' print("Documenting '%s'" % os_command) api_name = "Gnocchi API" title = "Gnocchi command-line client" out_filename = os_command + ".rst" out_file = commands.generate_heading(os_command, api_name, title, output_dir, out_filename, False) if not out_file: sys.exit(-1) commands.generate_command(os_command, out_file) commands.generate_subcommands( os_command, out_file, list(sorted(shell.GnocchiCommandManager.SHELL_COMMANDS.keys())), None, "", "") print("Finished.\n") out_file.close() with open("doc/source/gnocchi.rst", "r") as f: data = f.read().splitlines(True) for index, line in enumerate(data): if "This chapter documents" in line: break with open("doc/source/gnocchi.rst", "w") as f: f.writelines(data[index + 1:]) _RUN = True gnocchiclient-7.0.1/gnocchiclient/shell.py0000664000372000037200000002504113225150105021460 0ustar travistravis00000000000000# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import logging import os import sys import warnings from cliff import app from cliff import commandmanager from keystoneauth1 import adapter from keystoneauth1 import exceptions from keystoneauth1 import loading from gnocchiclient import auth from gnocchiclient import benchmark from gnocchiclient import client from gnocchiclient.v1 import aggregates_cli from gnocchiclient.v1 import archive_policy_cli from gnocchiclient.v1 import archive_policy_rule_cli as ap_rule_cli from gnocchiclient.v1 import build_cli from gnocchiclient.v1 import capabilities_cli from gnocchiclient.v1 import metric_cli from gnocchiclient.v1 import resource_cli from gnocchiclient.v1 import resource_type_cli from gnocchiclient.v1 import status_cli from gnocchiclient.version import __version__ class GnocchiCommandManager(commandmanager.CommandManager): SHELL_COMMANDS = { "status": status_cli.CliStatusShow, "server version": build_cli.CliBuildShow, "resource list": resource_cli.CliResourceList, "resource show": resource_cli.CliResourceShow, "resource history": resource_cli.CliResourceHistory, "resource search": resource_cli.CliResourceSearch, "resource create": resource_cli.CliResourceCreate, "resource update": resource_cli.CliResourceUpdate, "resource delete": resource_cli.CliResourceDelete, "resource batch delete": resource_cli.CliResourceBatchDelete, "resource-type list": resource_type_cli.CliResourceTypeList, "resource-type create": resource_type_cli.CliResourceTypeCreate, "resource-type update": resource_type_cli.CliResourceTypeUpdate, "resource-type show": resource_type_cli.CliResourceTypeShow, "resource-type delete": resource_type_cli.CliResourceTypeDelete, "archive-policy list": archive_policy_cli.CliArchivePolicyList, "archive-policy show": archive_policy_cli.CliArchivePolicyShow, "archive-policy create": archive_policy_cli.CliArchivePolicyCreate, "archive-policy update": archive_policy_cli.CliArchivePolicyUpdate, "archive-policy delete": archive_policy_cli.CliArchivePolicyDelete, "archive-policy-rule list": ap_rule_cli.CliArchivePolicyRuleList, "archive-policy-rule show": ap_rule_cli.CliArchivePolicyRuleShow, "archive-policy-rule create": ap_rule_cli.CliArchivePolicyRuleCreate, "archive-policy-rule update": ap_rule_cli.CliArchivePolicyRuleUpdate, "archive-policy-rule delete": ap_rule_cli.CliArchivePolicyRuleDelete, "metric list": metric_cli.CliMetricList, "metric show": metric_cli.CliMetricShow, "metric create": metric_cli.CliMetricCreate, "metric delete": metric_cli.CliMetricDelete, "measures show": metric_cli.CliMeasuresShow, "measures add": metric_cli.CliMeasuresAdd, "measures batch-metrics": metric_cli.CliMetricsMeasuresBatch, "measures batch-resources-metrics": metric_cli.CliResourcesMetricsMeasuresBatch, "measures aggregation": metric_cli.CliMeasuresAggregation, "aggregates": aggregates_cli.CliAggregates, "capabilities list": capabilities_cli.CliCapabilitiesList, "benchmark metric create": benchmark.CliBenchmarkMetricCreate, "benchmark metric show": benchmark.CliBenchmarkMetricShow, "benchmark measures add": benchmark.CliBenchmarkMeasuresAdd, "benchmark measures show": benchmark.CliBenchmarkMeasuresShow, } def load_commands(self, namespace): for name, command_class in self.SHELL_COMMANDS.items(): self.add_command(name, command_class) class GnocchiShell(app.App): def __init__(self): super(GnocchiShell, self).__init__( description='Gnocchi command line client', version=__version__, command_manager=GnocchiCommandManager('gnocchiclient'), deferred_help=True, ) self._client = None def build_option_parser(self, description, version): """Return an argparse option parser for this application. Subclasses may override this method to extend the parser with more global options. :param description: full description of the application :paramtype description: str :param version: version number for the application :paramtype version: str """ parser = super(GnocchiShell, self).build_option_parser( description, version, argparse_kwargs={'allow_abbrev': False}) parser.add_argument( '--gnocchi-api-version', default=os.environ.get('GNOCCHI_API_VERSION', '1'), help='Defaults to env[GNOCCHI_API_VERSION] or 1.') # NOTE(jd) This is a workaroun for people using Keystone auth with the # CLI. A lot of rc files do not export OS_AUTH_TYPE=password and # assumes it is the default. It's not in that case, but since we can't # fix all the rc files of the world, workaround it here. if ("OS_AUTH_PASSWORD" in os.environ and "OS_AUTH_TYPE" not in os.environ): os.environ.set("OS_AUTH_TYPE", "password") loading.register_session_argparse_arguments(parser=parser) plugin = loading.register_auth_argparse_arguments( parser=parser, argv=sys.argv, default="gnocchi-basic") if not isinstance(plugin, (auth.GnocchiNoAuthLoader, auth.GnocchiBasicLoader)): adapter.register_adapter_argparse_arguments( parser=parser, service_type="metric") adapter.register_service_adapter_argparse_arguments( parser=parser, service_type="metric") parser.add_argument( '--endpoint', default=os.environ.get('GNOCCHI_ENDPOINT'), help='Gnocchi endpoint (Env: GNOCCHI_ENDPOINT). ' 'Deprecated, use --os-endpoint-override and ' 'OS_ENDPOINT_OVERRIDE instead') return parser @property def client(self): # NOTE(sileht): we lazy load the client to not # load/connect auth stuffs if self._client is None: auth_plugin = loading.load_auth_from_argparse_arguments( self.options) session = loading.load_session_from_argparse_arguments( self.options, auth=auth_plugin) if isinstance(auth_plugin, (auth.GnocchiNoAuthPlugin, auth.GnocchiBasicPlugin)): # Normal endpoint kwargs = dict( version=self.options.gnocchi_api_version, session=session, ) else: # Openstck style endpoint kwargs = dict( version=(self.options.os_metric_api_version or self.options.os_api_version or self.options.gnocchi_api_version), session=session, adapter_options=dict( service_type=(self.options.os_metric_service_type or self.options.os_service_type), service_name=(self.options.os_metric_service_name or self.options.os_service_name), interface=(self.options.os_metric_interface or self.options.os_interface), region_name=self.options.os_region_name, endpoint_override=( self.options.os_metric_endpoint_override or self.options.os_endpoint_override or self.options.endpoint), ) ) self._client = client.Client(**kwargs) return self._client def clean_up(self, cmd, result, err): if err and isinstance(err, exceptions.HttpError): try: error = err.response.json() except Exception: pass else: if 'description' in error: # Gnocchi format print(error['description']) elif 'error' in error and 'message' in error['error']: # Keystone format print(error['error']['message']) def configure_logging(self): if self.options.debug: # --debug forces verbose_level 3 # Set this here so cliff.app.configure_logging() can work self.options.verbose_level = 3 super(GnocchiShell, self).configure_logging() root_logger = logging.getLogger('') # Set logging to the requested level if self.options.verbose_level == 0: # --quiet root_logger.setLevel(logging.ERROR) warnings.simplefilter("ignore") elif self.options.verbose_level == 1: # This is the default case, no --debug, --verbose or --quiet root_logger.setLevel(logging.WARNING) warnings.simplefilter("ignore") elif self.options.verbose_level == 2: # One --verbose root_logger.setLevel(logging.INFO) warnings.simplefilter("once") elif self.options.verbose_level >= 3: # Two or more --verbose root_logger.setLevel(logging.DEBUG) # Hide some useless message requests_log = logging.getLogger("requests") cliff_log = logging.getLogger('cliff') stevedore_log = logging.getLogger('stevedore') iso8601_log = logging.getLogger("iso8601") cliff_log.setLevel(logging.ERROR) stevedore_log.setLevel(logging.ERROR) iso8601_log.setLevel(logging.ERROR) if self.options.debug: requests_log.setLevel(logging.DEBUG) else: requests_log.setLevel(logging.ERROR) def main(args=None): if args is None: args = sys.argv[1:] return GnocchiShell().run(args) gnocchiclient-7.0.1/gnocchiclient/auth.py0000664000372000037200000001001313225150105021303 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import base64 import os from keystoneauth1 import loading from keystoneauth1 import plugin class GnocchiNoAuthPlugin(plugin.BaseAuthPlugin): """No authentication plugin for Gnocchi This is a keystoneauth plugin that instead of doing authentication, it just fill the 'x-user-id' and 'x-project-id' headers with the user provided one. """ def __init__(self, user_id, project_id, roles, endpoint): self._user_id = user_id self._project_id = project_id self._endpoint = endpoint self._roles = roles def get_headers(self, session, **kwargs): return {'x-user-id': self._user_id, 'x-project-id': self._project_id, 'x-roles': self._roles} def get_user_id(self, session, **kwargs): return self._user_id def get_project_id(self, session, **kwargs): return self._project_id def get_endpoint(self, session, **kwargs): return self._endpoint def get_auth_ref(self, session, **kwargs): return None class GnocchiOpt(loading.Opt): @property def argparse_args(self): return ['--%s' % o.name for o in self._all_opts] @property def argparse_default(self): # select the first ENV that is not false-y or return None for o in self._all_opts: v = os.environ.get('GNOCCHI_%s' % o.name.replace('-', '_').upper()) if v: return v return self.default class GnocchiNoAuthLoader(loading.BaseLoader): plugin_class = GnocchiNoAuthPlugin def get_options(self): options = super(GnocchiNoAuthLoader, self).get_options() options.extend([ GnocchiOpt('user-id', help='User ID', required=True, metavar=""), GnocchiOpt('project-id', help='Project ID', required=True, metavar=""), GnocchiOpt('roles', help='Roles', default="admin", metavar=""), GnocchiOpt('endpoint', help='Gnocchi endpoint', deprecated=[ GnocchiOpt('gnocchi-endpoint'), ], dest="endpoint", required=True, metavar=""), ]) return options class GnocchiBasicPlugin(plugin.BaseAuthPlugin): """Basic authentication plugin for Gnocchi.""" def __init__(self, user, endpoint): self._user = user.encode('utf-8') self._endpoint = endpoint def get_headers(self, session, **kwargs): return { 'Authorization': (b"basic " + base64.b64encode( self._user + b":")).decode('ascii') } def get_endpoint(self, session, **kwargs): return self._endpoint def get_auth_ref(self, session, **kwargs): return None class GnocchiBasicLoader(loading.BaseLoader): plugin_class = GnocchiBasicPlugin def get_options(self): options = super(GnocchiBasicLoader, self).get_options() options.extend([ GnocchiOpt('user', help='User', required=True, default="admin", metavar=""), GnocchiOpt('endpoint', help='Gnocchi endpoint', dest="endpoint", required=True, default="http://localhost:8041", metavar=""), ]) return options gnocchiclient-7.0.1/gnocchiclient/__init__.py0000664000372000037200000000000013225150105022074 0ustar travistravis00000000000000gnocchiclient-7.0.1/gnocchiclient/version.py0000664000372000037200000000123613225150105022036 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import pbr.version __version__ = pbr.version.VersionInfo('gnocchiclient').version_string() gnocchiclient-7.0.1/gnocchiclient/tests/0000775000372000037200000000000013225150244021143 5ustar travistravis00000000000000gnocchiclient-7.0.1/gnocchiclient/tests/__init__.py0000664000372000037200000000000013225150105023236 0ustar travistravis00000000000000gnocchiclient-7.0.1/gnocchiclient/tests/functional/0000775000372000037200000000000013225150244023305 5ustar travistravis00000000000000gnocchiclient-7.0.1/gnocchiclient/tests/functional/test_resource.py0000664000372000037200000002102113225150105026535 0ustar travistravis00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json import uuid from gnocchiclient.tests.functional import base class ResourceClientTest(base.ClientTestBase): RESOURCE_ID = str(uuid.uuid4()) RESOURCE_ID2 = "foo" PROJECT_ID = str(uuid.uuid4()) def test_help(self): self.gnocchi("help", params="resource list") self.gnocchi("help", params="resource history") self.gnocchi("help", params="resource search") def test_resource_scenario(self): apname = str(uuid.uuid4()) # Create an archive policy self.gnocchi( u'archive-policy', params=u"create %s" u" -d granularity:1s,points:86400" % apname) # CREATE result = self.gnocchi( u'resource', params=u"create %s --type generic" % self.RESOURCE_ID) resource = json.loads(result) self.assertEqual(self.RESOURCE_ID, resource["id"]) self.assertIsNone(resource["project_id"]) self.assertIsNotNone(resource["started_at"]) # CREATE FAIL result = self.gnocchi('resource', params="create generic -a id:%s" % self.RESOURCE_ID, fail_ok=True, merge_stderr=True) self.assertEqual( "Resource %s already exists (HTTP 409)\n" % self.RESOURCE_ID, result) # UPDATE result = self.gnocchi( 'resource', params=("update -t generic %s -a project_id:%s " "-n temperature:%s" % (self.RESOURCE_ID, self.PROJECT_ID, apname))) resource_updated = json.loads(result) self.assertEqual(self.RESOURCE_ID, resource_updated["id"]) self.assertEqual(self.PROJECT_ID, resource_updated["project_id"]) self.assertEqual(resource["started_at"], resource_updated["started_at"]) self.assertIn("temperature", resource_updated["metrics"]) # GET result = self.gnocchi( 'resource', params="show -t generic %s" % self.RESOURCE_ID) resource_got = json.loads(result) self.assertEqual(self.RESOURCE_ID, resource_got["id"]) self.assertEqual(self.PROJECT_ID, resource_got["project_id"]) self.assertEqual(resource["started_at"], resource_got["started_at"]) self.assertIn("temperature", resource_got["metrics"]) # HISTORY result = self.gnocchi( 'resource', params="history --type generic %s" % self.RESOURCE_ID) resource_history = json.loads(result) self.assertEqual(2, len(resource_history)) self.assertEqual(self.RESOURCE_ID, resource_history[0]["id"]) self.assertEqual(self.RESOURCE_ID, resource_history[1]["id"]) self.assertIsNone(resource_history[0]["project_id"]) self.assertEqual(self.PROJECT_ID, resource_history[1]["project_id"]) # LIST result = self.gnocchi('resource', params="list -t generic") self.assertIn(self.RESOURCE_ID, [r['id'] for r in json.loads(result)]) resource_list = [r for r in json.loads(result) if r['id'] == self.RESOURCE_ID][0] self.assertEqual(self.RESOURCE_ID, resource_list["id"]) self.assertEqual(self.PROJECT_ID, resource_list["project_id"]) self.assertEqual(resource["started_at"], resource_list["started_at"]) # Search result = self.gnocchi('resource', params=("search --type generic " "'project_id=%s'" ) % self.PROJECT_ID) resource_list = json.loads(result)[0] self.assertEqual(self.RESOURCE_ID, resource_list["id"]) self.assertEqual(self.PROJECT_ID, resource_list["project_id"]) self.assertEqual(resource["started_at"], resource_list["started_at"]) # UPDATE with Delete metric result = self.gnocchi( 'resource', params=("update -t generic %s -a project_id:%s " "-d temperature" % (self.RESOURCE_ID, self.PROJECT_ID))) resource_updated = json.loads(result) self.assertNotIn("temperature", resource_updated["metrics"]) result = self.gnocchi( 'resource', params=("update %s -d temperature" % self.RESOURCE_ID), fail_ok=True, merge_stderr=True) self.assertEqual( "Metric name temperature not found\n", result) # CREATE 2 result = self.gnocchi( 'resource', params=("create %s -t generic " "-a project_id:%s" ) % (self.RESOURCE_ID2, self.PROJECT_ID)) resource2 = json.loads(result) self.assertEqual(self.RESOURCE_ID2, resource2["original_resource_id"]) self.assertEqual(self.PROJECT_ID, resource2["project_id"]) self.assertIsNotNone(resource2["started_at"]) # Search + limit + short result = self.gnocchi('resource', params=("search " "-t generic " "'project_id=%s' " "--sort started_at:asc " "--marker %s " "--limit 1" ) % (self.PROJECT_ID, self.RESOURCE_ID)) resource_limit = json.loads(result)[0] self.assertEqual(self.RESOURCE_ID2, resource_limit["original_resource_id"]) self.assertEqual(self.PROJECT_ID, resource_limit["project_id"]) self.assertEqual(resource2["started_at"], resource_limit["started_at"]) # DELETE self.gnocchi('resource', params="delete %s" % self.RESOURCE_ID, has_output=False) self.gnocchi('resource', params="delete %s" % self.RESOURCE_ID2, has_output=False) # GET FAIL result = self.gnocchi('resource', params="show --type generic %s" % self.RESOURCE_ID, fail_ok=True, merge_stderr=True) self.assertEqual( "Resource %s does not exist (HTTP 404)\n" % self.RESOURCE_ID, result) # DELETE FAIL result = self.gnocchi('resource', params="delete %s" % self.RESOURCE_ID, fail_ok=True, merge_stderr=True, has_output=False) self.assertEqual( "Resource %s does not exist (HTTP 404)\n" % self.RESOURCE_ID, result) # Create and Batch Delete result1 = self.gnocchi( u'resource', params=u"create %s --type generic" % self.RESOURCE_ID) result2 = self.gnocchi( u'resource', params=u"create %s --type generic" % self.RESOURCE_ID2) resource1 = json.loads(result1) resource2 = json.loads(result2) self.assertEqual(self.RESOURCE_ID, resource1['id']) self.assertEqual(self.RESOURCE_ID2, resource2['original_resource_id']) result3 = self.gnocchi( 'resource batch delete ', params=("'id in [%s, %s]' " "-t generic") % (resource1["id"], resource2["id"])) resource3 = json.loads(result3) self.assertEqual(2, int(resource3["deleted"])) result4 = self.gnocchi( 'resource batch delete ', params=("'id in [%s, %s]' " "-t generic") % (resource1["id"], resource2["id"])) resource4 = json.loads(result4) self.assertEqual(0, int(resource4["deleted"])) # LIST EMPTY result = self.gnocchi('resource', params="list -t generic") resource_ids = [r['id'] for r in json.loads(result)] self.assertNotIn(self.RESOURCE_ID, resource_ids) self.assertNotIn(self.RESOURCE_ID2, resource_ids) gnocchiclient-7.0.1/gnocchiclient/tests/functional/test_benchmark.py0000664000372000037200000001067113225150105026651 0ustar travistravis00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json import uuid from gnocchiclient.tests.functional import base class BenchmarkMetricTest(base.ClientTestBase): def test_benchmark_metric_create_wrong_workers(self): result = self.gnocchi( u'benchmark', params=u"metric create -n 0", fail_ok=True, merge_stderr=True) self.assertIn("0 must be greater than 0", result) def test_benchmark_metric_create(self): apname = str(uuid.uuid4()) # PREPARE AN ARCHIVE POLICY self.gnocchi("archive-policy", params="create %s " "--back-window 0 -d granularity:1s,points:86400" % apname) result = self.gnocchi( u'benchmark', params=u"metric create -n 10 -a %s" % apname) result = json.loads(result) self.assertEqual(10, int(result['create executed'])) self.assertLessEqual(int(result['create failures']), 10) self.assertLessEqual(int(result['delete executed']), int(result['create executed'])) result = self.gnocchi( u'benchmark', params=u"metric create -k -n 10 -a %s" % apname) result = json.loads(result) self.assertEqual(10, int(result['create executed'])) self.assertLessEqual(int(result['create failures']), 10) self.assertNotIn('delete executed', result) def test_benchmark_metric_get(self): apname = str(uuid.uuid4()) # PREPARE AN ARCHIVE POLICY self.gnocchi("archive-policy", params="create %s " "--back-window 0 -d granularity:1s,points:86400" % apname) result = self.gnocchi( u'metric', params=u"create -a %s" % apname) metric = json.loads(result) result = self.gnocchi( u'benchmark', params=u"metric show -n 10 %s" % metric['id']) result = json.loads(result) self.assertEqual(10, int(result['show executed'])) self.assertLessEqual(int(result['show failures']), 10) def test_benchmark_measures_add(self): apname = str(uuid.uuid4()) # PREPARE AN ARCHIVE POLICY self.gnocchi("archive-policy", params="create %s " "--back-window 0 -d granularity:1s,points:86400" % apname) result = self.gnocchi( u'metric', params=u"create -a %s" % apname) metric = json.loads(result) result = self.gnocchi( u'benchmark', params=u"measures add -n 10 -b 4 %s" % metric['id']) result = json.loads(result) self.assertEqual(2, int(result['push executed'])) self.assertLessEqual(int(result['push failures']), 2) result = self.gnocchi( u'benchmark', params=u"measures add -s 2010-01-01 -n 10 -b 4 %s" % metric['id']) result = json.loads(result) self.assertEqual(2, int(result['push executed'])) self.assertLessEqual(int(result['push failures']), 2) result = self.gnocchi( u'benchmark', params=u"measures add --wait -s 2010-01-01 -n 10 -b 4 %s" % metric['id']) result = json.loads(result) self.assertEqual(2, int(result['push executed'])) self.assertLessEqual(int(result['push failures']), 2) self.assertIn("extra wait to process measures", result) def test_benchmark_measures_show(self): apname = str(uuid.uuid4()) # PREPARE AN ARCHIVE POLICY self.gnocchi("archive-policy", params="create %s " "--back-window 0 -d granularity:1s,points:86400" % apname) result = self.gnocchi( u'metric', params=u"create -a %s" % apname) metric = json.loads(result) result = self.gnocchi( u'benchmark', params=u"measures show -n 2 %s" % metric['id']) result = json.loads(result) self.assertEqual(2, int(result['show executed'])) self.assertLessEqual(int(result['show failures']), 2) gnocchiclient-7.0.1/gnocchiclient/tests/functional/test_archive_policy_rule.py0000664000372000037200000001030313225150105030736 0ustar travistravis00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json import uuid from gnocchiclient.tests.functional import base class ArchivePolicyRuleClientTest(base.ClientTestBase): def test_archive_policy_rule_scenario(self): apname = str(uuid.uuid4()) # Create an archive policy self.gnocchi( u'archive-policy', params=u"create %s" u" -d granularity:1s,points:86400" % apname) # CREATE result = self.gnocchi( u'archive-policy-rule', params=u"create test" u" --archive-policy-name %s" u" --metric-pattern 'disk.io.*'" % apname) policy_rule = json.loads(result) self.assertEqual('test', policy_rule["name"]) # CREATE FAIL result = self.gnocchi( u'archive-policy-rule', params=u"create test" u" --archive-policy-name high" u" --metric-pattern 'disk.io.*'", fail_ok=True, merge_stderr=True) self.assertEqual( "Archive policy rule test already exists (HTTP 409)\n", result) # GET result = self.gnocchi( 'archive-policy-rule', params="show test") policy_rule = json.loads(result) self.assertEqual("test", policy_rule["name"]) # LIST result = self.gnocchi('archive-policy-rule', params="list") rules = json.loads(result) rule_from_list = [p for p in rules if p['name'] == 'test'][0] for field in ["metric_pattern", "archive_policy_name"]: self.assertEqual(policy_rule[field], rule_from_list[field]) # CREATE - RENAME - GET self.gnocchi( u'archive-policy-rule', params=u"create to_rename" u" --archive-policy-name %s" u" --metric-pattern 'disk.io.*'" % apname) result = self.gnocchi( u'archive-policy-rule', params=u'update to_rename' u' --name renamed') policy_rule = json.loads(result) self.assertEqual('renamed', policy_rule["name"]) result = self.gnocchi( 'archive-policy-rule', params="show renamed") policy_rule = json.loads(result) self.assertEqual("renamed", policy_rule["name"]) # CONFIRM RENAME result = self.gnocchi('archive-policy-rule', params="show to_rename", fail_ok=True, merge_stderr=True) self.assertEqual( "Archive policy rule to_rename does not exist (HTTP 404)\n", result) # RENAME FAIL result = self.gnocchi( 'archive-policy-rule', params='update test' ' --name renamed', fail_ok=True, merge_stderr=True) self.assertEqual( 'Archive policy rule test does not support change:' ' Archive policy rule renamed already exists. (HTTP 400)\n', result) # DELETE self.gnocchi('archive-policy-rule', params="delete test", has_output=False) # GET FAIL result = self.gnocchi('archive-policy-rule', params="show test", fail_ok=True, merge_stderr=True, has_output=False) self.assertEqual( "Archive policy rule test does not exist (HTTP 404)\n", result) # DELETE FAIL result = self.gnocchi('archive-policy-rule', params="delete test", fail_ok=True, merge_stderr=True, has_output=False) self.assertEqual( "Archive policy rule test does not exist (HTTP 404)\n", result) gnocchiclient-7.0.1/gnocchiclient/tests/functional/__init__.py0000664000372000037200000000000013225150105025400 0ustar travistravis00000000000000gnocchiclient-7.0.1/gnocchiclient/tests/functional/test_metric.py0000664000372000037200000005523213225150105026204 0ustar travistravis00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json import os import tempfile import uuid import fixtures from gnocchiclient import auth from gnocchiclient import client from gnocchiclient.tests.functional import base from gnocchiclient import utils class MetricClientTest(base.ClientTestBase): def test_delete_several_metrics(self): apname = str(uuid.uuid4()) # PREPARE AN ARCHIVE POLICY self.gnocchi("archive-policy", params="create %s " "--back-window 0 -d granularity:1s,points:86400" % apname) # Create 2 metrics result = self.gnocchi( u'metric', params=u"create" u" --archive-policy-name %s" % apname) metric1 = json.loads(result) result = self.gnocchi( u'metric', params=u"create" u" --archive-policy-name %s" % apname) metric2 = json.loads(result) # DELETE self.gnocchi('metric', params="delete %s %s" % (metric1["id"], metric2["id"]), has_output=False) # GET FAIL result = self.gnocchi('metric', params="show %s" % metric1["id"], fail_ok=True, merge_stderr=True) self.assertEqual("Metric %s does not exist (HTTP 404)\n" % metric1["id"], result) result = self.gnocchi('metric', params="show %s" % metric2["id"], fail_ok=True, merge_stderr=True) self.assertEqual("Metric %s does not exist (HTTP 404)\n" % metric2["id"], result) def test_metric_listing(self): apname = str(uuid.uuid4()) # PREPARE AN ARCHIVE POLICY self.gnocchi("archive-policy", params="create " "-d granularity:1s,points:86400 %s" % apname) # Create 1005 metrics c = client.Client( 1, session_options={ 'auth': auth.GnocchiBasicPlugin( user="admin", endpoint=self.endpoint) }, ) for i in range(1005): c.metric.create({"archive_policy_name": apname}) result = self.gnocchi(u'metric', params=u"list") self.assertGreaterEqual(len(json.loads(result)), 1005) result = self.gnocchi(u'metric', params=u"list" u" --limit 2") self.assertEqual(2, len(json.loads(result))) def test_metric_measures_show_tz(self): ap_name = str(uuid.uuid4()) # PREPARE AN ARCHIVE POLICY self.gnocchi("archive-policy", params="create " + ap_name + " --back-window 0 -d granularity:1s,points:86400") # CREATE METRIC metric_name = str(uuid.uuid4()) result = self.gnocchi( u'metric', params=u"create" u" --archive-policy-name " + ap_name + " " + metric_name) metric = json.loads(result) # MEASURES ADD self.gnocchi('measures', params=("add %s " "-m '2015-03-06T14:33:57Z@43.11' " "--measure '2015-03-06T16:34:12+02:00@12' ") % metric["id"], has_output=False) # MEASURES SHOW self.useFixture(fixtures.EnvironmentVariable("TZ", "Europe/Paris")) result = self.gnocchi('measures', params="show --refresh " + metric["id"]) measures = json.loads(result) self.assertEqual("2015-03-06T15:33:57+01:00", measures[0]['timestamp']) self.assertEqual("2015-03-06T15:34:12+01:00", measures[1]['timestamp']) # MEASURES SHOW result = self.gnocchi('measures', params="show --utc --refresh " + metric["id"]) measures = json.loads(result) # Check that --utc is honored self.assertEqual("2015-03-06T14:33:57+00:00", measures[0]['timestamp']) self.assertEqual("2015-03-06T14:34:12+00:00", measures[1]['timestamp']) def test_metric_scenario(self): # PREPARE AN ARCHIVE POLICY self.gnocchi("archive-policy", params="create metric-test " "--back-window 0 -d granularity:1s,points:86400") # CREATE WITH NAME AND WITHOUT UNIT result = self.gnocchi( u'metric', params=u"create" u" --archive-policy-name metric-test some-name") metric = json.loads(result) self.assertIsNotNone(metric["id"]) self.assertEqual("admin", metric["creator"]) self.assertEqual('some-name', metric["name"]) self.assertIsNone(metric["unit"]) self.assertIsNone(metric["resource_id"]) self.assertIn("metric-test", metric["archive_policy_name"]) # CREATE WITH UNIT result = self.gnocchi( u'metric', params=u"create another-name" u" --archive-policy-name metric-test" u" --unit some-unit") metric = json.loads(result) self.assertIsNotNone(metric["id"]) self.assertEqual("admin", metric["creator"]) self.assertEqual('another-name', metric["name"]) self.assertEqual('some-unit', metric["unit"]) self.assertIsNone(metric["resource_id"]) self.assertIn("metric-test", metric["archive_policy_name"]) # GET result = self.gnocchi('metric', params="show %s" % metric["id"]) metric_get = json.loads(result) # `metric show` format resource/*, `metric create` does not metric_get['resource_id'] = metric_get['resource/id'] del metric_get['resource/id'] metric_get['archive_policy_name'] = metric_get['archive_policy/name'] for k in list(metric_get): if k.startswith("archive_policy/"): del metric_get[k] self.assertEqual(metric, metric_get) # MEASURES ADD self.gnocchi('measures', params=("add %s " "-m '2015-03-06T14:33:57Z@43.11' " "--measure '2015-03-06T14:34:12Z@12' ") % metric["id"], has_output=False) # MEASURES GET with refresh result = self.gnocchi('measures', params=("show %s " "--aggregation mean " "--granularity 1 " "--start 2015-03-06T14:32:00Z " "--stop 2015-03-06T14:36:00Z " "--refresh") % metric["id"]) measures = json.loads(result) self.assertEqual( [{'granularity': 1.0, 'timestamp': utils.dt_to_localtz( utils.parse_date('2015-03-06T14:33:57+00:00')).isoformat(), 'value': 43.11}, {'granularity': 1.0, 'timestamp': utils.dt_to_localtz( utils.parse_date('2015-03-06T14:34:12+00:00')).isoformat(), 'value': 12.0}], measures) # MEASURES GET result = self.retry_gnocchi( 5, 'measures', params=("show %s " "--aggregation mean " "--granularity 1 " "--start 2015-03-06T14:32:00Z " "--stop 2015-03-06T14:36:00Z" ) % metric["id"]) measures = json.loads(result) self.assertEqual([ {'granularity': 1.0, 'timestamp': utils.dt_to_localtz( utils.parse_date('2015-03-06T14:33:57+00:00')).isoformat(), 'value': 43.11}, {'granularity': 1.0, 'timestamp': utils.dt_to_localtz( utils.parse_date('2015-03-06T14:34:12+00:00')).isoformat(), 'value': 12.0} ], measures) # MEASURES GET RESAMPLE result = self.retry_gnocchi( 5, 'measures', params=("show %s " "--aggregation mean " "--granularity 1 --resample 3600 " "--start 2015-03-06T14:32:00Z " "--stop 2015-03-06T14:36:00Z" ) % metric["id"]) measures = json.loads(result) self.assertEqual([ {'granularity': 3600.0, 'timestamp': utils.dt_to_localtz( utils.parse_date(u'2015-03-06T14:00:00+00:00')).isoformat(), 'value': 27.555} ], measures) # MEASURES AGGREGATION result = self.gnocchi( 'measures', params=("aggregation " "--metric %s " "--aggregation mean " "--reaggregation sum " "--granularity 1 " "--start 2015-03-06T14:32:00Z " "--stop 2015-03-06T14:36:00Z" ) % metric["id"]) measures = json.loads(result) self.assertEqual([ {'granularity': 1.0, 'timestamp': utils.dt_to_localtz( utils.parse_date('2015-03-06T14:33:57+00:00')).isoformat(), 'value': 43.11}, {'granularity': 1.0, 'timestamp': utils.dt_to_localtz( utils.parse_date('2015-03-06T14:34:12+00:00')).isoformat(), 'value': 12.0} ], measures) # BATCHING measures = json.dumps({ metric['id']: [{'timestamp': '2015-03-06T14:34:12', 'value': 12}]}) tmpfile = tempfile.NamedTemporaryFile(delete=False) self.addCleanup(os.remove, tmpfile.name) with tmpfile as f: f.write(measures.encode('utf8')) self.gnocchi('measures', params=("batch-metrics %s" % tmpfile.name), has_output=False) self.gnocchi('measures', params="batch-metrics -", input=measures.encode('utf8'), has_output=False) # LIST result = self.gnocchi('metric', params="list") metrics = json.loads(result) metric_from_list = [p for p in metrics if p['id'] == metric['id']][0] metric['archive_policy/name'] = metric['archive_policy_name'] for field in ("id", "archive_policy/name", "name", "unit", "resource_id"): self.assertEqual(metric[field], metric_from_list[field], field) # LIST + limit result = self.gnocchi('metric', params=("list " "--sort name:asc " "--marker %s " "--limit 1") % metric['id']) metrics = json.loads(result) metric_from_list = metrics[0] self.assertEqual(1, len(metrics)) self.assertTrue(metric['name'] < metric_from_list['name']) # DELETE self.gnocchi('metric', params="delete %s" % metric["id"], has_output=False) # GET FAIL result = self.gnocchi('metric', params="show %s" % metric["id"], fail_ok=True, merge_stderr=True) self.assertEqual( "Metric %s does not exist (HTTP 404)\n" % metric["id"], result) # DELETE FAIL result = self.gnocchi('metric', params="delete %s" % metric["id"], fail_ok=True, merge_stderr=True, has_output=False) self.assertEqual( "Metric %s does not exist (HTTP 404)\n" % metric["id"], result) def test_metric_by_name_scenario(self): # PREPARE REQUIREMENT self.gnocchi("archive-policy", params="create metric-test2 " "--back-window 0 -d granularity:1s,points:86400") self.gnocchi("resource", params="create metric-res") metric_name = str(uuid.uuid4()) # CREATE result = self.gnocchi( u'metric', params=u"create" u" --archive-policy-name metric-test2 -r metric-res %s" u" --unit some-unit" % metric_name) metric = json.loads(result) self.assertIsNotNone(metric["id"]) self.assertEqual("admin", metric['creator']) self.assertEqual(metric_name, metric["name"]) self.assertEqual('some-unit', metric["unit"]) self.assertIsNotNone(metric["resource_id"]) self.assertIn("metric-test", metric["archive_policy/name"]) # CREATE FAIL result = self.gnocchi( u'metric', params=u"create" u" --archive-policy-name metric-test2 " "-r metric-res %s" % metric_name, fail_ok=True, merge_stderr=True) self.assertEqual( "Named metric %s already exists (HTTP 409)\n" % metric_name, result) # GET result = self.gnocchi('metric', params="show -r metric-res %s" % metric_name) metric_get = json.loads(result) # `metric show` returns resource details, `metric create` does not metric_get['resource_id'] = metric_get['resource/id'] for k in list(metric_get): if k.startswith("resource/"): del metric_get[k] self.assertEqual(metric, metric_get) # MEASURES ADD self.gnocchi('measures', params=("add " + metric_name + " -r metric-res " "-m '2015-03-06T14:33:57Z@43.11' " "--measure '2015-03-06T14:34:12Z@12'"), has_output=False) # MEASURES AGGREGATION with refresh result = self.gnocchi( 'measures', params=("aggregation " "--query \"id='metric-res'\" " "--resource-type \"generic\" " "-m " + metric_name + " " "--aggregation mean " "--needed-overlap 0 " "--start 2015-03-06T14:32:00Z " "--stop 2015-03-06T14:36:00Z " "--refresh")) measures = json.loads(result) self.assertEqual([ {'granularity': 1.0, 'timestamp': utils.dt_to_localtz( utils.parse_date('2015-03-06T14:33:57+00:00')).isoformat(), 'value': 43.11}, {'granularity': 1.0, 'timestamp': utils.dt_to_localtz( utils.parse_date('2015-03-06T14:34:12+00:00')).isoformat(), 'value': 12.0} ], measures) # MEASURES AGGREGATION result = self.gnocchi( 'measures', params=("aggregation " "--query \"id='metric-res'\" " "--resource-type \"generic\" " "-m " + metric_name + " " "--aggregation mean " "--needed-overlap 0 " "--start 2015-03-06T14:32:00Z " "--stop 2015-03-06T14:36:00Z")) measures = json.loads(result) self.assertEqual([ {'granularity': 1.0, 'timestamp': utils.dt_to_localtz( utils.parse_date(u'2015-03-06T14:33:57+00:00')).isoformat(), 'value': 43.11}, {'granularity': 1.0, 'timestamp': utils.dt_to_localtz( utils.parse_date(u'2015-03-06T14:34:12+00:00')).isoformat(), 'value': 12.0} ], measures) # MEASURES AGGREGATION WITH FILL result = self.gnocchi( 'measures', params=("aggregation " "--query \"id='metric-res'\" " "--resource-type \"generic\" " "-m " + metric_name + " " "--fill 0 " "--granularity 1 " "--start 2015-03-06T14:32:00Z " "--stop 2015-03-06T14:36:00Z")) measures = json.loads(result) self.assertEqual([ {'granularity': 1.0, 'timestamp': utils.dt_to_localtz( utils.parse_date(u'2015-03-06T14:33:57+00:00')).isoformat(), 'value': 43.11}, {'granularity': 1.0, 'timestamp': utils.dt_to_localtz( utils.parse_date(u'2015-03-06T14:34:12+00:00')).isoformat(), 'value': 12.0} ], measures) # MEASURES AGGREGATION RESAMPLE result = self.gnocchi( 'measures', params=("aggregation " "--query \"id='metric-res'\" " "--resource-type \"generic\" " "-m " + metric_name + " " "--granularity 1 " "--aggregation mean --resample=3600 " "--needed-overlap 0 " "--start 2015-03-06T14:32:00Z " "--stop 2015-03-06T14:36:00Z")) measures = json.loads(result) self.assertEqual([ {'granularity': 3600.0, 'timestamp': utils.dt_to_localtz( utils.parse_date('2015-03-06T14:00:00+00:00')).isoformat(), 'value': 27.555} ], measures) # MEASURES AGGREGATION GROUPBY result = self.gnocchi( 'measures', params=("aggregation " "--groupby project_id " "--groupby user_id " "--query \"id='metric-res'\" " "--resource-type \"generic\" " "-m " + metric_name + " " "--aggregation mean " "--needed-overlap 0 " "--start 2015-03-06T14:32:00Z " "--stop 2015-03-06T14:36:00Z")) measures = json.loads(result) self.assertEqual([ {'group': 'project_id: None, user_id: None', 'granularity': 1.0, 'timestamp': utils.dt_to_localtz( utils.parse_date(u'2015-03-06T14:33:57+00:00')).isoformat(), 'value': 43.11}, {'group': 'project_id: None, user_id: None', 'granularity': 1.0, 'timestamp': utils.dt_to_localtz( utils.parse_date(u'2015-03-06T14:34:12+00:00')).isoformat(), 'value': 12.0}, ], measures) # MEASURES GET result = self.gnocchi('measures', params=("show " + metric_name + " -r metric-res " "--aggregation mean " "--start 2015-03-06T14:32:00Z " "--stop 2015-03-06T14:36:00Z")) measures = json.loads(result) self.assertEqual([ {'granularity': 1.0, 'timestamp': utils.dt_to_localtz( utils.parse_date(u'2015-03-06T14:33:57+00:00')).isoformat(), 'value': 43.11}, {'granularity': 1.0, 'timestamp': utils.dt_to_localtz( utils.parse_date(u'2015-03-06T14:34:12+00:00')).isoformat(), 'value': 12.0}, ], measures) # BATCHING measures = json.dumps({'metric-res': {metric_name: [{ 'timestamp': '2015-03-06T14:34:12', 'value': 12 }]}}) tmpfile = tempfile.NamedTemporaryFile(delete=False) self.addCleanup(os.remove, tmpfile.name) with tmpfile as f: f.write(measures.encode('utf8')) self.gnocchi('measures', params=("batch-resources-metrics %s" % tmpfile.name), has_output=False) self.gnocchi('measures', params="batch-resources-metrics -", input=measures.encode('utf8'), has_output=False) # BATCHING --create-metrics measures = json.dumps({'metric-res': {'unknown-metric-name': [{ 'timestamp': '2015-03-06T14:34:12', 'value': 12 }]}}) self.gnocchi('measures', params="batch-resources-metrics --create-metrics -", input=measures.encode('utf8'), has_output=False) # LIST result = self.gnocchi('metric', params="list") metrics = json.loads(result) metric_from_list = [p for p in metrics if p['archive_policy/name'] == 'metric-test2'][0] for field in ["archive_policy/name", "name"]: # FIXME(sileht): add "resource_id" or "resource" # when LP#1497171 is fixed self.assertEqual(metric[field], metric_from_list[field]) # DELETE self.gnocchi('metric', params="delete -r metric-res " + metric_name, has_output=False) # GET FAIL result = self.gnocchi('metric', params="show -r metric-res " + metric_name, fail_ok=True, merge_stderr=True) self.assertEqual( "Metric " + metric_name + " does not exist (HTTP 404)\n", result) # DELETE FAIL result = self.gnocchi('metric', params="delete -r metric-res " + metric_name, fail_ok=True, merge_stderr=True, has_output=False) self.assertEqual( "Metric " + metric_name + " does not exist (HTTP 404)\n", result) # GET RESOURCE ID result = self.gnocchi( 'resource', params="show -t generic metric-res") resource_id = json.loads(result)["id"] # DELETE RESOURCE self.gnocchi('resource', params="delete metric-res", has_output=False) # GET FAIL WITH RESOURCE ERROR result = self.gnocchi('metric', params="show " + metric_name + " -r metric-res", fail_ok=True, merge_stderr=True) self.assertEqual( "Resource %s does not exist (HTTP 404)\n" % resource_id, result) gnocchiclient-7.0.1/gnocchiclient/tests/functional/test_resource_type.py0000664000372000037200000001003513225150105027601 0ustar travistravis00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json import uuid from gnocchiclient.tests.functional import base class ResourceTypeClientTest(base.ClientTestBase): RESOURCE_TYPE = str(uuid.uuid4()) RESOURCE_ID = str(uuid.uuid4()) def test_help(self): self.gnocchi("help", params="resource list", has_output=False) def test_resource_type_scenario(self): # LIST result = self.gnocchi('resource-type', params="list") r = json.loads(result) self.assertEqual([{'attributes': '', 'name': 'generic'}], r) # CREATE result = self.gnocchi( u'resource-type', params=u"create -a foo:string:true:max_length=16 " "-a bar:number:no:max=32 %s" % self.RESOURCE_TYPE) resource = json.loads(result) self.assertEqual(self.RESOURCE_TYPE, resource["name"]) self.assertEqual( "max_length=16, min_length=0, required=True, type=string", resource["attributes/foo"]) # SHOW result = self.gnocchi( u'resource-type', params=u"show %s" % self.RESOURCE_TYPE) resource = json.loads(result) self.assertEqual(self.RESOURCE_TYPE, resource["name"]) self.assertEqual( "max_length=16, min_length=0, required=True, type=string", resource["attributes/foo"]) # PATCH result = self.gnocchi( u'resource-type', params=u"update -r foo " "-a new:number:yes:max=16:fill=8 %s" % self.RESOURCE_TYPE) resource = json.loads(result) self.assertEqual(self.RESOURCE_TYPE, resource["name"]) self.assertNotIn("attributes/foo", resource) self.assertEqual( "max=16, min=None, required=True, type=number", resource["attributes/new"]) # SHOW result = self.gnocchi( u'resource-type', params=u"show %s" % self.RESOURCE_TYPE) resource = json.loads(result) self.assertEqual(self.RESOURCE_TYPE, resource["name"]) self.assertNotIn("attributes/foo", resource) self.assertEqual( "max=16, min=None, required=True, type=number", resource["attributes/new"]) # Create a resource for this type result = self.gnocchi( u'resource', params=(u"create %s -t %s -a new:5") % (self.RESOURCE_ID, self.RESOURCE_TYPE)) resource = json.loads(result) self.assertEqual(self.RESOURCE_ID, resource["id"]) self.assertEqual(5.0, resource["new"]) # Delete the resource self.gnocchi('resource', params="delete %s" % self.RESOURCE_ID, has_output=False) # DELETE self.gnocchi('resource-type', params="delete %s" % self.RESOURCE_TYPE, has_output=False) # DELETE AGAIN result = self.gnocchi('resource-type', params="delete %s" % self.RESOURCE_TYPE, fail_ok=True, merge_stderr=True, has_output=False) self.assertEqual( "Resource type %s does not exist (HTTP 404)\n" % self.RESOURCE_TYPE, result) # SHOW AGAIN result = self.gnocchi(u'resource-type', params=u"show %s" % self.RESOURCE_TYPE, fail_ok=True, merge_stderr=True) self.assertEqual( "Resource type %s does not exist (HTTP 404)\n" % self.RESOURCE_TYPE, result) gnocchiclient-7.0.1/gnocchiclient/tests/functional/test_archive_policy.py0000664000372000037200000000650413225150105027717 0ustar travistravis00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json import uuid from gnocchiclient.tests.functional import base class ArchivePolicyClientTest(base.ClientTestBase): def test_archive_policy_scenario(self): # CREATE apname = str(uuid.uuid4()) result = self.gnocchi( u'archive-policy', params=u"create %s" u" --back-window 0" u" -d granularity:1s,points:86400" % apname) policy = json.loads(result) self.assertEqual(apname, policy["name"]) # CREATE FAIL result = self.gnocchi( u'archive-policy', params=u"create %s" u" --back-window 0" u" -d granularity:1s,points:86400" % apname, fail_ok=True, merge_stderr=True) self.assertEqual( "Archive policy %s already exists (HTTP 409)\n" % apname, result) # GET result = self.gnocchi( 'archive-policy', params="show %s" % apname) policy = json.loads(result) self.assertEqual(apname, policy["name"]) # LIST result = self.gnocchi( 'archive-policy', params="list") policies = json.loads(result) policy_from_list = [p for p in policies if p['name'] == apname][0] for field in ["back_window", "definition", "aggregation_methods"]: self.assertEqual(policy[field], policy_from_list[field]) # UPDATE result = self.gnocchi( 'archive-policy', params='update %s' ' -d granularity:1s,points:60' % apname) policy = json.loads(result) self.assertEqual(apname, policy["name"]) # UPDATE FAIL result = self.gnocchi( 'archive-policy', params='update %s' ' -d granularity:5s,points:86400' % apname, fail_ok=True, merge_stderr=True) self.assertEqual( "Archive policy %s does not support change: 1.0 granularity " "interval was changed (HTTP 400)\n" % apname, result) # DELETE self.gnocchi('archive-policy', params="delete %s" % apname, has_output=False) # GET FAIL result = self.gnocchi('archive-policy', params="show %s" % apname, fail_ok=True, merge_stderr=True) self.assertEqual( "Archive policy %s does not exist (HTTP 404)\n" % apname, result) # DELETE FAIL result = self.gnocchi('archive-policy', params="delete %s" % apname, fail_ok=True, merge_stderr=True, has_output=False) self.assertEqual( "Archive policy %s does not exist (HTTP 404)\n" % apname, result) gnocchiclient-7.0.1/gnocchiclient/tests/functional/test_osc.py0000664000372000037200000000155313225150105025502 0ustar travistravis00000000000000# -*- encoding: utf-8 -*- # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json from gnocchiclient.tests.functional import base class OpenstackClentPluginTest(base.ClientTestBase): def test_osc_client(self): result = self.openstack("metric status") status = json.loads(result) self.assertEqual(2, len(status)) gnocchiclient-7.0.1/gnocchiclient/tests/functional/test_aggregates.py0000664000372000037200000001434613225150105027033 0ustar travistravis00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json from gnocchiclient.tests.functional import base class AggregatesClientTest(base.ClientTestBase): def test_scenario(self): # PREPARE AN ARCHIVE POLICY self.gnocchi("archive-policy", params="create agg-fetch-test " "--back-window 0 -d granularity:1s,points:86400") r1 = json.loads(self.gnocchi("resource", params="create metric-res1")) r2 = json.loads(self.gnocchi("resource", params="create metric-res2")) # CREATE A METRIC result = self.gnocchi( u'metric', params=u"create" u' -r metric-res1' u" --archive-policy-name agg-fetch-test metric-name") metric1 = json.loads(result) self.assertIsNotNone(metric1["id"]) self.assertEqual("admin", metric1["creator"]) self.assertEqual('metric-name', metric1["name"]) self.assertIsNone(metric1["unit"]) self.assertIsNotNone(metric1["resource_id"]) self.assertIn("agg-fetch-test", metric1["archive_policy/name"]) # CREATE ANOTHER METRIC result = self.gnocchi( u'metric', params=u"create" u' -r metric-res2' u" --archive-policy-name agg-fetch-test" u" --unit some-unit metric-name") metric2 = json.loads(result) self.assertIsNotNone(metric2["id"]) self.assertEqual("admin", metric2["creator"]) self.assertEqual('metric-name', metric2["name"]) self.assertEqual('some-unit', metric2["unit"]) self.assertIsNotNone(metric2["resource_id"]) self.assertIn("agg-fetch-test", metric2["archive_policy/name"]) # MEASURES ADD self.gnocchi('measures', params=("add %s " "-m '2015-03-06T14:33:57Z@43.11' " "--measure '2015-03-06T14:34:12Z@12' ") % metric1["id"], has_output=False) self.gnocchi('measures', params=("add %s " "-m '2015-03-06T14:33:57Z@43.11' " "--measure '2015-03-06T14:34:12Z@12' ") % metric2["id"], has_output=False) # MEASURES GET with refresh self.gnocchi('measures', params=("show %s " "--aggregation mean " "--granularity 1 " "--refresh") % metric1["id"]) self.gnocchi('measures', params=("show %s " "--aggregation mean " "--granularity 1 " "--refresh") % metric2["id"]) # MEASURES AGGREGATION METRIC IDS result = self.gnocchi( 'aggregates', params=("'(+ 2 (metric (%s mean) (%s mean)))' " "--granularity 1 " "--start 2015-03-06T14:32:00Z " "--stop 2015-03-06T14:36:00Z " ) % (metric1["id"], metric2["id"])) measures = json.loads(result) self.assertEqual(4, len(measures)) self.assertIn({'granularity': 1.0, 'name': '%s/mean' % metric1["id"], 'timestamp': '2015-03-06T14:33:57+00:00', 'value': 45.11}, measures) self.assertIn({'granularity': 1.0, 'name': '%s/mean' % metric1["id"], 'timestamp': '2015-03-06T14:34:12+00:00', 'value': 14.0}, measures) self.assertIn({'granularity': 1.0, 'name': '%s/mean' % metric2["id"], 'timestamp': '2015-03-06T14:33:57+00:00', 'value': 45.11}, measures) self.assertIn({'granularity': 1.0, 'name': '%s/mean' % metric2["id"], 'timestamp': '2015-03-06T14:34:12+00:00', 'value': 14.0}, measures) # MEASURES AGGREGATION METRIC NAMES result = self.gnocchi( 'aggregates', params=( "'(+ 2 (metric metric-name mean))' " "'original_resource_id like \"metric-res%\"' " "--groupby project_id " "--groupby user_id " "--resource-type generic " "--granularity 1 " "--start 2015-03-06T14:32:00Z " "--stop 2015-03-06T14:36:00Z " )) measures = json.loads(result) self.assertEqual(4, len(measures)) self.assertIn({'granularity': 1.0, 'group': u'project_id: None, user_id: None', 'name': u'%s/metric-name/mean' % r1["id"], 'timestamp': u'2015-03-06T14:33:57+00:00', 'value': 45.11}, measures) self.assertIn({'granularity': 1.0, 'group': u'project_id: None, user_id: None', 'name': u'%s/metric-name/mean' % r1["id"], 'timestamp': u'2015-03-06T14:34:12+00:00', 'value': 14.0}, measures) self.assertIn({'granularity': 1.0, 'group': u'project_id: None, user_id: None', 'name': u'%s/metric-name/mean' % r2["id"], 'timestamp': u'2015-03-06T14:33:57+00:00', 'value': 45.11}, measures) self.assertIn({'granularity': 1.0, 'group': u'project_id: None, user_id: None', 'name': u'%s/metric-name/mean' % r2["id"], 'timestamp': u'2015-03-06T14:34:12+00:00', 'value': 14.0}, measures) gnocchiclient-7.0.1/gnocchiclient/tests/functional/base.py0000664000372000037200000000660413225150105024573 0ustar travistravis00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import shlex import six import subprocess import time import testtools class ClientTestBase(testtools.TestCase): """Base class for gnocchiclient tests. Establishes the gnocchi client and retrieves the essential environment information. """ def setUp(self): super(ClientTestBase, self).setUp() self.cli_dir = os.environ.get('GNOCCHI_CLIENT_EXEC_DIR') self.endpoint = os.environ.get('PIFPAF_GNOCCHI_HTTP_URL') def openstack(self, action, flags='', params='', fail_ok=False, merge_stderr=False, input=None, has_output=True): flags = ((("--os-auth-type gnocchi-basic " "--os-user admin " "--os-endpoint %s") % self.endpoint) + ' ' + flags) return self._run("openstack", action, flags, params, fail_ok, merge_stderr, input, has_output) def gnocchi(self, action, flags='', params='', fail_ok=False, merge_stderr=False, input=None, has_output=True): flags = ((("--os-auth-plugin gnocchi-basic " "--user admin " "--endpoint %s") % self.endpoint) + ' ' + flags) return self._run("gnocchi", action, flags, params, fail_ok, merge_stderr, input, has_output) def _run(self, binary, action, flags='', params='', fail_ok=False, merge_stderr=False, input=None, has_output=True): fmt = '-f json' if has_output and action != 'help' else "" cmd = ' '.join([os.path.join(self.cli_dir, binary), flags, action, params, fmt]) if six.PY2: cmd = cmd.encode('utf-8') cmd = shlex.split(cmd) result = '' result_err = '' stdin = None if input is None else subprocess.PIPE stdout = subprocess.PIPE stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE proc = subprocess.Popen(cmd, stdin=stdin, stdout=stdout, stderr=stderr) result, result_err = proc.communicate(input=input) if not fail_ok and proc.returncode != 0: raise RuntimeError("Problem running command", proc.returncode, cmd, result, result_err) if not six.PY2: result = os.fsdecode(result) if not has_output and not fail_ok and action != 'help': self.assertEqual("", result) return result def retry_gnocchi(self, retry, *args, **kwargs): result = "" while not result.strip() and retry > 0: result = self.gnocchi(*args, **kwargs) if not result: time.sleep(1) retry -= 1 return result gnocchiclient-7.0.1/gnocchiclient/tests/functional/test_capabilities.py0000664000372000037200000000163313225150105027346 0ustar travistravis00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json from gnocchiclient.tests.functional import base class CapabilitiesClientTest(base.ClientTestBase): def test_capabilities_scenario(self): # GET result = self.gnocchi('capabilities', params="list") caps = json.loads(result) self.assertIsNotNone(caps) self.assertIn('aggregation_methods', caps) gnocchiclient-7.0.1/gnocchiclient/tests/functional/test_others.py0000664000372000037200000000203713225150105026220 0ustar travistravis00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json from gnocchiclient.tests.functional import base class MetricClientTest(base.ClientTestBase): def test_status_scenario(self): result = self.gnocchi("status") status = json.loads(result) self.assertEqual(2, len(status)) def test_build_scenario(self): result = self.gnocchi("server version") version = json.loads(result) self.assertEqual(1, len(version)) self.assertNotEqual("unknown", version["version"]) gnocchiclient-7.0.1/gnocchiclient/tests/unit/0000775000372000037200000000000013225150244022122 5ustar travistravis00000000000000gnocchiclient-7.0.1/gnocchiclient/tests/unit/__init__.py0000664000372000037200000000000013225150105024215 0ustar travistravis00000000000000gnocchiclient-7.0.1/gnocchiclient/tests/unit/test_auth.py0000664000372000037200000000162013225150105024467 0ustar travistravis00000000000000# -*- encoding: utf-8 -*- # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import testtools from gnocchiclient import auth class GnocchiBasicPluginTest(testtools.TestCase): def test_get_headers(self): p = auth.GnocchiBasicPlugin("foobar", "http://localhost") self.assertEqual({'Authorization': 'basic Zm9vYmFyOg=='}, p.get_headers(None)) gnocchiclient-7.0.1/gnocchiclient/tests/unit/test_exceptions.py0000664000372000037200000000500013225150105025703 0ustar travistravis00000000000000# -*- encoding: utf-8 -*- # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json from requests import models import testtools from gnocchiclient import exceptions class ExceptionsTest(testtools.TestCase): def test_from_response_404(self): r = models.Response() r.status_code = 404 r.headers['Content-Type'] = "application/json" r._content = json.dumps( {"description": "Archive policy rule foobar does not exist"} ).encode('utf-8') exc = exceptions.from_response(r) self.assertIsInstance(exc, exceptions.ArchivePolicyRuleNotFound) def test_resource_type_before_resource(self): r = models.Response() r.status_code = 404 r.headers['Content-Type'] = "application/json" r._content = json.dumps( {"description": "Resource type foobar does not exist"} ).encode('utf-8') exc = exceptions.from_response(r) self.assertIsInstance(exc, exceptions.ResourceTypeNotFound) def test_from_response_keystone_401(self): r = models.Response() r.status_code = 401 r.headers['Content-Type'] = "application/json" r._content = json.dumps({"error": { "message": "The request you have made requires authentication.", "code": 401, "title": "Unauthorized"}} ).encode('utf-8') exc = exceptions.from_response(r) self.assertIsInstance(exc, exceptions.Unauthorized) self.assertEqual("The request you have made requires authentication.", exc.message) def test_from_response_unknown_middleware(self): r = models.Response() r.status_code = 400 r.headers['Content-Type'] = "application/json" r._content = json.dumps( {"unknown": "random message"} ).encode('utf-8') exc = exceptions.from_response(r) self.assertIsInstance(exc, exceptions.ClientException) self.assertEqual('{"unknown": "random message"}', exc.message) gnocchiclient-7.0.1/gnocchiclient/utils.py0000664000372000037200000001127513225150105021515 0ustar travistravis00000000000000# -*- encoding: utf-8 -*- # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from dateutil import tz import iso8601 import six from six.moves.urllib import parse as urllib_parse def add_query_argument(cmd, parser, *args, **kwargs): return parser.add_argument( cmd, help=u"A query to filter resource. " u"The syntax is a combination of attribute, operator and value. " u"For example: id=90d58eea-70d7-4294-a49a-170dcdf44c3c would filter " u"resource with a certain id. More complex queries can be built, " u"e.g.: not (flavor_id!=\"1\" and memory>=24). " u"Use \"\" to force data to be interpreted as string. " u"Supported operators are: not, and, ∧ or, ∨, >=, <=, !=, >, <, =, " u"==, eq, ne, lt, gt, ge, le, in, like, ≠, ≥, ≤, like, in.", *args, **kwargs) def list2cols(cols, objs): return cols, [tuple([o[k] for k in cols]) for o in objs] def format_string_list(l): return ", ".join(l) def format_dict_list(l): return "\n".join( "- " + ", ".join("%s: %s" % (k, v) for k, v in elem.items()) for elem in l) def format_dict_dict(value): return "\n".join( "- %s: " % name + " , ".join("%s: %s" % (k, v) for k, v in elem.items()) for name, elem in value.items()) def format_move_dict_to_root(obj, field): for attr in obj[field]: obj["%s/%s" % (field, attr)] = obj[field][attr] del obj[field] def format_resource_type(rt): format_move_dict_to_root(rt, "attributes") for key in rt: if key.startswith("attributes"): rt[key] = ", ".join( "%s=%s" % (k, v) for k, v in sorted(rt[key].items())) def format_archive_policy(ap): ap['definition'] = format_dict_list(ap['definition']) ap['aggregation_methods'] = format_string_list(ap['aggregation_methods']) def format_resource_for_metric(metric): # NOTE(sileht): Gnocchi < 2.0 if 'resource' not in metric: return if not metric['resource']: metric['resource/id'] = None del metric['resource'] else: format_move_dict_to_root(metric, "resource") def dict_from_parsed_args(parsed_args, attrs): d = {} for attr in attrs: value = getattr(parsed_args, attr) if value is not None: d[attr] = value return d def dict_to_querystring(objs): strings = [] for k, values in sorted(objs.items()): if values is not None: if not isinstance(values, (list, tuple)): values = [values] strings.append("&".join( ("%s=%s" % (k, v) for v in map(urllib_parse.quote, map(six.text_type, values))))) return "&".join(strings) def get_pagination_options(parsed_args): options = dict( sorts=parsed_args.sort, limit=parsed_args.limit, marker=parsed_args.marker) if hasattr(parsed_args, 'details'): options['details'] = parsed_args.details if hasattr(parsed_args, 'history'): options['history'] = parsed_args.history return options def build_pagination_options(details=False, history=False, limit=None, marker=None, sorts=None): options = {} if details: options["details"] = "true" if history: options["history"] = "true" if limit: options["limit"] = int(limit) if marker: options["marker"] = marker if sorts: options["sort"] = sorts return options def get_client(obj): if hasattr(obj.app, 'client_manager'): # NOTE(sileht): cliff objects loaded by OSC return obj.app.client_manager.metric else: # TODO(sileht): Remove this when OSC is able # to install the gnocchi client binary itself return obj.app.client LOCAL_TIMEZONE = tz.gettz() def parse_date(s): """Parse date from string. If no timezone is specified, default is assumed to be local time zone. :param s: The date to parse. :type s: str """ return iso8601.parse_date(s, LOCAL_TIMEZONE) def dt_to_localtz(d): return d.astimezone(LOCAL_TIMEZONE) gnocchiclient-7.0.1/gnocchiclient/v1/0000775000372000037200000000000013225150244020327 5ustar travistravis00000000000000gnocchiclient-7.0.1/gnocchiclient/v1/client.py0000664000372000037200000000527513225150105022164 0ustar travistravis00000000000000# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import keystoneauth1.session from gnocchiclient import client from gnocchiclient.v1 import aggregates from gnocchiclient.v1 import archive_policy from gnocchiclient.v1 import archive_policy_rule from gnocchiclient.v1 import build from gnocchiclient.v1 import capabilities from gnocchiclient.v1 import metric from gnocchiclient.v1 import resource from gnocchiclient.v1 import resource_type from gnocchiclient.v1 import status class Client(object): """Client for the Gnocchi v1 API. :param session: keystoneauth1 session :type session: :py:class:`keystoneauth1.session.Session` (optional) :param adapter_options: options to pass to :py:class:`keystoneauth1.adapter.Adapter` :type adapter_options: dict (optional) :param session_options: options to pass to :py:class:`keystoneauth1.session.Session` :type session_options: dict (optional) """ def __init__(self, session=None, adapter_options=None, session_options=None): """Initialize a new client for the Gnocchi v1 API.""" session_options = session_options or {} adapter_options = adapter_options or {} adapter_options.setdefault('service_type', "metric") if session is None: session = keystoneauth1.session.Session(**session_options) else: if session_options: raise ValueError("session and session_options are exclusive") self.api = client.SessionClient(session, **adapter_options) self.resource = resource.ResourceManager(self) self.resource_type = resource_type.ResourceTypeManager(self) self.archive_policy = archive_policy.ArchivePolicyManager(self) self.archive_policy_rule = ( archive_policy_rule.ArchivePolicyRuleManager(self)) self.metric = metric.MetricManager(self) self.aggregates = aggregates.AggregatesManager(self) self.capabilities = capabilities.CapabilitiesManager(self) self.status = status.StatusManager(self) self.build = build.BuildManager(self) gnocchiclient-7.0.1/gnocchiclient/v1/build_cli.py0000664000372000037200000000153213225150105022624 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cliff import show from gnocchiclient import utils class CliBuildShow(show.ShowOne): """Show the version of Gnocchi server""" def take_action(self, parsed_args): return self.dict2columns({ "version": utils.get_client(self).build.get(), }) gnocchiclient-7.0.1/gnocchiclient/v1/build.py0000664000372000037200000000136413225150105022000 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from gnocchiclient.v1 import base class BuildManager(base.Manager): def get(self): """Get Gnocchi build.""" return self._get("").json().get("build", "unknown") gnocchiclient-7.0.1/gnocchiclient/v1/metric_cli.py0000664000372000037200000003473313225150105023021 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json import logging import sys from cliff import command from cliff import lister from cliff import show from gnocchiclient import utils LOG_DEP = logging.getLogger('deprecated') class CliMetricWithResourceID(command.Command): def get_parser(self, prog_name): parser = super(CliMetricWithResourceID, self).get_parser(prog_name) parser.add_argument("--resource-id", "-r", help="ID of the resource") return parser class CliMetricList(lister.Lister): """List metrics""" COLS = ('id', 'archive_policy/name', 'name', 'unit', 'resource_id') def get_parser(self, prog_name): parser = super(CliMetricList, self).get_parser(prog_name) parser.add_argument("--limit", type=int, metavar="", help="Number of metrics to return " "(Default is server default)") parser.add_argument("--marker", metavar="", help="Last item of the previous listing. " "Return the next results after this value") parser.add_argument("--sort", action="append", metavar="", help="Sort of metric attribute " "(example: user_id:desc-nullslast") return parser def take_action(self, parsed_args): metrics = utils.get_client(self).metric.list( **utils.get_pagination_options(parsed_args)) for metric in metrics: utils.format_archive_policy(metric["archive_policy"]) utils.format_move_dict_to_root(metric, "archive_policy") return utils.list2cols(self.COLS, metrics) class DeprecatedCliMetricList(CliMetricList): """Deprecated: List metrics""" def take_action(self, parsed_args): LOG_DEP.warning('This command has been deprecated. ' 'Please use "metric list" instead.') return super(DeprecatedCliMetricList, self).take_action(parsed_args) class CliMetricShow(CliMetricWithResourceID, show.ShowOne): """Show a metric""" def get_parser(self, prog_name): parser = super(CliMetricShow, self).get_parser(prog_name) parser.add_argument("metric", help="ID or name of the metric") return parser def take_action(self, parsed_args): metric = utils.get_client(self).metric.get( metric=parsed_args.metric, resource_id=parsed_args.resource_id) metric['archive_policy/name'] = metric["archive_policy"]["name"] del metric['archive_policy'] del metric['created_by_user_id'] del metric['created_by_project_id'] utils.format_resource_for_metric(metric) return self.dict2columns(metric) class DeprecatedCliMetricShow(CliMetricShow): """Deprecated: Show a metric""" def take_action(self, parsed_args): LOG_DEP.warning('This command has been deprecated. ' 'Please use "metric show" instead.') return super(DeprecatedCliMetricShow, self).take_action(parsed_args) class CliMetricCreateBase(show.ShowOne, CliMetricWithResourceID): def get_parser(self, prog_name): parser = super(CliMetricCreateBase, self).get_parser(prog_name) parser.add_argument("--archive-policy-name", "-a", dest="archive_policy_name", help="name of the archive policy") return parser class CliMetricCreate(CliMetricCreateBase): """Create a metric""" def get_parser(self, prog_name): parser = super(CliMetricCreate, self).get_parser(prog_name) parser.add_argument("name", nargs='?', metavar="METRIC_NAME", help="Name of the metric") parser.add_argument("--unit", "-u", help="unit of the metric") return parser def take_action(self, parsed_args): metric = utils.get_client(self).metric._create_new( archive_policy_name=parsed_args.archive_policy_name, name=parsed_args.name, resource_id=parsed_args.resource_id, unit=parsed_args.unit, ) utils.format_resource_for_metric(metric) if 'archive_policy' in metric: metric['archive_policy/name'] = metric["archive_policy"]["name"] del metric['archive_policy'] del metric['created_by_user_id'] del metric['created_by_project_id'] return self.dict2columns(metric) class DeprecatedCliMetricCreate(CliMetricCreate): """Deprecated: Create a metric""" def take_action(self, parsed_args): LOG_DEP.warning('This command has been deprecated. ' 'Please use "metric create" instead.') return super(DeprecatedCliMetricCreate, self).take_action(parsed_args) class CliMetricDelete(CliMetricWithResourceID): """Delete a metric""" def get_parser(self, prog_name): parser = super(CliMetricDelete, self).get_parser(prog_name) parser.add_argument("metric", nargs='+', help="IDs or names of the metric") return parser def take_action(self, parsed_args): for metric in parsed_args.metric: utils.get_client(self).metric.delete( metric=metric, resource_id=parsed_args.resource_id) class DeprecatedCliMetricDelete(CliMetricDelete): """Deprecated: Delete a metric""" def take_action(self, parsed_args): LOG_DEP.warning('This command has been deprecated. ' 'Please use "metric delete" instead.') return super(DeprecatedCliMetricDelete, self).take_action(parsed_args) class CliMeasuresReturn(lister.Lister): def get_parser(self, prog_name): parser = super(CliMeasuresReturn, self).get_parser(prog_name) parser.add_argument("--utc", help="Return timestamps as UTC", default=False, action="store_true") return parser @staticmethod def format_measures_with_tz(parsed_args, measures): if parsed_args.utc: t = lambda x: x else: t = utils.dt_to_localtz return [(t(dt).isoformat(), g, v) for dt, g, v in measures] class CliMeasuresShow(CliMetricWithResourceID, CliMeasuresReturn, lister.Lister): """Get measurements of a metric""" COLS = ('timestamp', 'granularity', 'value') def get_parser(self, prog_name): parser = super(CliMeasuresShow, self).get_parser(prog_name) parser.add_argument("metric", help="ID or name of the metric") parser.add_argument("--aggregation", help="aggregation to retrieve") parser.add_argument("--start", type=utils.parse_date, help="beginning of the period") parser.add_argument("--stop", type=utils.parse_date, help="end of the period") parser.add_argument("--granularity", help="granularity to retrieve") parser.add_argument("--refresh", action="store_true", help="force aggregation of all known measures") parser.add_argument("--resample", help=("granularity to resample time-series to " "(in seconds)")) return parser def take_action(self, parsed_args): measures = utils.get_client(self).metric.get_measures( metric=parsed_args.metric, resource_id=parsed_args.resource_id, aggregation=parsed_args.aggregation, start=parsed_args.start, stop=parsed_args.stop, granularity=parsed_args.granularity, refresh=parsed_args.refresh, resample=parsed_args.resample ) return self.COLS, self.format_measures_with_tz(parsed_args, measures) class CliMeasuresAddBase(CliMetricWithResourceID): def get_parser(self, prog_name): parser = super(CliMeasuresAddBase, self).get_parser(prog_name) parser.add_argument("metric", help="ID or name of the metric") return parser class CliMeasuresAdd(CliMeasuresAddBase): """Add measurements to a metric""" def measure(self, measure): timestamp, __, value = measure.rpartition("@") return {'timestamp': utils.parse_date(timestamp).isoformat(), 'value': float(value)} def get_parser(self, prog_name): parser = super(CliMeasuresAdd, self).get_parser(prog_name) parser.add_argument("-m", "--measure", action='append', required=True, type=self.measure, help=("timestamp and value of a measure " "separated with a '@'")) return parser def take_action(self, parsed_args): utils.get_client(self).metric.add_measures( metric=parsed_args.metric, resource_id=parsed_args.resource_id, measures=parsed_args.measure, ) class CliMeasuresBatch(command.Command): def stdin_or_file(self, value): if value == "-": return sys.stdin else: return open(value, 'r') def get_parser(self, prog_name): parser = super(CliMeasuresBatch, self).get_parser(prog_name) parser.add_argument("file", type=self.stdin_or_file, help=("File containing measurements to batch or " "- for stdin (see Gnocchi REST API docs for " "the format")) return parser class CliMetricsMeasuresBatch(CliMeasuresBatch): def take_action(self, parsed_args): with parsed_args.file as f: utils.get_client(self).metric.batch_metrics_measures(json.load(f)) class CliResourcesMetricsMeasuresBatch(CliMeasuresBatch): def get_parser(self, prog_name): parser = super(CliResourcesMetricsMeasuresBatch, self).get_parser( prog_name) parser.add_argument("--create-metrics", action='store_true', help="Create unknown metrics"), return parser def take_action(self, parsed_args): with parsed_args.file as f: utils.get_client(self).metric.batch_resources_metrics_measures( json.load(f), create_metrics=parsed_args.create_metrics) class CliMeasuresAggregation(CliMeasuresReturn): """Get measurements of aggregated metrics""" COLS = ('timestamp', 'granularity', 'value') def get_parser(self, prog_name): parser = super(CliMeasuresAggregation, self).get_parser(prog_name) parser.add_argument("-m", "--metric", nargs='+', required=True, help="metrics IDs or metric name") parser.add_argument("--aggregation", help="granularity aggregation " "function to retrieve") parser.add_argument("--reaggregation", help="groupby aggregation function to retrieve") parser.add_argument("--start", type=utils.parse_date, help="beginning of the period") parser.add_argument("--stop", type=utils.parse_date, help="end of the period") parser.add_argument("--granularity", help="granularity to retrieve") parser.add_argument("--needed-overlap", type=float, help=("percent of datapoints in each " "metrics required")) utils.add_query_argument("--query", parser) parser.add_argument("--resource-type", default="generic", help="Resource type to query"), parser.add_argument("--groupby", action='append', help="Attribute to use to group resources"), parser.add_argument("--refresh", action="store_true", help="force aggregation of all known measures") parser.add_argument("--resample", help=("granularity to resample time-series to " "(in seconds)")) parser.add_argument("--fill", help=("Value to use when backfilling timestamps " "with missing values in a subset of series. " "Value should be a float or 'null'.")) return parser def take_action(self, parsed_args): metrics = parsed_args.metric if parsed_args.query: if len(parsed_args.metric) != 1: raise ValueError("One metric is required if query is provided") metrics = parsed_args.metric[0] measures = utils.get_client(self).metric.aggregation( metrics=metrics, query=parsed_args.query, aggregation=parsed_args.aggregation, reaggregation=parsed_args.reaggregation, start=parsed_args.start, stop=parsed_args.stop, granularity=parsed_args.granularity, needed_overlap=parsed_args.needed_overlap, resource_type=parsed_args.resource_type, groupby=parsed_args.groupby, refresh=parsed_args.refresh, resample=parsed_args.resample, fill=parsed_args.fill ) if parsed_args.groupby: ms = [] for g in measures: group_name = ", ".join("%s: %s" % (k, g['group'][k]) for k in sorted(g['group'])) for m in g['measures']: i = [group_name] i.extend(self.format_measures_with_tz(parsed_args, [m])[0]) ms.append(i) return ('group',) + self.COLS, ms return self.COLS, self.format_measures_with_tz(parsed_args, measures) gnocchiclient-7.0.1/gnocchiclient/v1/archive_policy_rule_cli.py0000664000372000037200000000710313225150105025554 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cliff import command from cliff import lister from cliff import show from gnocchiclient import utils class CliArchivePolicyRuleList(lister.Lister): """List archive policy rules""" COLS = ('name', 'archive_policy_name', 'metric_pattern') def take_action(self, parsed_args): ap_rules = utils.get_client(self).archive_policy_rule.list() return utils.list2cols(self.COLS, ap_rules) class CliArchivePolicyRuleShow(show.ShowOne): """Show an archive policy rule""" def get_parser(self, prog_name): parser = super(CliArchivePolicyRuleShow, self).get_parser(prog_name) parser.add_argument("name", help="Name of the archive policy rule") return parser def take_action(self, parsed_args): ap_rule = utils.get_client(self).archive_policy_rule.get( name=parsed_args.name) return self.dict2columns(ap_rule) class CliArchivePolicyRuleCreate(show.ShowOne): """Create an archive policy rule""" def get_parser(self, prog_name): parser = super(CliArchivePolicyRuleCreate, self).get_parser(prog_name) parser.add_argument("name", help="Rule name") parser.add_argument("-a", "--archive-policy-name", dest="archive_policy_name", required=True, help="Archive policy name") parser.add_argument("-m", "--metric-pattern", dest="metric_pattern", required=True, help="Wildcard of metric name to match") return parser def take_action(self, parsed_args): rule = utils.dict_from_parsed_args( parsed_args, ["name", "metric_pattern", "archive_policy_name"]) policy = utils.get_client(self).archive_policy_rule.create(rule) return self.dict2columns(policy) class CliArchivePolicyRuleUpdate(show.ShowOne): """Update an archive policy rule""" def get_parser(self, prog_name): parser = super(CliArchivePolicyRuleUpdate, self).get_parser(prog_name) parser.add_argument("name", help="Rule name") parser.add_argument("-n", "--name", dest="new_name", required=True, help="New rule name") return parser def take_action(self, parsed_args): policy = utils.get_client(self).archive_policy_rule.update( parsed_args.name, parsed_args.new_name) return self.dict2columns(policy) class CliArchivePolicyRuleDelete(command.Command): """Delete an archive policy rule""" def get_parser(self, prog_name): parser = super(CliArchivePolicyRuleDelete, self).get_parser(prog_name) parser.add_argument("name", help="Name of the archive policy rule") return parser def take_action(self, parsed_args): utils.get_client(self).archive_policy_rule.delete(parsed_args.name) gnocchiclient-7.0.1/gnocchiclient/v1/__init__.py0000664000372000037200000000000013225150105022422 0ustar travistravis00000000000000gnocchiclient-7.0.1/gnocchiclient/v1/status.py0000664000372000037200000000144413225150105022223 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from gnocchiclient.v1 import base class StatusManager(base.Manager): url = "v1/status" def get(self, details=False): """Get Gnocchi status.""" return self._get(self.url + '?details=%s' % details).json() gnocchiclient-7.0.1/gnocchiclient/v1/resource_type_cli.py0000664000372000037200000001202513225150105024414 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import distutils.util from cliff import command from cliff import lister from cliff import show from gnocchiclient import utils class CliResourceTypeList(lister.Lister): """List resource types""" COLS = ('name', 'attributes') def take_action(self, parsed_args): resource_types = utils.get_client(self).resource_type.list() for resource_type in resource_types: resource_type['attributes'] = utils.format_dict_dict( resource_type['attributes']) return utils.list2cols(self.COLS, resource_types) class CliResourceTypeCreate(show.ShowOne): """Create a resource type""" def get_parser(self, prog_name): parser = super(CliResourceTypeCreate, self).get_parser(prog_name) parser.add_argument("name", help="name of the resource type") parser.add_argument("-a", "--attribute", action='append', type=self._resource_attribute, default=[], help=(u"attribute definition, " u"attribute_name:" u"attribute_type:" u"attribute_is_required:" u"attribute_type_option_name=" u"attribute_type_option_value:... " u"For example: " u"display_name:string:true:max_length=255")) return parser @classmethod def _resource_attribute(cls, value): config = value.split(":") name = config.pop(0) attrs = {} if config: attrs["type"] = config.pop(0) if config: attrs["required"] = bool(distutils.util.strtobool(config.pop(0))) while config: param, _, value = config.pop(0).partition("=") opts = attrs if param == 'fill': opts = attrs.setdefault("options", {}) try: opts[param] = int(value) except ValueError: try: opts[param] = float(value) except ValueError: opts[param] = value return (name, attrs) def take_action(self, parsed_args): resource_type = {'name': parsed_args.name} if parsed_args.attribute: resource_type['attributes'] = dict(parsed_args.attribute) res = utils.get_client(self).resource_type.create( resource_type=resource_type) utils.format_resource_type(res) return self.dict2columns(res) class CliResourceTypeUpdate(CliResourceTypeCreate): def get_parser(self, prog_name): parser = super(CliResourceTypeUpdate, self).get_parser(prog_name) parser.add_argument("-r", "--remove-attribute", action='append', default=[], help=u"attribute name") return parser def take_action(self, parsed_args): operations = [] if parsed_args.attribute: for name, attrs in parsed_args.attribute: operations.append({'op': 'add', 'path': '/attributes/%s' % name, 'value': attrs}) if parsed_args.remove_attribute: for name in parsed_args.remove_attribute: operations.append({'op': 'remove', 'path': '/attributes/%s' % name}) res = utils.get_client(self).resource_type.update( parsed_args.name, operations) utils.format_resource_type(res) return self.dict2columns(res) class CliResourceTypeShow(show.ShowOne): """Show a resource type""" def get_parser(self, prog_name): parser = super(CliResourceTypeShow, self).get_parser(prog_name) parser.add_argument("name", help="name of the resource type") return parser def take_action(self, parsed_args): res = utils.get_client(self).resource_type.get(name=parsed_args.name) utils.format_resource_type(res) return self.dict2columns(res) class CliResourceTypeDelete(command.Command): """Delete a resource type""" def get_parser(self, prog_name): parser = super(CliResourceTypeDelete, self).get_parser(prog_name) parser.add_argument("name", help="name of the resource type") return parser def take_action(self, parsed_args): utils.get_client(self).resource_type.delete(parsed_args.name) gnocchiclient-7.0.1/gnocchiclient/v1/archive_policy.py0000664000372000037200000000362113225150105023677 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import ujson from gnocchiclient.v1 import base class ArchivePolicyManager(base.Manager): url = "v1/archive_policy/" def list(self): """List archive policies """ return self._get(self.url).json() def get(self, name): """Get an archive policy :param name: Name of the archive policy :type name: str """ return self._get(self.url + name).json() def create(self, archive_policy): """Create an archive policy :param archive_policy: the archive policy :type archive_policy: dict """ return self._post( self.url, headers={'Content-Type': "application/json"}, data=ujson.dumps(archive_policy)).json() def update(self, name, archive_policy): """Update an archive policy :param name: the name of archive policy :type name: str :param archive_policy: the archive policy :type archive_policy: dict """ return self._patch( self.url + '/' + name, headers={'Content-Type': "application/json"}, data=ujson.dumps(archive_policy)).json() def delete(self, name): """Delete an archive policy :param name: Name of the archive policy :type name: str """ self._delete(self.url + name) gnocchiclient-7.0.1/gnocchiclient/v1/status_cli.py0000664000372000037200000000210713225150105023047 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cliff import show from gnocchiclient import utils class CliStatusShow(show.ShowOne): """Show the status of measurements processing""" def take_action(self, parsed_args): status = utils.get_client(self).status.get() return self.dict2columns({ "storage/total number of measures to process": status['storage']['summary']['measures'], "storage/number of metric having measures to process": status['storage']['summary']['metrics'], }) gnocchiclient-7.0.1/gnocchiclient/v1/metric.py0000664000372000037200000003303513225150105022164 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime import uuid from debtcollector import removals import iso8601 import ujson from gnocchiclient import utils from gnocchiclient.v1 import base class MetricManager(base.Manager): metric_url = "v1/metric/" resource_url = "v1/resource/generic/%s/metric/" metric_batch_url = "v1/batch/metrics/measures" resources_batch_url = "v1/batch/resources/metrics/measures" def list(self, limit=None, marker=None, sorts=None): """List metrics :param limit: maximum number of resources to return :type limit: int :param marker: the last item of the previous page; we return the next results after this value. :type marker: str :param sorts: list of resource attributes to order by. (example ["user_id:desc-nullslast", "project_id:asc"] :type sorts: list of str """ params = utils.build_pagination_options(False, False, limit, marker, sorts) metrics = [] page_url = "%s?%s" % (self.metric_url[:-1], utils.dict_to_querystring(params)) while page_url: page = self._get(page_url) metrics.extend(page.json()) if limit is None or len(metrics) < limit: page_url = page.links.get("next", {'url': None})['url'] else: break return metrics @staticmethod def _ensure_metric_is_uuid(metric, attribute="resource_id"): try: uuid.UUID(metric) except ValueError: raise TypeError("%s is required to get a metric by name" % attribute) def get(self, metric, resource_id=None): """Get an metric :param metric: ID or Name of the metric :type metric: str :param resource_id: ID of the resource (required to get a metric by name) :type resource_id: str """ if resource_id is None: self._ensure_metric_is_uuid(metric) url = self.metric_url + metric else: url = (self.resource_url % resource_id) + metric return self._get(url).json() # FIXME(jd): This is what create will be after debtcollector warnings have # been removed. We provide it right now for the benchmark code, that can't # pickle a debtcollector-ed method. def _create_new(self, name=None, archive_policy_name=None, resource_id=None, unit=None): """Create an metric :param name: Metric name. :type name: str :param archive_policy_name: Archive policy name. :type archive_policy_name: str :param resource_id: The resource ID to attach the metric to. :type resource_id: str :param unit: The unit of the metric. :type unit: str """ metric = {} if name is not None: metric["name"] = name if archive_policy_name is not None: metric["archive_policy_name"] = archive_policy_name if unit is not None: metric["unit"] = unit if resource_id is None: return self._post( self.metric_url, headers={'Content-Type': "application/json"}, data=ujson.dumps(metric)).json() if name is None: raise TypeError( "Metric name is required if resource_id is set") return self._post( self.resource_url % resource_id, headers={'Content-Type': "application/json"}, data=ujson.dumps({name: metric})).json()[0] # FIXME(jd): remove refetch_metric when LP#1497171 is fixed @removals.removed_kwarg("refetch_metric") @removals.removed_kwarg("metric") def create(self, metric=None, refetch_metric=True, name=None, archive_policy_name=None, resource_id=None, unit=None): """Create an metric :param name: Metric name. :type name: str :param archive_policy_name: Archive policy name. :type archive_policy_name: str :param resource_id: The resource ID to attach the metric to. :type resource_id: str :param unit: The unit of the metric. :type unit: str """ if metric is None: metric = {} if name is not None: metric["name"] = name if archive_policy_name is not None: metric["archive_policy_name"] = archive_policy_name if resource_id is not None: metric["resource_id"] = resource_id if unit is not None: metric["unit"] = unit resource_id = metric.get('resource_id') if resource_id is None: metric = self._post( self.metric_url, headers={'Content-Type': "application/json"}, data=ujson.dumps(metric)).json() # FIXME(sileht): create and get have a # different output: LP#1497171 if refetch_metric: return self.get(metric["id"]) return metric metric_name = metric.get('name') if metric_name is None: raise TypeError("metric_name is required if resource_id is set") del metric['resource_id'] metric = {metric_name: metric} metric = self._post( self.resource_url % resource_id, headers={'Content-Type': "application/json"}, data=ujson.dumps(metric)) return self.get(metric_name, resource_id) def delete(self, metric, resource_id=None): """Delete an metric :param metric: ID or Name of the metric :type metric: str :param resource_id: ID of the resource (required to get a metric by name) :type resource_id: str """ if resource_id is None: self._ensure_metric_is_uuid(metric) url = self.metric_url + metric else: url = self.resource_url % resource_id + metric self._delete(url) def add_measures(self, metric, measures, resource_id=None): """Add measurements to a metric :param metric: ID or Name of the metric :type metric: str :param resource_id: ID of the resource (required to get a metric by name) :type resource_id: str :param measures: measurements :type measures: list of dict(timestamp=timestamp, value=float) """ if resource_id is None: self._ensure_metric_is_uuid(metric) url = self.metric_url + metric + "/measures" else: url = self.resource_url % resource_id + metric + "/measures" return self._post( url, headers={'Content-Type': "application/json"}, data=ujson.dumps(measures)) def batch_metrics_measures(self, measures): """Add measurements to metrics :param measures: measurements :type dict(metric_id: list of dict(timestamp=timestamp, value=float)) """ return self._post( self.metric_batch_url, headers={'Content-Type': "application/json"}, data=ujson.dumps(measures)) def batch_resources_metrics_measures(self, measures, create_metrics=False): """Add measurements to named metrics if resources :param measures: measurements :type dict(resource_id: dict(metric_name: list of dict(timestamp=timestamp, value=float))) """ return self._post( self.resources_batch_url, headers={'Content-Type': "application/json"}, data=ujson.dumps(measures), params=dict(create_metrics=create_metrics)) def get_measures(self, metric, start=None, stop=None, aggregation=None, granularity=None, resource_id=None, refresh=False, resample=None, **kwargs): """Get measurements of a metric :param metric: ID or Name of the metric :type metric: str :param start: beginning of the period :type start: timestamp :param stop: end of the period :type stop: timestamp :param aggregation: aggregation to retrieve :type aggregation: str :param granularity: granularity to retrieve (in seconds) :type granularity: int :param resource_id: ID of the resource (required to get a metric by name) :type resource_id: str :param refresh: force aggregation of all known measures :type refresh: bool :param resample: resample measures to new granularity :type resample: float All other arguments are arguments are dedicated to custom aggregation method passed as-is to the Gnocchi. """ if isinstance(start, datetime.datetime): start = start.isoformat() if isinstance(stop, datetime.datetime): stop = stop.isoformat() params = dict(start=start, stop=stop, aggregation=aggregation, granularity=granularity, refresh=refresh, resample=resample) params.update(kwargs) if resource_id is None: self._ensure_metric_is_uuid(metric) url = self.metric_url + metric + "/measures" else: url = self.resource_url % resource_id + metric + "/measures" measures = self._get(url, params=params).json() return [(iso8601.parse_date(ts), g, value) for ts, g, value in measures] def aggregation(self, metrics, query=None, start=None, stop=None, aggregation=None, reaggregation=None, granularity=None, needed_overlap=None, resource_type="generic", groupby=None, refresh=False, resample=None, fill=None): """Get measurements of an aggregated metrics :param metrics: IDs of metric or metric name :type metric: list or str :param query: The query dictionary :type query: dict :param start: beginning of the period :type start: timestamp :param stop: end of the period :type stop: timestamp :param aggregation: granularity aggregation function to retrieve :type aggregation: str :param reaggregation: groupby aggregation function to retrieve :type reaggregation: str :param granularity: granularity to retrieve (in seconds) :type granularity: int :param needed_overlap: percent of datapoints in each metrics required :type needed_overlap: float :param resource_type: type of resource for the query :type resource_type: str :param groupby: list of attribute to group by :type groupby: list :param refresh: force aggregation of all known measures :type refresh: bool :param resample: resample measures to new granularity :type resample: float :param fill: value to use when backfilling missing datapoints :type fill: float or 'null' See Gnocchi REST API documentation for the format of *query dictionary* http://docs.openstack.org/developer/gnocchi/rest.html#searching-for-resources """ if isinstance(start, datetime.datetime): start = start.isoformat() if isinstance(stop, datetime.datetime): stop = stop.isoformat() params = dict(start=start, stop=stop, aggregation=aggregation, reaggregation=reaggregation, granularity=granularity, needed_overlap=needed_overlap, groupby=groupby, refresh=refresh, resample=resample, fill=fill) if query is None: for metric in metrics: self._ensure_metric_is_uuid(metric) params['metric'] = metrics measures = self._get("v1/aggregation/metric", params=params).json() else: if isinstance(query, dict): measures = self._post( "v1/aggregation/resource/%s/metric/%s?%s" % ( resource_type, metrics, utils.dict_to_querystring(params)), headers={'Content-Type': "application/json"}, data=ujson.dumps(query)).json() else: params['filter'] = query measures = self._post( "v1/aggregation/resource/%s/metric/%s?%s" % ( resource_type, metrics, utils.dict_to_querystring(params)), headers={'Content-Type': "application/json"}).json() if groupby is None: return [(iso8601.parse_date(ts), g, value) for ts, g, value in measures] for group in measures: group["measures"] = [ (iso8601.parse_date(ts), g, value) for ts, g, value in group["measures"] ] return measures gnocchiclient-7.0.1/gnocchiclient/v1/resource_type.py0000664000372000037200000000370013225150105023565 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import ujson from gnocchiclient.v1 import base class ResourceTypeManager(base.Manager): url = "v1/resource_type/" def list(self): """List resource types.""" return self._get(self.url).json() def create(self, resource_type): """Create a resource type :param resource_type: resource type :type resource_type: dict """ return self._post( self.url, headers={'Content-Type': "application/json"}, data=ujson.dumps(resource_type)).json() def get(self, name): """Get a resource type :param name: name of the resource type :type name: str """ return self._get(self.url + name, headers={'Content-Type': "application/json"}).json() def delete(self, name): """Delete a resource type :param resource_type: Resource type :type resource_type: dict """ self._delete(self.url + name) def update(self, name, operations): """Update a resource type :param name: name of the resource type :type name: str :param operations: operations in RFC6902 format :type name: list """ return self._patch( self.url + name, headers={'Content-Type': "application/json-patch+json"}, data=ujson.dumps(operations)).json() gnocchiclient-7.0.1/gnocchiclient/v1/archive_policy_rule.py0000664000372000037200000000356713225150105024737 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import ujson from gnocchiclient.v1 import base class ArchivePolicyRuleManager(base.Manager): url = "v1/archive_policy_rule/" def list(self): """List archive policy rules """ return self._get(self.url).json() def get(self, name): """Get an archive policy rules :param name: Name of the archive policy rule :type name: str """ return self._get(self.url + name).json() def create(self, archive_policy_rule): """Create an archive policy rule """ return self._post( self.url, headers={'Content-Type': "application/json"}, data=ujson.dumps(archive_policy_rule)).json() def update(self, name, new_name): """Update an archive policy rule :param name: the name of archive policy rule :type name: str :param new_name: the new name of archive policy rule :type new_name: str """ return self._patch( self.url + '/' + name, headers={'Content-Type': "application/json"}, data=ujson.dumps({'name': new_name})).json() def delete(self, name): """Delete an archive policy rule :param name: Name of the archive policy rule :type name: str """ self._delete(self.url + name) gnocchiclient-7.0.1/gnocchiclient/v1/aggregates.py0000664000372000037200000000720513225150105023012 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime import iso8601 import ujson from gnocchiclient import utils from gnocchiclient.v1 import base class AggregatesManager(base.Manager): def fetch(self, operations, search=None, resource_type='generic', start=None, stop=None, granularity=None, needed_overlap=None, groupby=None, fill=None, details=False): """Get measurements of an aggregated metrics :param operations: operations :type operations: list or str :param start: beginning of the period :type start: timestamp :param stop: end of the period :type stop: timestamp :param granularity: granularity to retrieve (in seconds) :type granularity: int :param needed_overlap: percent of datapoints in each metrics required :type needed_overlap: float :param groupby: list of attribute to group by :type groupby: list :param fill: value to use when backfilling missing datapoints :type fill: float or 'null' :param details: also returns the list of metrics or resources associated to the operations :type details: boolean See Gnocchi REST API documentation for the format of *query dictionary* http://docs.openstack.org/developer/gnocchi/rest.html#aggregates """ if isinstance(start, datetime.datetime): start = start.isoformat() if isinstance(stop, datetime.datetime): stop = stop.isoformat() params = dict(start=start, stop=stop, granularity=granularity, needed_overlap=needed_overlap, fill=fill, details=details) data = dict(operations=operations) if search is not None: data["search"] = search data["resource_type"] = resource_type params["groupby"] = groupby aggregates = self._post("v1/aggregates?%s" % ( utils.dict_to_querystring(params)), headers={'Content-Type': "application/json"}, data=ujson.dumps(data)).json() if search is not None and groupby is not None: for group in aggregates: self._convert_dates(group["measures"]["measures"]) else: self._convert_dates(aggregates["measures"]) return aggregates @classmethod def _convert_dates(cls, data): # NOTE(sileht): browse to aggregates measures dict tree and convert # date when we found timeseries, dict can looks like # {"aggregated": ...}, {"metric_id": {"agg": ...}} or # {"resource_id": {"metric_name": {"agg": ...}}} for key in data: if isinstance(data[key], list): data[key] = [(iso8601.parse_date(ts), g, value) for ts, g, value in data[key]] elif isinstance(data[key], dict): cls._convert_dates(data[key]) else: raise RuntimeError("Unexpected aggregates API output %s" % data[key]) gnocchiclient-7.0.1/gnocchiclient/v1/aggregates_cli.py0000664000372000037200000000670313225150105023643 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cliff import lister from gnocchiclient import utils class CliAggregates(lister.Lister): """Get measurements of aggregated metrics""" COLS = ('name', 'timestamp', 'granularity', 'value') def get_parser(self, prog_name): parser = super(CliAggregates, self).get_parser(prog_name) parser.add_argument("operations", help="Operations to apply to time series") utils.add_query_argument("search", parser, nargs="?", default=None) parser.add_argument("--resource-type", default="generic", help="Resource type to query"), parser.add_argument("--start", help="beginning of the period") parser.add_argument("--stop", help="end of the period") parser.add_argument("--granularity", help="granularity to retrieve") parser.add_argument("--needed-overlap", type=float, help=("percentage of overlap across datapoints")) parser.add_argument("--groupby", action='append', help="Attribute to use to group resources"), parser.add_argument("--fill", help=("Value to use when backfilling timestamps " "with missing values in a subset of series. " "Value should be a float or 'null'.")) return parser def take_action(self, parsed_args): aggregates = utils.get_client(self).aggregates.fetch( operations=parsed_args.operations, resource_type=parsed_args.resource_type, search=parsed_args.search, start=parsed_args.start, stop=parsed_args.stop, granularity=parsed_args.granularity, needed_overlap=parsed_args.needed_overlap, groupby=parsed_args.groupby, ) if parsed_args.search and parsed_args.groupby: ms = [] for g in aggregates: group_name = ", ".join("%s: %s" % (k, g['group'][k]) for k in sorted(g['group'])) for row in self.flatten_measures(g["measures"]["measures"]): ms.append((group_name, ) + row) return ('group',) + self.COLS, ms return self.COLS, list(self.flatten_measures(aggregates["measures"])) @classmethod def flatten_measures(cls, data, labels=None): if labels is None: labels = tuple() for key in data: if isinstance(data[key], list): name = "/".join(labels + (key, )) for ts, g, value in data[key]: yield (name, ts.isoformat(), g, value) else: for row in cls.flatten_measures(data[key], labels + (key,)): yield row gnocchiclient-7.0.1/gnocchiclient/v1/capabilities_cli.py0000664000372000037200000000151013225150105024152 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cliff import show from gnocchiclient import utils class CliCapabilitiesList(show.ShowOne): """List capabilities""" def take_action(self, parsed_args): caps = utils.get_client(self).capabilities.list() return self.dict2columns(caps) gnocchiclient-7.0.1/gnocchiclient/v1/resource_cli.py0000664000372000037200000002453513225150105023364 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import distutils.util from cliff import command from cliff import lister from cliff import show from gnocchiclient import exceptions from gnocchiclient import utils class CliResourceList(lister.Lister): """List resources""" COLS = ('id', 'type', 'project_id', 'user_id', 'original_resource_id', 'started_at', 'ended_at', 'revision_start', 'revision_end') def get_parser(self, prog_name, history=True): parser = super(CliResourceList, self).get_parser(prog_name) parser.add_argument("--details", action='store_true', help="Show all attributes of generic resources"), if history: parser.add_argument("--history", action='store_true', help="Show history of the resources"), parser.add_argument("--limit", type=int, metavar="", help="Number of resources to return " "(Default is server default)") parser.add_argument("--marker", metavar="", help="Last item of the previous listing. " "Return the next results after this value") parser.add_argument("--sort", action="append", metavar="", help="Sort of resource attribute " "(example: user_id:desc-nullslast") parser.add_argument("--type", "-t", dest="resource_type", default="generic", help="Type of resource") return parser def _list2cols(self, resources): """Return a formatted list of resources.""" if not resources: return self.COLS, [] cols = list(self.COLS) for k in resources[0]: if k not in cols: cols.append(k) if 'creator' in cols: cols.remove('created_by_user_id') cols.remove('created_by_project_id') return utils.list2cols(cols, resources) def take_action(self, parsed_args): resources = utils.get_client(self).resource.list( resource_type=parsed_args.resource_type, **utils.get_pagination_options(parsed_args)) # Do not dump metrics because it makes the list way too long for r in resources: del r['metrics'] return self._list2cols(resources) class CliResourceHistory(CliResourceList): """Show the history of a resource""" def get_parser(self, prog_name): parser = super(CliResourceHistory, self).get_parser(prog_name, history=False) parser.add_argument("resource_id", help="ID of a resource") return parser def take_action(self, parsed_args): resources = utils.get_client(self).resource.history( resource_type=parsed_args.resource_type, resource_id=parsed_args.resource_id, **utils.get_pagination_options(parsed_args)) if parsed_args.formatter == 'table': return self._list2cols(list(map(normalize_metrics, resources))) return self._list2cols(resources) class CliResourceSearch(CliResourceList): """Search resources with specified query rules""" def get_parser(self, prog_name): parser = super(CliResourceSearch, self).get_parser(prog_name) utils.add_query_argument("query", parser) return parser def take_action(self, parsed_args): resources = utils.get_client(self).resource.search( resource_type=parsed_args.resource_type, query=parsed_args.query, **utils.get_pagination_options(parsed_args)) # Do not dump metrics because it makes the list way too long for r in resources: del r['metrics'] return self._list2cols(resources) def normalize_metrics(res): res['metrics'] = "\n".join(sorted( ["%s: %s" % (name, _id) for name, _id in res['metrics'].items()])) return res class CliResourceShow(show.ShowOne): """Show a resource""" def get_parser(self, prog_name): parser = super(CliResourceShow, self).get_parser(prog_name) parser.add_argument("--type", "-t", dest="resource_type", default="generic", help="Type of resource") parser.add_argument("resource_id", help="ID of a resource") return parser def take_action(self, parsed_args): res = utils.get_client(self).resource.get( resource_type=parsed_args.resource_type, resource_id=parsed_args.resource_id) if parsed_args.formatter == 'table': normalize_metrics(res) return self.dict2columns(res) class CliResourceCreate(show.ShowOne): """Create a resource""" def get_parser(self, prog_name): parser = super(CliResourceCreate, self).get_parser(prog_name) parser.add_argument("--type", "-t", dest="resource_type", default="generic", help="Type of resource") parser.add_argument("resource_id", help="ID of the resource") parser.add_argument("-a", "--attribute", action='append', default=[], help=("name and value of an attribute " "separated with a ':'")) parser.add_argument("-m", "--add-metric", action='append', default=[], help="name:id of a metric to add"), parser.add_argument( "-n", "--create-metric", action='append', default=[], help="name:archive_policy_name of a metric to create"), return parser def _resource_from_args(self, parsed_args, update=False): # Get the resource type to set the correct type rt_attrs = utils.get_client(self).resource_type.get( name=parsed_args.resource_type)['attributes'] resource = {} if not update: resource['id'] = parsed_args.resource_id if parsed_args.attribute: for attr in parsed_args.attribute: attr, __, value = attr.partition(":") attr_type = rt_attrs.get(attr, {}).get('type') if attr_type == "number": value = float(value) elif attr_type == "bool": value = bool(distutils.util.strtobool(value)) resource[attr] = value if (parsed_args.add_metric or parsed_args.create_metric or (update and parsed_args.delete_metric)): if update: r = utils.get_client(self).resource.get( parsed_args.resource_type, parsed_args.resource_id) default = r['metrics'] for metric_name in parsed_args.delete_metric: try: del default[metric_name] except KeyError: raise exceptions.MetricNotFound( message="Metric name %s not found" % metric_name) else: default = {} resource['metrics'] = default for metric in parsed_args.add_metric: name, _, value = metric.partition(":") resource['metrics'][name] = value for metric in parsed_args.create_metric: name, _, value = metric.partition(":") if value is "": resource['metrics'][name] = {} else: resource['metrics'][name] = {'archive_policy_name': value} return resource def take_action(self, parsed_args): resource = self._resource_from_args(parsed_args) res = utils.get_client(self).resource.create( resource_type=parsed_args.resource_type, resource=resource) if parsed_args.formatter == 'table': normalize_metrics(res) return self.dict2columns(res) class CliResourceUpdate(CliResourceCreate): """Update a resource""" def get_parser(self, prog_name): parser = super(CliResourceUpdate, self).get_parser(prog_name) parser.add_argument("-d", "--delete-metric", action='append', default=[], help="Name of a metric to delete"), return parser def take_action(self, parsed_args): resource = self._resource_from_args(parsed_args, update=True) res = utils.get_client(self).resource.update( resource_type=parsed_args.resource_type, resource_id=parsed_args.resource_id, resource=resource) if parsed_args.formatter == 'table': normalize_metrics(res) return self.dict2columns(res) class CliResourceDelete(command.Command): """Delete a resource""" def get_parser(self, prog_name): parser = super(CliResourceDelete, self).get_parser(prog_name) parser.add_argument("resource_id", help="ID of the resource") return parser def take_action(self, parsed_args): utils.get_client(self).resource.delete(parsed_args.resource_id) class CliResourceBatchDelete(show.ShowOne): """Delete a batch of resources based on attribute values""" def get_parser(self, prog_name): parser = super(CliResourceBatchDelete, self).get_parser(prog_name) parser.add_argument("--type", "-t", dest="resource_type", default="generic", help="Type of resource") utils.add_query_argument("query", parser) return parser def take_action(self, parsed_args): res = utils.get_client(self).resource.batch_delete( resource_type=parsed_args.resource_type, query=parsed_args.query) return self.dict2columns(res) gnocchiclient-7.0.1/gnocchiclient/v1/resource.py0000664000372000037200000001534513225150105022534 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import ujson from gnocchiclient import utils from gnocchiclient.v1 import base class ResourceManager(base.Manager): url = "v1/resource/" def list(self, resource_type="generic", details=False, history=False, limit=None, marker=None, sorts=None): """List resources :param resource_type: Type of the resource :type resource_type: str :param details: Show all attributes of resources :type details: bool :param history: Show the history of resources :type history: bool :param limit: maximum number of resources to return :type limit: int :param marker: the last item of the previous page; we return the next results after this value. :type marker: str :param sorts: list of resource attributes to order by. (example ["user_id:desc-nullslast", "project_id:asc"] :type sorts: list of str """ params = utils.build_pagination_options( details, history, limit, marker, sorts) url = "%s%s?%s" % (self.url, resource_type, utils.dict_to_querystring(params)) return self._get(url).json() def get(self, resource_type, resource_id, history=False): """Get a resource :param resource_type: Type of the resource :type resource_type: str :param resource_id: ID of the resource :type resource_id: str :param history: Show the history of the resource :type history: bool """ history = "/history" if history else "" url = self.url + "%s/%s%s" % (resource_type, resource_id, history) return self._get(url).json() def history(self, resource_type, resource_id, details=False, limit=None, marker=None, sorts=None): """Get a resource :param resource_type: Type of the resource :type resource_type: str :param resource_id: ID of the resource :type resource_id: str :param details: Show all attributes of resources :type details: bool :param limit: maximum number of resources to return :type limit: int :param marker: the last item of the previous page; we returns the next results after this value. :type marker: str :param sorts: list of resource attributes to order by. (example ["user_id:desc-nullslast", "project_id:asc"] :type sorts: list of str """ params = utils.build_pagination_options(details, False, limit, marker, sorts) url = "%s%s/%s/history?%s" % (self.url, resource_type, resource_id, utils.dict_to_querystring(params)) return self._get(url).json() def create(self, resource_type, resource): """Create a resource :param resource_type: Type of the resource :type resource_type: str :param resource: Attribute of the resource :type resource: dict """ return self._post( self.url + resource_type, headers={'Content-Type': "application/json"}, data=ujson.dumps(resource)).json() def update(self, resource_type, resource_id, resource): """Update a resource :param resource_type: Type of the resource :type resource_type: str :param resource_id: ID of the resource :type resource_id: str :param resource: Attribute of the resource :type resource: dict """ return self._patch( self.url + resource_type + "/" + resource_id, headers={'Content-Type': "application/json"}, data=ujson.dumps(resource)).json() def delete(self, resource_id): """Delete a resource :param resource_id: ID of the resource :type resource_id: str """ self._delete(self.url + "generic/" + resource_id) def batch_delete(self, query, resource_type="generic"): """Delete a batch of resources based on attribute values :param resource_type: Type of the resource :type resource_type: str """ if isinstance(query, dict): return self._delete( self.url + resource_type + "/", headers={'Content-Type': "application/json"}, data=ujson.dumps(query)).json() return self._delete( self.url + resource_type + "/?filter=" + query, headers={'Content-Type': "application/json"}).json() def search(self, resource_type="generic", query=None, details=False, history=False, limit=None, marker=None, sorts=None): """List resources :param resource_type: Type of the resource :type resource_type: str :param query: The query dictionary :type query: dict :param details: Show all attributes of resources :type details: bool :param history: Show the history of resources :type history: bool :param limit: maximum number of resources to return :type limit: int :param marker: the last item of the previous page; we returns the next results after this value. :type marker: str :param sorts: list of resource attributes to order by. (example ["user_id:desc-nullslast", "project_id:asc"] :type sorts: list of str See Gnocchi REST API documentation for the format of *query dictionary* http://gnocchi.xyz/rest.html#searching-for-resources """ query = query or {} params = utils.build_pagination_options( details, history, limit, marker, sorts) url = "v1/search/resource/%s?%%s" % resource_type if isinstance(query, dict): return self._post( url % utils.dict_to_querystring(params), headers={'Content-Type': "application/json"}, data=ujson.dumps(query)).json() params['filter'] = query return self._post( url % utils.dict_to_querystring(params), headers={'Content-Type': "application/json"}).json() gnocchiclient-7.0.1/gnocchiclient/v1/base.py0000664000372000037200000000333013225150105021606 0ustar travistravis00000000000000# Copyright 2012-2015 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import six class Manager(object): DEFAULT_HEADERS = { "Accept": "application/json, */*", } def __init__(self, client): self.client = client def _set_default_headers(self, kwargs): headers = kwargs.get('headers', {}) for k, v in six.iteritems(self.DEFAULT_HEADERS): if k not in headers: headers[k] = v kwargs['headers'] = headers return kwargs def _get(self, *args, **kwargs): self._set_default_headers(kwargs) return self.client.api.get(*args, **kwargs) def _post(self, *args, **kwargs): self._set_default_headers(kwargs) return self.client.api.post(*args, **kwargs) def _put(self, *args, **kwargs): self._set_default_headers(kwargs) return self.client.api.put(*args, **kwargs) def _patch(self, *args, **kwargs): self._set_default_headers(kwargs) return self.client.api.patch(*args, **kwargs) def _delete(self, *args, **kwargs): self._set_default_headers(kwargs) return self.client.api.delete(*args, **kwargs) gnocchiclient-7.0.1/gnocchiclient/v1/capabilities.py0000664000372000037200000000143213225150105023326 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from gnocchiclient.v1 import base class CapabilitiesManager(base.Manager): cap_url = "v1/capabilities/" def list(self): """List capabilities """ return self._get(self.cap_url).json() gnocchiclient-7.0.1/gnocchiclient/v1/archive_policy_cli.py0000664000372000037200000001116613225150105024531 0ustar travistravis00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cliff import command from cliff import lister from cliff import show from gnocchiclient import utils class CliArchivePolicyList(lister.Lister): """List archive policies""" COLS = ('name', 'back_window', 'definition', 'aggregation_methods') def take_action(self, parsed_args): policies = utils.get_client(self).archive_policy.list() if parsed_args.formatter == 'table': for ap in policies: utils.format_archive_policy(ap) return utils.list2cols(self.COLS, policies) class CliArchivePolicyShow(show.ShowOne): """Show an archive policy""" def get_parser(self, prog_name): parser = super(CliArchivePolicyShow, self).get_parser(prog_name) parser.add_argument("name", help="Name of the archive policy") return parser def take_action(self, parsed_args): ap = utils.get_client(self).archive_policy.get( name=parsed_args.name) if parsed_args.formatter == 'table': utils.format_archive_policy(ap) return self.dict2columns(ap) def archive_policy_definition(string): parts = string.split(",") defs = {} for part in parts: attr, __, value = part.partition(":") if (attr not in ['granularity', 'points', 'timespan'] or value is None): raise ValueError defs[attr] = value if len(defs) < 2: raise ValueError return defs class CliArchivePolicyWriteBase(show.ShowOne): def get_parser(self, prog_name): parser = super(CliArchivePolicyWriteBase, self).get_parser(prog_name) parser.add_argument("name", help="name of the archive policy") parser.add_argument("-d", "--definition", action='append', required=True, type=archive_policy_definition, metavar="", help=("two attributes (separated by ',') of an " "archive policy definition with its name " "and value separated with a ':'")) return parser class CliArchivePolicyCreate(CliArchivePolicyWriteBase): """Create an archive policy""" def get_parser(self, prog_name): parser = super(CliArchivePolicyCreate, self).get_parser(prog_name) parser.add_argument("-b", "--back-window", dest="back_window", type=int, help="back window of the archive policy") parser.add_argument("-m", "--aggregation-method", action="append", dest="aggregation_methods", help="aggregation method of the archive policy") return parser def take_action(self, parsed_args): archive_policy = utils.dict_from_parsed_args( parsed_args, ['name', 'back_window', 'aggregation_methods', 'definition']) ap = utils.get_client(self).archive_policy.create( archive_policy=archive_policy) if parsed_args.formatter == 'table': utils.format_archive_policy(ap) return self.dict2columns(ap) class CliArchivePolicyUpdate(CliArchivePolicyWriteBase): """Update an archive policy""" def take_action(self, parsed_args): archive_policy = utils.dict_from_parsed_args( parsed_args, ['definition']) ap = self.app.client.archive_policy.update( name=parsed_args.name, archive_policy=archive_policy) if parsed_args.formatter == 'table': utils.format_archive_policy(ap) return self.dict2columns(ap) class CliArchivePolicyDelete(command.Command): """Delete an archive policy""" def get_parser(self, prog_name): parser = super(CliArchivePolicyDelete, self).get_parser(prog_name) parser.add_argument("name", help="Name of the archive policy") return parser def take_action(self, parsed_args): utils.get_client(self).archive_policy.delete(name=parsed_args.name) gnocchiclient-7.0.1/gnocchiclient/benchmark.py0000664000372000037200000003170613225150105022310 0ustar travistravis00000000000000# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import argparse import datetime import functools import itertools import logging import math import random import time import types from cliff import show import futurist import iso8601 from monotonic import monotonic as now # noqa import six.moves from gnocchiclient import utils from gnocchiclient.v1 import metric_cli LOG = logging.getLogger(__name__) def _pickle_method(m): if m.im_self is None: return getattr, (m.im_class, m.im_func.func_name) else: return getattr, (m.im_self, m.im_func.func_name) six.moves.copyreg.pickle(types.MethodType, _pickle_method) def _positive_non_zero_int(argument_value): if argument_value is None: return None try: value = int(argument_value) except ValueError: msg = "%s must be an integer" % argument_value raise argparse.ArgumentTypeError(msg) if value <= 0: msg = "%s must be greater than 0" % argument_value raise argparse.ArgumentTypeError(msg) return value class StopWatch(object): def __init__(self): self.started_at = now() def elapsed(self): return max(0.0, now() - self.started_at) def measure_job(fn, *args, **kwargs): # because we cannot pickle BenchmarkPool class sw = StopWatch() return fn(*args, **kwargs), sw.elapsed() class BenchmarkPool(futurist.ProcessPoolExecutor): def submit_job(self, times, fn, *args, **kwargs): self.sw = StopWatch() self.times = times return [self.submit(measure_job, fn, *args, **kwargs) for i in six.moves.range(times)] def map_job(self, fn, iterable, **kwargs): r = [] self.times = 0 self.sw = StopWatch() for item in iterable: r.append(self.submit(measure_job, fn, item, **kwargs)) self.times += 1 return r def _log_progress(self, verb): runtime = self.sw.elapsed() done = self.statistics.executed rate = done / runtime if runtime != 0 else 0 LOG.info( "%d/%d, " "total: %.2f seconds, " "rate: %.2f %s/second" % (done, self.times, runtime, rate, verb)) def wait_job(self, verb, futures): while self.statistics.executed != self.times: self._log_progress(verb) time.sleep(0.2) runtime = self.sw.elapsed() self._log_progress(verb) self.shutdown(wait=True) results = [] latencies = [] for f in futures: try: result, latency = f.result() results.append(result) latencies.append(latency) except Exception as e: LOG.error("Error with %s metric: %s" % (verb, e)) latencies = sorted(latencies) return results, runtime, { 'client workers': self._max_workers, verb + ' runtime': "%.2f seconds" % runtime, verb + ' runtime (cumulated)': "%.2f seconds" % sum(latencies), verb + ' executed': self.statistics.executed, verb + ' speed': ( "%.2f %s/s" % ((self.statistics.executed * self._max_workers / sum(latencies)) if runtime != 0 else 0, verb) ), verb + ' failures': self.statistics.failures, verb + ' failures rate': ( "%.2f %%" % ( 100 * self.statistics.failures / float(self.statistics.executed) ) ), verb + ' latency min': min(latencies), verb + ' latency max': max(latencies), verb + ' latency mean': sum(latencies) / len(latencies), verb + ' latency median': self._percentile(latencies, 0.5), verb + ' latency 95%\'ile': self._percentile(latencies, 0.95), verb + ' latency 99%\'ile': self._percentile(latencies, 0.99), verb + ' latency 99.9%\'ile': self._percentile(latencies, 0.999), } @staticmethod def _percentile(sorted_list, percent): # NOTE(sileht): we don't to want depends on numpy if not sorted_list: return None k = (len(sorted_list) - 1) * percent f = math.floor(k) c = math.ceil(k) if f == c: return sorted_list[int(k)] d0 = sorted_list[int(f)] * (c - k) d1 = sorted_list[int(c)] * (k - f) return d0 + d1 class CliBenchmarkBase(show.ShowOne): def get_parser(self, prog_name): parser = super(CliBenchmarkBase, self).get_parser(prog_name) parser.add_argument("--workers", "-w", default=None, type=_positive_non_zero_int, help="Number of workers to use") return parser class CliBenchmarkMetricShow(CliBenchmarkBase, metric_cli.CliMetricWithResourceID): """Do benchmark testing of metric show""" def get_parser(self, prog_name): parser = super(CliBenchmarkMetricShow, self).get_parser(prog_name) parser.add_argument("metric", nargs='+', help="ID or name of the metrics") parser.add_argument("--count", "-n", required=True, type=_positive_non_zero_int, help="Number of metrics to get") return parser def take_action(self, parsed_args): pool = BenchmarkPool(parsed_args.workers) LOG.info("Getting metrics") futures = pool.map_job(self.app.client.metric.get, parsed_args.metric * parsed_args.count, resource_id=parsed_args.resource_id) result, runtime, stats = pool.wait_job("show", futures) return self.dict2columns(stats) class CliBenchmarkMetricCreate(CliBenchmarkBase, metric_cli.CliMetricCreateBase): """Do benchmark testing of metric creation""" def get_parser(self, prog_name): parser = super(CliBenchmarkMetricCreate, self).get_parser(prog_name) parser.add_argument("--count", "-n", required=True, type=_positive_non_zero_int, help="Number of metrics to create") parser.add_argument("--keep", "-k", action='store_true', help="Keep created metrics") return parser def take_action(self, parsed_args): pool = BenchmarkPool(parsed_args.workers) LOG.info("Creating metrics") futures = pool.submit_job( parsed_args.count, self.app.client.metric._create_new, archive_policy_name=parsed_args.archive_policy_name, resource_id=parsed_args.resource_id) created_metrics, runtime, stats = pool.wait_job("create", futures) if not parsed_args.keep: LOG.info("Deleting metrics") pool = BenchmarkPool(parsed_args.workers) futures = pool.map_job(self.app.client.metric.delete, [m['id'] for m in created_metrics]) _, runtime, dstats = pool.wait_job("delete", futures) stats.update(dstats) return self.dict2columns(stats) class CliBenchmarkMeasuresAdd(CliBenchmarkBase, metric_cli.CliMeasuresAddBase): """Do benchmark testing of adding measurements""" def get_parser(self, prog_name): parser = super(CliBenchmarkMeasuresAdd, self).get_parser(prog_name) parser.add_argument("--count", "-n", required=True, type=_positive_non_zero_int, help="Number of total measures to send") parser.add_argument("--batch", "-b", default=1, type=_positive_non_zero_int, help="Number of measures to send in each batch") parser.add_argument("--timestamp-start", "-s", default=( datetime.datetime.now(tz=iso8601.iso8601.UTC) - datetime.timedelta(days=365)), type=utils.parse_date, help="First timestamp to use") parser.add_argument("--timestamp-end", "-e", default=( datetime.datetime.now(tz=iso8601.iso8601.UTC)), type=utils.parse_date, help="Last timestamp to use") parser.add_argument("--wait", default=False, action='store_true', help="Wait for all measures to be processed") return parser def take_action(self, parsed_args): pool = BenchmarkPool(parsed_args.workers) LOG.info("Sending measures") if parsed_args.timestamp_end <= parsed_args.timestamp_start: raise ValueError("End timestamp must be after start timestamp") # If batch size is bigger than the number of measures to send, we # reduce it to make sure we send something. if parsed_args.batch > parsed_args.count: parsed_args.batch = parsed_args.count start = int(parsed_args.timestamp_start.strftime("%s")) end = int(parsed_args.timestamp_end.strftime("%s")) count = parsed_args.batch if (end - start) < count: raise ValueError( "The specified time range is not large enough " "for the number of points") random_values = (random.randint(- 2 ** 32, 2 ** 32) for _ in six.moves.range(count)) measures = [{"timestamp": ts, "value": v} for ts, v in six.moves.zip( six.moves.range(start, end, (end - start) // count), random_values)] times = parsed_args.count // parsed_args.batch futures = pool.map_job(functools.partial( self.app.client.metric.add_measures, parsed_args.metric), itertools.repeat(measures, times), resource_id=parsed_args.resource_id) _, runtime, stats = pool.wait_job("push", futures) stats['measures per request'] = parsed_args.batch stats['measures push speed'] = ( "%.2f push/s" % ( parsed_args.batch * float(stats['push speed'][:-7]) ) ) if parsed_args.wait: sw = StopWatch() while True: status = self.app.client.status.get() remaining = int(status['storage']['summary']['measures']) if remaining == 0: stats['extra wait to process measures'] = ( "%s seconds" % sw.elapsed() ) break else: LOG.info( "Remaining measures to be processed: %d" % remaining) time.sleep(1) return self.dict2columns(stats) class CliBenchmarkMeasuresShow(CliBenchmarkBase, metric_cli.CliMeasuresShow): """Do benchmark testing of measurements show""" def get_parser(self, prog_name): parser = super(CliBenchmarkMeasuresShow, self).get_parser(prog_name) parser.add_argument("--count", "-n", required=True, type=_positive_non_zero_int, help="Number of total measures to send") return parser def take_action(self, parsed_args): pool = BenchmarkPool(parsed_args.workers) LOG.info("Getting measures") futures = pool.submit_job(parsed_args.count, self.app.client.metric.get_measures, metric=parsed_args.metric, resource_id=parsed_args.resource_id, aggregation=parsed_args.aggregation, start=parsed_args.start, stop=parsed_args.stop) result, runtime, stats = pool.wait_job("show", futures) stats['measures per request'] = len(result[0]) return self.dict2columns(stats) gnocchiclient-7.0.1/LICENSE0000664000372000037200000002363713225150105016204 0ustar travistravis00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. gnocchiclient-7.0.1/ChangeLog0000664000372000037200000000405613225150244016747 0ustar travistravis00000000000000CHANGES ======= 7.0.1 ----- * Set the Cliff namespace * Use Python 3 for docs and pep8 test target * resource-type: fixed attributes specific options of resource-type * Remove outdated tox-travis configuration 7.0.0 ----- * Pass explicit kwargs to metric.create * Add "server version" client * tests: fix tests not specifying UTC * metric: reduce output on metric show * tests: fix start/stop timestamp not being in UTC * Only run tests on pull requests 6.0.0 ----- * aggregates: new output parsing * Remove client side query parsing * tests: use a random metric name * tests: use a fixture to fake TZ in environ * Show and parse timestamps in local timezone by default * shell: workaround missing OS\_AUTH\_TYPE when using Keystone auth * add aggregates API * Make build\_pagination\_options return a dict * Update mailmap entries for sileht and gord * tests: move to pytest * metric: add support for metric pagination by default * Fix osc client * Clear .gitignore * Use pretty\_tox to run tests * Remove tempest dependency * metric\_cli: convert datetime before returning * Add archive-policy-rule update 5.0.0 ----- * Fix exception name for keystoneauth1 * metric: parse ISO8601 as datetime Python objects * benchmark: generate just enough point for a batch * benchmark: print the number of measures per request on show * Remove useless comment * Use cumulated runtime to compute op/s * Use Travis to upload releases * Add missing dependency on pyparsing 4.0.0 ----- * fix benchmark stopwatch * Translate keystoneauth1 exceptions in gnocchiclient exceptions * Remove \_\_version\_\_ from gnocchiclient module * Remove deprecated compatibility session options in client * Remove wrong comment * Replace oslotest by testtools 3.3.1 ----- * Add missing requirement * Remove ugly Travis/Docker workaround 3.3.0 ----- * Don't require osc\_lib * Drop oslo.utils * Drop oslo.serialization 3.2.0 ----- * Add ResourceTypeAlreadyExists exception * fix use of unicode breaking python 2 help output * Install pifpaf with gnocchi flavor * travis: allow Docker to write in home directory gnocchiclient-7.0.1/gnocchiclient.egg-info/0000775000372000037200000000000013225150244021473 5ustar travistravis00000000000000gnocchiclient-7.0.1/gnocchiclient.egg-info/top_level.txt0000664000372000037200000000001613225150244024222 0ustar travistravis00000000000000gnocchiclient gnocchiclient-7.0.1/gnocchiclient.egg-info/not-zip-safe0000664000372000037200000000000113225150244023721 0ustar travistravis00000000000000 gnocchiclient-7.0.1/gnocchiclient.egg-info/requires.txt0000664000372000037200000000045013225150244024072 0ustar travistravis00000000000000pbr>=1.4 cliff>1.16.0 ujson keystoneauth1>=2.0.0 six futurist iso8601 monotonic python-dateutil debtcollector [doc] sphinx!=1.2.0,!=1.3b1,>=1.1.2 sphinx_rtd_theme openstack-doc-tools>=1.0.1 [openstack] osc-lib>=0.3.0 [test] testtools>=1.4.0 fixtures python-openstackclient pytest pytest-xdist gnocchiclient-7.0.1/gnocchiclient.egg-info/PKG-INFO0000664000372000037200000000323313225150244022571 0ustar travistravis00000000000000Metadata-Version: 1.1 Name: gnocchiclient Version: 7.0.1 Summary: Python client library for Gnocchi Home-page: http://gnocchi.xyz/gnocchiclient Author: Gnocchi Author-email: UNKNOWN License: UNKNOWN Description-Content-Type: UNKNOWN Description: ============= gnocchiclient ============= .. image:: https://travis-ci.org/gnocchixyz/python-gnocchiclient.png?branch=master :target: https://travis-ci.org/gnocchixyz/python-gnocchiclient :alt: Build Status .. image:: https://badge.fury.io/py/gnocchiclient.svg :target: https://badge.fury.io/py/gnocchiclient Python bindings to the Gnocchi API This is a client for Gnocchi API. There's :doc:`a Python API ` (the :mod:`gnocchiclient` module), and a :doc:`command-line script ` (installed as :program:`gnocchi`). Each implements the entire Gnocchi API. * Free software: Apache license * Documentation: http://gnocchi.xyz/gnocchiclient * Source: https://github.com/gnocchixyz/python-gnocchiclient * Bugs: https://github.com/gnocchixyz/python-gnocchiclient/issues Platform: UNKNOWN Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 gnocchiclient-7.0.1/gnocchiclient.egg-info/pbr.json0000664000372000037200000000005613225150244023152 0ustar travistravis00000000000000{"git_version": "e35af68", "is_release": true}gnocchiclient-7.0.1/gnocchiclient.egg-info/dependency_links.txt0000664000372000037200000000000113225150244025541 0ustar travistravis00000000000000 gnocchiclient-7.0.1/gnocchiclient.egg-info/entry_points.txt0000664000372000037200000000710613225150244024775 0ustar travistravis00000000000000[console_scripts] gnocchi = gnocchiclient.shell:main [keystoneauth1.plugin] gnocchi-basic = gnocchiclient.auth:GnocchiBasicLoader gnocchi-noauth = gnocchiclient.auth:GnocchiNoAuthLoader [openstack.cli.extension] metric = gnocchiclient.osc [openstack.metric.v1] metric_aggregates = gnocchiclient.v1.aggregates_cli:CliAggregates metric_archive-policy-rule_create = gnocchiclient.v1.archive_policy_rule_cli:CliArchivePolicyRuleCreate metric_archive-policy-rule_delete = gnocchiclient.v1.archive_policy_rule_cli:CliArchivePolicyRuleDelete metric_archive-policy-rule_list = gnocchiclient.v1.archive_policy_rule_cli:CliArchivePolicyRuleList metric_archive-policy-rule_show = gnocchiclient.v1.archive_policy_rule_cli:CliArchivePolicyRuleShow metric_archive-policy_create = gnocchiclient.v1.archive_policy_cli:CliArchivePolicyCreate metric_archive-policy_delete = gnocchiclient.v1.archive_policy_cli:CliArchivePolicyDelete metric_archive-policy_list = gnocchiclient.v1.archive_policy_cli:CliArchivePolicyList metric_archive-policy_show = gnocchiclient.v1.archive_policy_cli:CliArchivePolicyShow metric_archive-policy_update = gnocchiclient.v1.archive_policy_cli:CliArchivePolicyUpdate metric_benchmark measures add = gnocchiclient.benchmark:CliBenchmarkMeasuresAdd metric_benchmark measures show = gnocchiclient.benchmark:CliBenchmarkMeasuresShow metric_benchmark metric create = gnocchiclient.benchmark:CliBenchmarkMetricCreate metric_benchmark metric show = gnocchiclient.benchmark:CliBenchmarkMetricShow metric_capabilities list = gnocchiclient.v1.capabilities_cli:CliCapabilitiesList metric_create = gnocchiclient.v1.metric_cli:CliMetricCreate metric_delete = gnocchiclient.v1.metric_cli:CliMetricDelete metric_list = gnocchiclient.v1.metric_cli:CliMetricList metric_measures aggregation = gnocchiclient.v1.metric_cli:CliMeasuresAggregation metric_measures_add = gnocchiclient.v1.metric_cli:CliMeasuresAdd metric_measures_batch-metrics = gnocchiclient.v1.metric_cli:CliMetricsMeasuresBatch metric_measures_batch-resources-metrics = gnocchiclient.v1.metric_cli:CliResourcesMetricsMeasuresBatch metric_measures_show = gnocchiclient.v1.metric_cli:CliMeasuresShow metric_metric_create = gnocchiclient.v1.metric_cli:DeprecatedCliMetricCreate metric_metric_delete = gnocchiclient.v1.metric_cli:DeprecatedCliMetricDelete metric_metric_list = gnocchiclient.v1.metric_cli:DeprecatedCliMetricList metric_metric_show = gnocchiclient.v1.metric_cli:DeprecatedCliMetricShow metric_resource-type_create = gnocchiclient.v1.resource_type_cli:CliResourceTypeCreate metric_resource-type_delete = gnocchiclient.v1.resource_type_cli:CliResourceTypeDelete metric_resource-type_list = gnocchiclient.v1.resource_type_cli:CliResourceTypeList metric_resource-type_show = gnocchiclient.v1.resource_type_cli:CliResourceTypeShow metric_resource-type_update = gnocchiclient.v1.resource_type_cli:CliResourceTypeUpdate metric_resource_batch_delete = gnocchiclient.v1.resource_cli:CliResourceBatchDelete metric_resource_create = gnocchiclient.v1.resource_cli:CliResourceCreate metric_resource_delete = gnocchiclient.v1.resource_cli:CliResourceDelete metric_resource_history = gnocchiclient.v1.resource_cli:CliResourceHistory metric_resource_list = gnocchiclient.v1.resource_cli:CliResourceList metric_resource_search = gnocchiclient.v1.resource_cli:CliResourceSearch metric_resource_show = gnocchiclient.v1.resource_cli:CliResourceShow metric_resource_update = gnocchiclient.v1.resource_cli:CliResourceUpdate metric_server_version = gnocchiclient.v1.build_cli:CliBuildShow metric_show = gnocchiclient.v1.metric_cli:CliMetricShow metric_status = gnocchiclient.v1.status_cli:CliStatusShow gnocchiclient-7.0.1/gnocchiclient.egg-info/SOURCES.txt0000664000372000037200000000435513225150244023366 0ustar travistravis00000000000000.coveragerc .mailmap .travis.yml AUTHORS ChangeLog LICENSE README.rst requirements.txt setup.cfg setup.py tox.ini doc/source/api.rst doc/source/conf.py doc/source/index.rst doc/source/installation.rst doc/source/shell.rst doc/source/_static/gnocchi-icon-source.png doc/source/_static/gnocchi-icon.ico doc/source/_static/gnocchi-logo.png gnocchiclient/__init__.py gnocchiclient/auth.py gnocchiclient/benchmark.py gnocchiclient/client.py gnocchiclient/exceptions.py gnocchiclient/gendoc.py gnocchiclient/osc.py gnocchiclient/shell.py gnocchiclient/utils.py gnocchiclient/version.py gnocchiclient.egg-info/PKG-INFO gnocchiclient.egg-info/SOURCES.txt gnocchiclient.egg-info/dependency_links.txt gnocchiclient.egg-info/entry_points.txt gnocchiclient.egg-info/not-zip-safe gnocchiclient.egg-info/pbr.json gnocchiclient.egg-info/requires.txt gnocchiclient.egg-info/top_level.txt gnocchiclient/tests/__init__.py gnocchiclient/tests/functional/__init__.py gnocchiclient/tests/functional/base.py gnocchiclient/tests/functional/test_aggregates.py gnocchiclient/tests/functional/test_archive_policy.py gnocchiclient/tests/functional/test_archive_policy_rule.py gnocchiclient/tests/functional/test_benchmark.py gnocchiclient/tests/functional/test_capabilities.py gnocchiclient/tests/functional/test_metric.py gnocchiclient/tests/functional/test_osc.py gnocchiclient/tests/functional/test_others.py gnocchiclient/tests/functional/test_resource.py gnocchiclient/tests/functional/test_resource_type.py gnocchiclient/tests/unit/__init__.py gnocchiclient/tests/unit/test_auth.py gnocchiclient/tests/unit/test_exceptions.py gnocchiclient/v1/__init__.py gnocchiclient/v1/aggregates.py gnocchiclient/v1/aggregates_cli.py gnocchiclient/v1/archive_policy.py gnocchiclient/v1/archive_policy_cli.py gnocchiclient/v1/archive_policy_rule.py gnocchiclient/v1/archive_policy_rule_cli.py gnocchiclient/v1/base.py gnocchiclient/v1/build.py gnocchiclient/v1/build_cli.py gnocchiclient/v1/capabilities.py gnocchiclient/v1/capabilities_cli.py gnocchiclient/v1/client.py gnocchiclient/v1/metric.py gnocchiclient/v1/metric_cli.py gnocchiclient/v1/resource.py gnocchiclient/v1/resource_cli.py gnocchiclient/v1/resource_type.py gnocchiclient/v1/resource_type_cli.py gnocchiclient/v1/status.py gnocchiclient/v1/status_cli.pygnocchiclient-7.0.1/tox.ini0000664000372000037200000000226113225150105016500 0ustar travistravis00000000000000[tox] minversion = 1.6 envlist = py35,py36,py27,pypy,pep8,docs-gnocchi.xyz skipsdist = True [testenv] usedevelop = True setenv = VIRTUAL_ENV={envdir} GNOCCHI_CLIENT_EXEC_DIR={envdir}/bin passenv = GNOCCHI_* # NOTE(jd): the -e argument is on its own line so it's passed as an argument to pip deps = .[test,openstack] -e git+https://github.com/gnocchixyz/gnocchi.git#egg=gnocchi[postgresql,file] pifpaf[gnocchi] commands = pifpaf run gnocchi -- pytest {posargs:gnocchiclient/tests} [testenv:pep8] basepython = python3 deps = hacking<0.13,>=0.12 doc8>=0.8.0 commands = flake8 doc8 --ignore-path doc/source/gnocchi.rst --ignore-path-errors doc/source/shell.rst;D000 doc/source [testenv:venv] deps = .[test,doc] commands = {posargs} [testenv:cover] deps = {[testenv]deps} pytest-cov commands = pifpaf run gnocchi -- pytest --cov=gnocchiclient {posargs:gnocchiclient/tests} [testenv:docs-gnocchi.xyz] basepython = python3 deps = .[test,doc] commands = python setup.py build_sphinx [flake8] show-source = True ignore = exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build [pytest] addopts = --verbose --numprocesses=auto norecursedirs = .tox gnocchiclient-7.0.1/.travis.yml0000664000372000037200000000305613225150105017301 0ustar travistravis00000000000000language: python sudo: required services: - docker cache: directories: - ~/.cache/pip env: - TARGET: pep8 - TARGET: py27 - TARGET: py35 - TARGET: docs-gnocchi.xyz before_script: # Only run if this is a pull-request - if \[ "$TRAVIS_PULL_REQUEST" != "false" -o -n "$TRAVIS_TAG" \]; then docker pull gnocchixyz/ci-tools:latest ; fi script: # Only run if this is a pull-request - if \[ "$TRAVIS_PULL_REQUEST" != "false" -o -n "$TRAVIS_TAG" \]; then docker run -v ~/.cache/pip:/home/tester/.cache/pip -v $(pwd):/home/tester/src gnocchixyz/ci-tools:latest tox -e ${TARGET} ; fi notifications: email: false irc: on_success: change on_failure: always channels: - "irc.freenode.org#gnocchi" deploy: provider: pypi user: jd password: secure: eQzA9F55MW2v2eQysmV38tyuj0cqYaQh6iTWLf5ZBBqiSu8OwmJeouRCDpHOM93XDerLwzlNfmq2NwaP3lxbhc1kU9fxkyI9CbX4fQzHkrl30Eee9fq82qfnH+cBt9zZBGgk+HEClHxZINiQKF1NTzWA7PDkGRnLDfBrMqUum1FqYoli1XNoCOwgYRcI0UdaE96qqSqE4rlp4vuAR8sfDSh6SxLBUtEHzZM2ScreRZV11DwRSB5a9nc+PGAU6FdmSU6uil2kojmKU84q/+BbSpRG4Ch10MDWIN3UIuPfP4t8zhgC3grfTzq7Q+9l6VYFeQHdAsoHb050d9YabnZNxUqDt/ZIABKcpu/YiZzIXGbpGh4fw4mVupbOoFpVFg1Zg/nQAv99HYDtp5QwqYbA5T3LZXDi2v6+oPWCrBh5ll9bk7DhWsPhBFj5jjBIDY4sgXuRTLaGbik+CLxnmqR/KwOFtLM9cJQjCdfpi1K9VIqNI+NpIgQN5++FhqkL2vWTeiypraK5p1MiuHN0+UA2bdnKpadzE6VuIqjIbfZIKHRZDMkejQ9ruZgIMUTscafyMf9mY9NHXtqD6PA1rK/S4+3n5KIFRDFVVsBUPpao4jIFzaLfSPYciC4PrhyVNzfoIRnlStzuiUPjU0S89UwRAQiapf0NMNmXK2CMWvR9xIg= on: all_branches: true tags: true distributions: "sdist bdist_wheel" gnocchiclient-7.0.1/setup.cfg0000664000372000037200000001145013225150244017012 0ustar travistravis00000000000000[metadata] name = gnocchiclient summary = Python client library for Gnocchi description-file = README.rst author = Gnocchi home-page = http://gnocchi.xyz/gnocchiclient classifier = Intended Audience :: Information Technology Intended Audience :: System Administrators License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 [files] packages = gnocchiclient [entry_points] console_scripts = gnocchi = gnocchiclient.shell:main keystoneauth1.plugin = gnocchi-noauth = gnocchiclient.auth:GnocchiNoAuthLoader gnocchi-basic = gnocchiclient.auth:GnocchiBasicLoader openstack.cli.extension = metric = gnocchiclient.osc openstack.metric.v1 = # FIXME(sileht): don't duplicate entry with the one in shell.py metric_server_version = gnocchiclient.v1.build_cli:CliBuildShow metric_status = gnocchiclient.v1.status_cli:CliStatusShow metric_resource_list = gnocchiclient.v1.resource_cli:CliResourceList metric_resource_show = gnocchiclient.v1.resource_cli:CliResourceShow metric_resource_history = gnocchiclient.v1.resource_cli:CliResourceHistory metric_resource_search = gnocchiclient.v1.resource_cli:CliResourceSearch metric_resource_create = gnocchiclient.v1.resource_cli:CliResourceCreate metric_resource_update = gnocchiclient.v1.resource_cli:CliResourceUpdate metric_resource_delete = gnocchiclient.v1.resource_cli:CliResourceDelete metric_resource_batch_delete = gnocchiclient.v1.resource_cli:CliResourceBatchDelete metric_resource-type_list = gnocchiclient.v1.resource_type_cli:CliResourceTypeList metric_resource-type_create = gnocchiclient.v1.resource_type_cli:CliResourceTypeCreate metric_resource-type_show = gnocchiclient.v1.resource_type_cli:CliResourceTypeShow metric_resource-type_update = gnocchiclient.v1.resource_type_cli:CliResourceTypeUpdate metric_resource-type_delete = gnocchiclient.v1.resource_type_cli:CliResourceTypeDelete metric_archive-policy_list = gnocchiclient.v1.archive_policy_cli:CliArchivePolicyList metric_archive-policy_show = gnocchiclient.v1.archive_policy_cli:CliArchivePolicyShow metric_archive-policy_create = gnocchiclient.v1.archive_policy_cli:CliArchivePolicyCreate metric_archive-policy_update = gnocchiclient.v1.archive_policy_cli:CliArchivePolicyUpdate metric_archive-policy_delete = gnocchiclient.v1.archive_policy_cli:CliArchivePolicyDelete metric_archive-policy-rule_list = gnocchiclient.v1.archive_policy_rule_cli:CliArchivePolicyRuleList metric_archive-policy-rule_show = gnocchiclient.v1.archive_policy_rule_cli:CliArchivePolicyRuleShow metric_archive-policy-rule_create = gnocchiclient.v1.archive_policy_rule_cli:CliArchivePolicyRuleCreate metric_archive-policy-rule_delete = gnocchiclient.v1.archive_policy_rule_cli:CliArchivePolicyRuleDelete # FIXME(rabel): Deprecate metric_metric entry points metric_metric_list = gnocchiclient.v1.metric_cli:DeprecatedCliMetricList metric_metric_show = gnocchiclient.v1.metric_cli:DeprecatedCliMetricShow metric_metric_create = gnocchiclient.v1.metric_cli:DeprecatedCliMetricCreate metric_metric_delete = gnocchiclient.v1.metric_cli:DeprecatedCliMetricDelete metric_list = gnocchiclient.v1.metric_cli:CliMetricList metric_show = gnocchiclient.v1.metric_cli:CliMetricShow metric_create = gnocchiclient.v1.metric_cli:CliMetricCreate metric_delete = gnocchiclient.v1.metric_cli:CliMetricDelete metric_measures_show = gnocchiclient.v1.metric_cli:CliMeasuresShow metric_measures_add = gnocchiclient.v1.metric_cli:CliMeasuresAdd metric_measures_batch-metrics = gnocchiclient.v1.metric_cli:CliMetricsMeasuresBatch metric_measures_batch-resources-metrics = gnocchiclient.v1.metric_cli:CliResourcesMetricsMeasuresBatch metric_measures aggregation = gnocchiclient.v1.metric_cli:CliMeasuresAggregation metric_aggregates = gnocchiclient.v1.aggregates_cli:CliAggregates metric_capabilities list = gnocchiclient.v1.capabilities_cli:CliCapabilitiesList metric_benchmark metric create = gnocchiclient.benchmark:CliBenchmarkMetricCreate metric_benchmark metric show = gnocchiclient.benchmark:CliBenchmarkMetricShow metric_benchmark measures add = gnocchiclient.benchmark:CliBenchmarkMeasuresAdd metric_benchmark measures show = gnocchiclient.benchmark:CliBenchmarkMeasuresShow [extras] test = testtools>=1.4.0 fixtures python-openstackclient pytest pytest-xdist doc = sphinx!=1.2.0,!=1.3b1,>=1.1.2 sphinx_rtd_theme openstack-doc-tools>=1.0.1 openstack = osc-lib>=0.3.0 # Apache-2.0 [build_sphinx] source-dir = doc/source build-dir = doc/build all_files = 1 [upload_sphinx] upload-dir = doc/build/html [pbr] autodoc_index_modules = true autodoc_exclude_modules = gnocchiclient.tests.* [wheel] universal = 1 [egg_info] tag_build = tag_date = 0 gnocchiclient-7.0.1/requirements.txt0000664000372000037200000000051613225150105020452 0ustar travistravis00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr>=1.4 cliff>1.16.0 # Apache-2.0 ujson keystoneauth1>=2.0.0 six futurist iso8601 monotonic python-dateutil debtcollector gnocchiclient-7.0.1/PKG-INFO0000664000372000037200000000323313225150244016266 0ustar travistravis00000000000000Metadata-Version: 1.1 Name: gnocchiclient Version: 7.0.1 Summary: Python client library for Gnocchi Home-page: http://gnocchi.xyz/gnocchiclient Author: Gnocchi Author-email: UNKNOWN License: UNKNOWN Description-Content-Type: UNKNOWN Description: ============= gnocchiclient ============= .. image:: https://travis-ci.org/gnocchixyz/python-gnocchiclient.png?branch=master :target: https://travis-ci.org/gnocchixyz/python-gnocchiclient :alt: Build Status .. image:: https://badge.fury.io/py/gnocchiclient.svg :target: https://badge.fury.io/py/gnocchiclient Python bindings to the Gnocchi API This is a client for Gnocchi API. There's :doc:`a Python API ` (the :mod:`gnocchiclient` module), and a :doc:`command-line script ` (installed as :program:`gnocchi`). Each implements the entire Gnocchi API. * Free software: Apache license * Documentation: http://gnocchi.xyz/gnocchiclient * Source: https://github.com/gnocchixyz/python-gnocchiclient * Bugs: https://github.com/gnocchixyz/python-gnocchiclient/issues Platform: UNKNOWN Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 gnocchiclient-7.0.1/AUTHORS0000664000372000037200000000031013225150244016232 0ustar travistravis00000000000000Cristian Calin Julien Danjou Mehdi Abaakouk Olivier Destras gord chung sum12 gnocchiclient-7.0.1/.coveragerc0000664000372000037200000000017613225150105017311 0ustar travistravis00000000000000[run] branch = True source = gnocchiclient omit = gnocchiclient/gendoc.py gnocchiclient/osc.py [report] ignore_errors = True