gnocchiclient 2.2.0

-----BEGIN PGP SIGNATURE-----
 Version: GnuPG v1
 
 iQIcBAABCAAGBQJWzrRuAAoJEBiStC/OquvIeGYP/iV4Kmt7avOsr6SvhlRQz1V3
 uYEHSEBNNZTaRwscPcGP0nI8mA2uLdrxncydTy9DD7m7xE5iTLqKwt5Gsa1OwewG
 v/68pWAUPzGi67Ntk23Udb/n0RaDyo9YAHjZ+XZZjtU7L/6V96kbBi++0KnUwfgs
 I4woIU4+UZvdTC3N5x1Oz1D8VvjTrp0SvBDBNy+33xV7/weNnNQwaWPpfmBdiN5M
 29IgYxrDSHu7/eX1e13gMt8jYbRWxPVJhw/r7nrVcHrftrxIRTLk5lf0ziRDDhyt
 e8gHW811LwsONQvnd+5OjqdijDQBMwmc9h4VLhqc1A9yTa6F1yucJd0WM9Vsvaxr
 DbVJwV6fIcPDqoRk/O5L4uwFijezQmHsM1bBDqdVPTnJdLLoU7u+aX6pkfVzXT+u
 nJEVAFz4gENbObCIbrQpIM20NLHqqTchTmSC+wolqrGWM6JLDfADJGViJqKYuC2V
 2kU8FHmDspFdPHwdRptUnBWOalZ0/VvJbbbiPNbk3TW0dU5WQH8xMp+JFq8pdL16
 tZFWeNKQEMI218qktqIoYQR4wq/O8ctcWfPdid1c81O3AaAg0L39N892WbDihLnw
 faSBD2YXrEe3DsBP5CIr3A/VxsqKCinnhUAN5EvUWDIn0qNu/8cLUrQm2nX4ECGF
 MIHmKtbKFFkqBQUDnvv9
 =4gno
 -----END PGP SIGNATURE-----

Merge tag '2.2.0' into debian/mitaka

gnocchiclient 2.2.0
This commit is contained in:
Corey Bryant 2016-03-02 09:29:29 -05:00
commit a54ab30782
29 changed files with 415 additions and 208 deletions

2
.gitignore vendored
View File

@ -54,4 +54,4 @@ ChangeLog
.*sw?
# generated docs
doc/source/ref/
doc/source/api

View File

@ -1,3 +1,2 @@
# Format is:
# <preferred e-mail> <other e-mail 1>
# <preferred e-mail> <other e-mail 2>
<sileht@sileht.net> <sileht@redhat.com>
<sileht@sileht.net> <sileht@sileh.net>

View File

@ -1,6 +0,0 @@
include AUTHORS
include ChangeLog
exclude .gitignore
exclude .gitreview
global-exclude *.pyc

View File

@ -1,2 +0,0 @@
[python: **.py]

View File

@ -21,7 +21,7 @@ Reference
For more information, see the reference:
.. toctree::
:maxdepth: 2
:maxdepth: 2
ref/v1/index
api/autoindex

View File

@ -21,51 +21,13 @@ ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", ".."))
sys.path.insert(0, ROOT)
sys.path.insert(0, BASE_DIR)
def gen_ref(ver, title, names):
refdir = os.path.join(BASE_DIR, "ref")
pkg = "gnocchiclient"
if ver:
pkg = "%s.%s" % (pkg, ver)
refdir = os.path.join(refdir, ver)
if not os.path.exists(refdir):
os.makedirs(refdir)
idxpath = os.path.join(refdir, "index.rst")
with open(idxpath, "w") as idx:
idx.write(("%(title)s\n"
"%(signs)s\n"
"\n"
".. toctree::\n"
" :maxdepth: 1\n"
"\n") % {"title": title, "signs": "=" * len(title)})
for name in names:
idx.write(" %s\n" % name)
rstpath = os.path.join(refdir, "%s.rst" % name)
with open(rstpath, "w") as rst:
rst.write(("%(title)s\n"
"%(signs)s\n"
"\n"
".. automodule:: %(pkg)s.%(name)s\n"
" :members:\n"
" :undoc-members:\n"
" :show-inheritance:\n"
" :noindex:\n")
% {"title": " ".join([n.capitalize()
for n in name.split("_")]),
"signs": "=" * len(name),
"pkg": pkg, "name": name})
gen_ref("v1", "Version 1 API", ["client", "resource", "archive_policy",
"archive_policy_rule", "metric"])
# -- General configuration ----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'sphinx.ext.autodoc',
#'sphinx.ext.intersphinx',
'oslosphinx'
#'sphinx.ext.intersphinx'
]
# autodoc generation is a bit aggressive and a nuisance when doing heavy
@ -99,6 +61,15 @@ pygments_style = 'sphinx'
# html_theme_path = ["."]
# html_theme = '_theme'
# html_static_path = ['static']
html_theme = os.getenv("SPHINX_HTML_THEME", 'openstack')
if html_theme == "sphinx_rtd_theme":
import sphinx_rtd_theme
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
else:
import oslosphinx
html_theme_path = [os.path.join(os.path.dirname(oslosphinx.__file__),
'theme')]
# Output file base name for HTML help builder.
htmlhelp_basename = '%sdoc' % project

View File

@ -37,7 +37,7 @@ For example, in Bash you would use::
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:`--gnocchi-endpoint` and :option:`--os-auth-token`. You can alternatively
token using :option:`--endpoint` and :option:`--os-auth-token`. You can alternatively
set these environment variables::
export GNOCCHI_ENDPOINT=http://gnocchi.example.org:8041
@ -45,8 +45,9 @@ set these environment variables::
export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155
Also, if the server doesn't support authentication, you can provide
:option:`--os-auth-plugon` gnocchi-noauth, :option:`--gnocchi-endpoint`, :option:`--user-id`
and :option:`--project-id`. You can alternatively set these environment variables::
:option:`--os-auth-plugin` gnocchi-noauth, :option:`--endpoint`,
:option:`--user-id` and :option:`--project-id`. You can alternatively set these
environment variables::
export OS_AUTH_PLUGIN=gnocchi-noauth
export GNOCCHI_ENDPOINT=http://gnocchi.example.org:8041
@ -74,4 +75,4 @@ List resources::
Search of resources::
gnocchi resource search --query "project_id=5a301761-f78b-46e2-8900-8b4f6fe6675a and type=instance"
gnocchi resource search "project_id='5a301761-f78b-46e2-8900-8b4f6fe6675a' and type=instance"

View File

@ -35,5 +35,5 @@ class SessionClient(adapter.Adapter):
**kwargs)
if raise_exc and resp.status_code >= 400:
raise exceptions.from_response(resp, url, method)
raise exceptions.from_response(resp, method)
return resp

View File

@ -114,7 +114,7 @@ class Conflict(ClientException):
message = "Conflict"
class NamedMetricAreadyExists(Conflict, MutipleMeaningException):
class NamedMetricAlreadyExists(Conflict, MutipleMeaningException):
message = "Named metric already exists"
match = re.compile("Named metric .* does not exist")
@ -131,7 +131,7 @@ class ArchivePolicyAlreadyExists(Conflict, MutipleMeaningException):
class ArchivePolicyRuleAlreadyExists(Conflict, MutipleMeaningException):
message = "Archive policy rule already exists"
match = re.compile("Archive policy Rule .* already exists")
match = re.compile("Archive policy rule .* already exists")
class OverLimit(RetryAfterException):
@ -165,9 +165,9 @@ _error_classes = [BadRequest, Unauthorized, Forbidden, NotFound,
MethodNotAllowed, NotAcceptable, Conflict, OverLimit,
RateLimit, NotImplemented]
_error_classes_enhanced = {
NotFound: [MetricNotFound, ResourceNotFound, ArchivePolicyNotFound,
ArchivePolicyRuleNotFound],
Conflict: [NamedMetricAreadyExists, ResourceAlreadyExists,
NotFound: [MetricNotFound, ResourceNotFound, ArchivePolicyRuleNotFound,
ArchivePolicyNotFound],
Conflict: [NamedMetricAlreadyExists, ResourceAlreadyExists,
ArchivePolicyAlreadyExists,
ArchivePolicyRuleAlreadyExists]
}
@ -176,14 +176,14 @@ _code_map = dict(
for c in _error_classes)
def from_response(response, url, method=None):
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 exception_from_response(resp)
raise from_response(resp)
"""
if response.status_code:
@ -196,7 +196,7 @@ def from_response(response, url, method=None):
kwargs = {
'code': response.status_code,
'method': method,
'url': url,
'url': response.url,
'request_id': req_id,
}
@ -209,12 +209,21 @@ def from_response(response, url, method=None):
except ValueError:
pass
else:
desc = body.get('description')
for enhanced_cls in enhanced_classes:
if enhanced_cls.match.match(desc):
cls = enhanced_cls
break
kwargs['message'] = desc
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

View File

@ -1,28 +0,0 @@
# 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 oslo_i18n as i18n
_translators = i18n.TranslatorFactory(domain='gnocchiclient')
# The primary translation function using the well-known name "_"
_ = _translators.primary
# Translators for log levels.
#
# The abbreviated names are meant to reflect the usual use of a short
# name like '_'. The "L" is for "log" and the other letter comes from
# the level.
_LI = _translators.log_info
_LW = _translators.log_warning
_LE = _translators.log_error
_LC = _translators.log_critical

View File

@ -69,10 +69,17 @@ class GnocchiNoAuthLoader(loading.BaseLoader):
def get_options(self):
options = super(GnocchiNoAuthLoader, self).get_options()
options.extend([
GnocchiOpt('user-id', help='User ID', required=True),
GnocchiOpt('project-id', help='Project ID', required=True),
GnocchiOpt('roles', help='Roles', default="admin"),
GnocchiOpt('gnocchi-endpoint', help='Gnocchi endpoint',
dest="endpoint", required=True),
GnocchiOpt('user-id', help='User ID', required=True,
metavar="<gnocchi user id>"),
GnocchiOpt('project-id', help='Project ID', required=True,
metavar="<gnocchi project id>"),
GnocchiOpt('roles', help='Roles', default="admin",
metavar="<gnocchi roles>"),
GnocchiOpt('endpoint', help='Gnocchi endpoint',
deprecated=[
GnocchiOpt('gnocchi-endpoint'),
],
dest="endpoint", required=True,
metavar="<gnocchi endpoint>"),
])
return options

View File

@ -45,6 +45,7 @@ class GnocchiCommandManager(commandmanager.CommandManager):
"resource create": resource_cli.CliResourceCreate,
"resource update": resource_cli.CliResourceUpdate,
"resource delete": resource_cli.CliResourceDelete,
"resource list-types": resource_cli.CliResourceTypeList,
"archive-policy list": archive_policy_cli.CliArchivePolicyList,
"archive-policy show": archive_policy_cli.CliArchivePolicyShow,
"archive-policy create": archive_policy_cli.CliArchivePolicyCreate,
@ -59,6 +60,9 @@ class GnocchiCommandManager(commandmanager.CommandManager):
"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,
"capabilities list": capabilities_cli.CliCapabilitiesList,
"benchmark metric create": benchmark.CliBenchmarkMetricCreate,
@ -123,9 +127,7 @@ class GnocchiShell(app.App):
if not isinstance(plugin, noauth.GnocchiNoAuthLoader):
parser.add_argument(
'--gnocchi-endpoint',
metavar='<endpoint>',
dest='endpoint',
'--endpoint',
default=os.environ.get('GNOCCHI_ENDPOINT'),
help='Gnocchi endpoint (Env: GNOCCHI_ENDPOINT)')

View File

@ -34,12 +34,12 @@ class GnocchiClient(object):
self.project_id = str(uuid.uuid4())
def gnocchi(self, action, flags='', params='',
fail_ok=False, merge_stderr=False):
fail_ok=False, merge_stderr=False, input=None):
creds = ("--os-auth-plugin gnocchi-noauth "
"--user-id %s --project-id %s "
"--gnocchi-endpoint %s") % (self.user_id,
self.project_id,
self.endpoint)
"--endpoint %s") % (self.user_id,
self.project_id,
self.endpoint)
flags = creds + ' ' + flags
@ -58,10 +58,11 @@ class GnocchiClient(object):
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, stdout=stdout, stderr=stderr)
result, result_err = proc.communicate()
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 exceptions.CommandFailed(proc.returncode,
cmd,

View File

@ -25,7 +25,7 @@ class ArchivePolicyRuleClientTest(base.ClientTestBase):
# CREATE
result = self.gnocchi(
u'archive-policy-rule', params=u"create test"
u" --archive-policy %s"
u" --archive-policy-name %s"
u" --metric-pattern 'disk.io.*'" % apname)
policy_rule = self.details_multiple(result)[0]
self.assertEqual('test', policy_rule["name"])
@ -33,7 +33,7 @@ class ArchivePolicyRuleClientTest(base.ClientTestBase):
# CREATE FAIL
result = self.gnocchi(
u'archive-policy-rule', params=u"create test"
u" --archive-policy high"
u" --archive-policy-name high"
u" --metric-pattern 'disk.io.*'",
fail_ok=True, merge_stderr=True)
self.assertFirstLineStartsWith(

View File

@ -23,7 +23,7 @@ class BenchmarkMetricTest(base.ClientTestBase):
def test_benchmark_metric_create(self):
apname = str(uuid.uuid4())
# PREPARE AN ACHIVE POLICY
# PREPARE AN ARCHIVE POLICY
self.gnocchi("archive-policy", params="create %s "
"--back-window 0 -d granularity:1s,points:86400" % apname)
@ -44,7 +44,7 @@ class BenchmarkMetricTest(base.ClientTestBase):
def test_benchmark_metric_get(self):
apname = str(uuid.uuid4())
# PREPARE AN ACHIVE POLICY
# PREPARE AN ARCHIVE POLICY
self.gnocchi("archive-policy", params="create %s "
"--back-window 0 -d granularity:1s,points:86400" % apname)
@ -60,7 +60,7 @@ class BenchmarkMetricTest(base.ClientTestBase):
def test_benchmark_measures_add(self):
apname = str(uuid.uuid4())
# PREPARE AN ACHIVE POLICY
# PREPARE AN ARCHIVE POLICY
self.gnocchi("archive-policy", params="create %s "
"--back-window 0 -d granularity:1s,points:86400" % apname)
@ -84,7 +84,7 @@ class BenchmarkMetricTest(base.ClientTestBase):
def test_benchmark_measures_show(self):
apname = str(uuid.uuid4())
# PREPARE AN ACHIVE POLICY
# PREPARE AN ARCHIVE POLICY
self.gnocchi("archive-policy", params="create %s "
"--back-window 0 -d granularity:1s,points:86400" % apname)

View File

@ -9,6 +9,9 @@
# 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
from gnocchiclient.tests.functional import base
@ -17,7 +20,7 @@ from gnocchiclient.tests.functional import base
class MetricClientTest(base.ClientTestBase):
def test_delete_several_metrics(self):
apname = str(uuid.uuid4())
# PREPARE AN ACHIVE POLICY
# PREPARE AN ARCHIVE POLICY
self.gnocchi("archive-policy", params="create %s "
"--back-window 0 -d granularity:1s,points:86400" % apname)
# Create 2 metrics
@ -49,7 +52,7 @@ class MetricClientTest(base.ClientTestBase):
metric2["id"])
def test_metric_scenario(self):
# PREPARE AN ACHIVE POLICY
# PREPARE AN ARCHIVE POLICY
self.gnocchi("archive-policy", params="create metric-test "
"--back-window 0 -d granularity:1s,points:86400")
@ -63,7 +66,7 @@ class MetricClientTest(base.ClientTestBase):
metric["created_by_project_id"])
self.assertEqual(self.clients.user_id, metric["created_by_user_id"])
self.assertEqual('some-name', metric["name"])
self.assertEqual('None', metric["resource"])
self.assertEqual('None', metric["resource/id"])
self.assertIn("metric-test", metric["archive_policy/name"])
# CREATE WITHOUT NAME
@ -76,7 +79,7 @@ class MetricClientTest(base.ClientTestBase):
metric["created_by_project_id"])
self.assertEqual(self.clients.user_id, metric["created_by_user_id"])
self.assertEqual('None', metric["name"])
self.assertEqual('None', metric["resource"])
self.assertEqual('None', metric["resource/id"])
self.assertIn("metric-test", metric["archive_policy/name"])
# GET
@ -96,6 +99,7 @@ class MetricClientTest(base.ClientTestBase):
result = self.retry_gnocchi(
5, 'measures', params=("show %s "
"--aggregation mean "
"--granularity 1 "
"--start 2015-03-06T14:32:00 "
"--stop 2015-03-06T14:36:00"
) % metric["id"])
@ -123,6 +127,18 @@ class MetricClientTest(base.ClientTestBase):
'timestamp': '2015-03-06T14:34:12+00:00',
'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))
self.gnocchi('measures', params=("batch-metrics -"),
input=measures.encode('utf8'))
# LIST
result = self.gnocchi('metric', params="list")
metrics = self.parser.listing(result)
@ -167,7 +183,7 @@ class MetricClientTest(base.ClientTestBase):
metric["created_by_project_id"])
self.assertEqual(self.clients.user_id, metric["created_by_user_id"])
self.assertEqual('metric-name', metric["name"])
self.assertNotEqual('None', metric["resource"])
self.assertNotEqual('None', metric["resource/id"])
self.assertIn("metric-test", metric["archive_policy/name"])
# CREATE FAIL
@ -209,7 +225,7 @@ class MetricClientTest(base.ClientTestBase):
# MEASURES AGGREGATION
result = self.gnocchi(
'measures', params=("--debug aggregation "
'measures', params=("aggregation "
"--query \"id='metric-res'\" "
"--resource-type \"generic\" "
"-m metric-name "
@ -225,6 +241,20 @@ class MetricClientTest(base.ClientTestBase):
'timestamp': '2015-03-06T14:34:12+00:00',
'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))
self.gnocchi('measures', params=("batch-resources-metrics -"),
input=measures.encode('utf8'))
# LIST
result = self.gnocchi('metric', params="list")
metrics = self.parser.listing(result)

View File

@ -13,11 +13,13 @@
import uuid
from gnocchiclient.tests.functional import base
from gnocchiclient import utils
class ResourceClientTest(base.ClientTestBase):
RESOURCE_ID = str(uuid.uuid4())
RESOURCE_ID2 = str(uuid.uuid4())
RAW_RESOURCE_ID2 = str(uuid.uuid4()) + "/foo"
RESOURCE_ID2 = utils.encode_resource_id(RAW_RESOURCE_ID2)
PROJECT_ID = str(uuid.uuid4())
def test_help(self):
@ -36,7 +38,6 @@ class ResourceClientTest(base.ClientTestBase):
result = self.gnocchi(
u'resource', params=u"create %s --type generic" %
self.RESOURCE_ID)
resource = self.details_multiple(result)
resource = self.details_multiple(result)[0]
self.assertEqual(self.RESOURCE_ID, resource["id"])
self.assertEqual('None', resource["project_id"])
@ -95,20 +96,30 @@ class ResourceClientTest(base.ClientTestBase):
# Search
result = self.gnocchi('resource',
params=("search --type generic "
"--query 'project_id=%s'"
"'project_id=%s'"
) % self.PROJECT_ID)
resource_list = self.parser.listing(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 = self.details_multiple(result)[0]
self.assertNotIn("temperature", resource_updated["metrics"])
# CREATE 2
result = self.gnocchi(
'resource', params=("create %s -t generic "
"-a project_id:%s"
) % (self.RESOURCE_ID2, self.PROJECT_ID))
) % (self.RAW_RESOURCE_ID2, self.PROJECT_ID))
resource2 = self.details_multiple(result)[0]
self.assertEqual(self.RESOURCE_ID2, resource2["id"])
self.assertEqual(self.RAW_RESOURCE_ID2,
resource2["original_resource_id"])
self.assertEqual(self.PROJECT_ID, resource2["project_id"])
self.assertNotEqual('None', resource2["started_at"])
@ -116,7 +127,7 @@ class ResourceClientTest(base.ClientTestBase):
result = self.gnocchi('resource',
params=("search "
"-t generic "
"--query 'project_id=%s' "
"'project_id=%s' "
"--sort started_at:asc "
"--marker %s "
"--limit 1"
@ -156,3 +167,14 @@ class ResourceClientTest(base.ClientTestBase):
resource_ids = [r['id'] for r in self.parser.listing(result)]
self.assertNotIn(self.RESOURCE_ID, resource_ids)
self.assertNotIn(self.RESOURCE_ID2, resource_ids)
# LIST THE RESOUCES TYPES
resource_type = ('instance', 'generic', 'volume',
'instance_disk', 'stack', 'identity')
result = self.gnocchi(
'resource', params="list-types")
result_list = self.parser.listing(result)
type_from_list = [t['resource_type'] for t in result_list]
for one_type in resource_type:
self.assertIn(one_type, type_from_list)

View File

@ -0,0 +1,55 @@
# -*- 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 oslotest import base
from requests import models
from gnocchiclient import exceptions
class ExceptionsTest(base.BaseTestCase):
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_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)

View File

@ -23,6 +23,10 @@ class SearchQueryBuilderTest(base.BaseTestCase):
self.assertEqual(expected, req)
def test_search_query_builder(self):
self._do_test('foo=7EED6CC3-EDC8-48C9-8EF6-8A36B9ACC91C',
{"=": {"foo": "7EED6CC3-EDC8-48C9-8EF6-8A36B9ACC91C"}})
self._do_test('foo=7EED6CC3EDC848C98EF68A36B9ACC91C',
{"=": {"foo": "7EED6CC3EDC848C98EF68A36B9ACC91C"}})
self._do_test('foo=bar', {"=": {"foo": "bar"}})
self._do_test('foo!=1', {"!=": {"foo": 1.0}})
self._do_test('foo=True', {"=": {"foo": True}})
@ -81,3 +85,14 @@ class SearchQueryBuilderTest(base.BaseTestCase):
]},
{"=": {"foo": "quote"}},
]})
def test_dict_to_querystring(self):
expected = ["start=2016-02-10T13%3A54%3A53%2B00%3A00"
"&stop=2016-02-10T13%3A56%3A42%2B02%3A00",
"stop=2016-02-10T13%3A56%3A42%2B02%3A00"
"&start=2016-02-10T13%3A54%3A53%2B00%3A00"]
self.assertIn(utils.dict_to_querystring(
{"start": "2016-02-10T13:54:53+00:00",
"stop": "2016-02-10T13:56:42+02:00"}),
expected)

View File

@ -12,8 +12,11 @@
# License for the specific language governing permissions and limitations
# under the License.
import uuid
import pyparsing as pp
import six
from six.moves.urllib import parse as urllib_parse
uninary_operators = ("not", )
binary_operator = (u">=", u"<=", u"!=", u">", u"<", u"=", u"==", u"eq", u"ne",
@ -26,8 +29,9 @@ null = pp.Regex("None|none|null").setParseAction(pp.replaceWith(None))
boolean = "False|True|false|true"
boolean = pp.Regex(boolean).setParseAction(lambda t: t[0].lower() == "true")
hex_string = lambda n: pp.Word(pp.hexnums, exact=n)
uuid = pp.Combine(hex_string(8) + ("-" + hex_string(4)) * 3 +
"-" + hex_string(12))
uuid_string = pp.Combine(hex_string(8) +
(pp.Optional("-") + hex_string(4)) * 3 +
pp.Optional("-") + hex_string(12))
number = r"[+-]?\d+(:?\.\d*)?(:?[eE][+-]?\d+)?"
number = pp.Regex(number).setParseAction(lambda t: float(t[0]))
identifier = pp.Word(pp.alphas, pp.alphanums + "_")
@ -36,7 +40,7 @@ comparison_term = pp.Forward()
in_list = pp.Group(pp.Suppress('[') +
pp.Optional(pp.delimitedList(comparison_term)) +
pp.Suppress(']'))("list")
comparison_term << (null | boolean | uuid | identifier | number |
comparison_term << (null | boolean | uuid_string | identifier | number |
quoted_string | in_list)
condition = pp.Group(comparison_term + operator + comparison_term)
@ -80,8 +84,17 @@ def _parsed_query2dict(parsed_query):
return result
class MalformedQuery(Exception):
def __init__(self, reason):
super(MalformedQuery, self).__init__(
"Malformed Query: %s" % reason)
def search_query_builder(query):
parsed_query = expr.parseString(query)[0]
try:
parsed_query = expr.parseString(query, parseAll=True)[0]
except pp.ParseException as e:
raise MalformedQuery(six.text_type(e))
return _parsed_query2dict(parsed_query)
@ -112,6 +125,18 @@ def format_archive_policy(ap):
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:
@ -122,6 +147,27 @@ def dict_from_parsed_args(parsed_args, attrs):
def dict_to_querystring(objs):
return "&".join(["%s=%s" % (k, v)
return "&".join(["%s=%s" % (k, urllib_parse.quote(six.text_type(v)))
for k, v in objs.items()
if v is not None])
# uuid5 namespace for id transformation.
# NOTE(chdent): This UUID must stay the same, forever, across all
# of gnocchi to preserve its value as a URN namespace.
RESOURCE_ID_NAMESPACE = uuid.UUID('0a7a15ff-aa13-4ac2-897c-9bdf30ce175b')
def encode_resource_id(value):
try:
try:
return str(uuid.UUID(value))
except ValueError:
if len(value) <= 255:
if six.PY2:
value = value.encode('utf-8')
return str(uuid.uuid5(RESOURCE_ID_NAMESPACE, value))
raise ValueError(
'transformable resource id >255 max allowed characters')
except Exception as e:
raise ValueError(e)

View File

@ -23,6 +23,8 @@ 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):
"""List archive metrics
@ -51,6 +53,7 @@ class MetricManager(base.Manager):
self._ensure_metric_is_uuid(metric)
url = self.metric_url + metric
else:
resource_id = utils.encode_resource_id(resource_id)
url = (self.resource_url % resource_id) + metric
return self._get(url).json()
@ -79,6 +82,7 @@ class MetricManager(base.Manager):
raise TypeError("metric_name is required if resource_id is set")
del metric['resource_id']
resource_id = utils.encode_resource_id(resource_id)
metric = {metric_name: metric}
metric = self._post(
self.resource_url % resource_id,
@ -99,6 +103,7 @@ class MetricManager(base.Manager):
self._ensure_metric_is_uuid(metric)
url = self.metric_url + metric
else:
resource_id = utils.encode_resource_id(resource_id)
url = self.resource_url % resource_id + metric
self._delete(url)
@ -117,13 +122,39 @@ class MetricManager(base.Manager):
self._ensure_metric_is_uuid(metric)
url = self.metric_url + metric + "/measures"
else:
resource_id = utils.encode_resource_id(resource_id)
url = self.resource_url % resource_id + metric + "/measures"
return self._post(
url, headers={'Content-Type': "application/json"},
data=jsonutils.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=jsonutils.dumps(measures))
def batch_resources_metrics_measures(self, measures):
"""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=jsonutils.dumps(measures))
def get_measures(self, metric, start=None, stop=None, aggregation=None,
resource_id=None, **kwargs):
granularity=None, resource_id=None, **kwargs):
"""Get measurements of a metric
:param metric: ID or Name of the metric
@ -134,6 +165,8 @@ class MetricManager(base.Manager):
: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
@ -147,12 +180,14 @@ class MetricManager(base.Manager):
if isinstance(stop, datetime.datetime):
stop = stop.isoformat()
params = dict(start=start, stop=stop, aggregation=aggregation)
params = dict(start=start, stop=stop, aggregation=aggregation,
granularity=granularity)
params.update(kwargs)
if resource_id is None:
self._ensure_metric_is_uuid(metric)
url = self.metric_url + metric + "/measures"
else:
resource_id = utils.encode_resource_id(resource_id)
url = self.resource_url % resource_id + metric + "/measures"
return self._get(url, params=params).json()

View File

@ -11,6 +11,9 @@
# License for the specific language governing permissions and limitations
# under the License.
import json
import sys
from cliff import command
from cliff import lister
from cliff import show
@ -54,6 +57,7 @@ class CliMetricShow(CliMetricWithResourceID, show.ShowOne):
resource_id=parsed_args.resource_id)
utils.format_archive_policy(metric["archive_policy"])
utils.format_move_dict_to_root(metric, "archive_policy")
utils.format_resource_for_metric(metric)
return self.dict2columns(metric)
@ -88,6 +92,7 @@ class CliMetricCreate(CliMetricCreateBase):
metric = self.app.client.metric.create(metric)
utils.format_archive_policy(metric["archive_policy"])
utils.format_move_dict_to_root(metric, "archive_policy")
utils.format_resource_for_metric(metric)
return self.dict2columns(metric)
@ -121,6 +126,8 @@ class CliMeasuresShow(CliMetricWithResourceID, lister.Lister):
help="beginning of the period")
parser.add_argument("--stop",
help="end of the period")
parser.add_argument("--granularity",
help="granularity to retrieve (in seconds)")
return parser
def take_action(self, parsed_args):
@ -130,6 +137,7 @@ class CliMeasuresShow(CliMetricWithResourceID, lister.Lister):
aggregation=parsed_args.aggregation,
start=parsed_args.start,
stop=parsed_args.stop,
granularity=parsed_args.granularity,
)
return self.COLS, measures
@ -164,6 +172,35 @@ class CliMeasuresAdd(CliMeasuresAddBase):
)
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:
self.app.client.metric.batch_metrics_measures(json.load(f))
class CliResourcesMetricsMeasuresBatch(CliMeasuresBatch):
def take_action(self, parsed_args):
with parsed_args.file as f:
self.app.client.metric.batch_resources_metrics_measures(
json.load(f))
class CliMeasuresAggregation(lister.Lister):
"""Get measurements of aggregated metrics"""

View File

@ -14,6 +14,7 @@
from oslo_serialization import jsonutils
from six.moves.urllib import parse as urllib_parse
from gnocchiclient import utils
from gnocchiclient.v1 import base
@ -51,8 +52,9 @@ class ResourceManager(base.Manager):
: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
: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
@ -72,6 +74,7 @@ class ResourceManager(base.Manager):
:type history: bool
"""
history = "/history" if history else ""
resource_id = utils.encode_resource_id(resource_id)
url = self.url + "%s/%s%s" % (resource_type, resource_id, history)
return self._get(url).json()
@ -89,11 +92,13 @@ class ResourceManager(base.Manager):
: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
"""
qs = _get_pagination_options(details, False, limit, marker, sorts)
resource_id = utils.encode_resource_id(resource_id)
url = "%s%s/%s/history?%s" % (self.url, resource_type, resource_id, qs)
return self._get(url).json()
@ -121,6 +126,7 @@ class ResourceManager(base.Manager):
:type resource: dict
"""
resource_id = utils.encode_resource_id(resource_id)
return self._patch(
self.url + resource_type + "/" + resource_id,
headers={'Content-Type': "application/json"},
@ -132,6 +138,7 @@ class ResourceManager(base.Manager):
:param resource_id: ID of the resource
:type resource_id: str
"""
resource_id = utils.encode_resource_id(resource_id)
self._delete(self.url + "generic/" + resource_id)
def search(self, resource_type="generic", query=None, details=False,
@ -139,7 +146,7 @@ class ResourceManager(base.Manager):
"""List resources
:param resource_type: Type of the resource
:param resource_type: str
:type resource_type: str
:param query: The query dictionary
:type query: dict
:param details: Show all attributes of resources
@ -150,6 +157,7 @@ class ResourceManager(base.Manager):
: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
@ -165,3 +173,13 @@ class ResourceManager(base.Manager):
return self._post(
url, headers={'Content-Type': "application/json"},
data=jsonutils.dumps(query)).json()
def list_types(self):
"""List the resource types supported by gnocchi"""
# (Note/jzl)Based on the discussion result, keep the keyword
# 'resource-type' and use the command 'resource list-types' to
# list the types supported by gnocchi.
# Opened a reminding bug to me to handle with it,
# when resource-type is ready
# https://bugs.launchpad.net/python-gnocchiclient/+bug/1535176
return self._get(self.url).json()

View File

@ -22,6 +22,7 @@ class CliResourceList(lister.Lister):
COLS = ('id', 'type',
'project_id', 'user_id',
'original_resource_id',
'started_at', 'ended_at',
'revision_start', 'revision_end')
@ -87,7 +88,7 @@ class CliResourceSearch(CliResourceList):
def get_parser(self, prog_name):
parser = super(CliResourceSearch, self).get_parser(prog_name)
parser.add_argument("--query", help="Query"),
parser.add_argument("query", help="Query")
return parser
def take_action(self, parsed_args):
@ -140,11 +141,8 @@ class CliResourceCreate(show.ShowOne):
default=[],
help="name:id of a metric to add"),
parser.add_argument(
"-n", "--create-metric", action='append',
"-n", "--create-metric", action='append', default=[],
help="name:archive_policy_name of a metric to create"),
parser.add_argument("-d", "--delete-metric", action='append',
default=[],
help="Name of a metric to delete"),
return parser
def _resource_from_args(self, parsed_args, update=False):
@ -157,19 +155,19 @@ class CliResourceCreate(show.ShowOne):
resource[attr] = value
if (parsed_args.add_metric
or parsed_args.create_metric
or parsed_args.delete_metric):
or (update and parsed_args.delete_metric)):
if update:
r = self.app.client.resource.get(parsed_args.resource_type,
parsed_args.resource_id)
default = r['metrics']
for metric_name in parsed_args.delete_metric:
default.pop(metric_name, None)
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.delete_metric:
resource['metrics'].pop(name, None)
for metric in parsed_args.create_metric:
name, _, value = metric.partition(":")
if value is "":
@ -190,6 +188,13 @@ class CliResourceCreate(show.ShowOne):
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 = self.app.client.resource.update(
@ -211,3 +216,14 @@ class CliResourceDelete(command.Command):
def take_action(self, parsed_args):
self.app.client.resource.delete(parsed_args.resource_id)
class CliResourceTypeList(lister.Lister):
"""List the resource types that gnocchi supports"""
COLS = ('resource_type',
'resource_controller_url')
def take_action(self, parsed_args):
resources = self.app.client.resource.list_types()
return self.COLS, list(resources.items())

View File

@ -3,9 +3,7 @@
# process, which may cause wedges in the gate later.
pbr<2.0,>=1.4
Babel>=1.3
cliff>=1.14.0 # Apache-2.0
oslo.i18n>=1.5.0 # Apache-2.0
oslo.serialization>=1.4.0 # Apache-2.0
oslo.utils>=2.0.0 # Apache-2.0
keystoneauth1>=1.0.0

View File

@ -18,36 +18,23 @@ clean_exit () {
}
GNOCCHI_DATA=`mktemp -d /tmp/gnocchi-data-XXXXX`
MYSQL_DATA=`mktemp -d /tmp/gnocchi-mysql-XXXXX`
trap "clean_exit \"$GNOCCHI_DATA\" \"$MYSQL_DATA\"" EXIT
mkfifo ${MYSQL_DATA}/out
PATH=$PATH:/usr/libexec
mysqld --no-defaults --datadir=${MYSQL_DATA} --pid-file=${MYSQL_DATA}/mysql.pid --socket=${MYSQL_DATA}/mysql.socket --skip-networking --skip-grant-tables &> ${MYSQL_DATA}/out &
# Wait for MySQL to start listening to connections
wait_for_line "mysqld: ready for connections." ${MYSQL_DATA}/out
export GNOCCHI_TEST_INDEXER_URL="mysql+pymysql://root@localhost/test?unix_socket=${MYSQL_DATA}/mysql.socket&charset=utf8"
mysql --no-defaults -S ${MYSQL_DATA}/mysql.socket -e 'CREATE DATABASE test;'
trap "clean_exit \"$GNOCCHI_DATA\"" EXIT
source $(which overtest) mysql
mkfifo ${GNOCCHI_DATA}/out
echo '{"default": ""}' > ${GNOCCHI_DATA}/policy.json
cat > ${GNOCCHI_DATA}/gnocchi.conf <<EOF
[oslo_policy]
policy_file = ${GNOCCHI_DATA}/policy.json
policy_file = ${VIRTUAL_ENV}/etc/gnocchi/policy.json
[api]
paste_config = ${VIRTUAL_ENV}/etc/gnocchi/api-paste.ini
[storage]
metric_processing_delay = 1
file_basepath = ${GNOCCHI_DATA}
driver = file
coordination_url = file://${GNOCCHI_DATA}
[indexer]
url = mysql+pymysql://root@localhost/test?unix_socket=${MYSQL_DATA}/mysql.socket&charset=utf8
EOF
cat <<EOF > ${GNOCCHI_DATA}/api-paste.ini
[pipeline:main]
pipeline = gnocchi
[app:gnocchi]
paste.app_factory = gnocchi.rest.app:app_factory
url = ${OVERTEST_URL/#mysql:/mysql+pymysql:}
EOF
gnocchi-upgrade --config-file ${GNOCCHI_DATA}/gnocchi.conf
gnocchi-metricd --config-file ${GNOCCHI_DATA}/gnocchi.conf &>/dev/null &

View File

@ -29,6 +29,19 @@ console_scripts =
keystoneauth1.plugin =
gnocchi-noauth = gnocchiclient.noauth:GnocchiNoAuthLoader
[extras]
test =
coverage>=3.6
python-subunit>=0.0.18
oslotest>=1.10.0 # Apache-2.0
tempest-lib>=0.6.1
testrepository>=0.0.18
testtools>=1.4.0
doc =
sphinx!=1.2.0,!=1.3b1,>=1.1.2
oslosphinx>=2.5.0 # Apache-2.0
[build_sphinx]
source-dir = doc/source
@ -38,16 +51,10 @@ all_files = 1
[upload_sphinx]
upload-dir = doc/build/html
[compile_catalog]
directory = gnocchiclient/locale
domain = gnocchiclient
[pbr]
autodoc_index_modules = true
autodoc_exclude_modules =
gnocchiclient.tests.*
[update_catalog]
domain = gnocchiclient
output_dir = gnocchiclient/locale
input_file = gnocchiclient/locale/gnocchiclient.pot
[extract_messages]
keywords = _ gettext ngettext l_ lazy_gettext
mapping_file = babel.cfg
output_file = gnocchiclient/locale/gnocchiclient.pot
[wheel]
universal = 1

View File

@ -1,19 +0,0 @@
# 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.
hacking<0.11,>=0.10.0
coverage>=3.6
discover
python-subunit>=0.0.18
sphinx!=1.2.0,!=1.3b1,>=1.1.2
oslosphinx>=2.5.0 # Apache-2.0
oslotest>=1.10.0 # Apache-2.0
tempest-lib>=0.6.1
testrepository>=0.0.18
testscenarios>=0.4
testtools>=1.4.0
http://tarballs.openstack.org/gnocchi/gnocchi-master.tar.gz#egg=gnocchi
# FIXME(sileht): should be in gnocchi ?
keystonemiddleware

22
tox.ini
View File

@ -5,36 +5,42 @@ skipsdist = True
[testenv]
usedevelop = True
install_command = pip install -U --allow-external gnocchi --allow-insecure gnocchi {opts} {packages}
setenv =
VIRTUAL_ENV={envdir}
GNOCCHI_CLIENT_EXEC_DIR={envdir}/bin
passenv = GNOCCHI_* OS_TEST_TIMEOUT OS_STDOUT_CAPTURE OS_STDERR_CAPTURE OS_LOG_CAPTURE
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
deps = .[test]
http://tarballs.openstack.org/gnocchi/gnocchi-master.tar.gz#egg=gnocchi[mysql,file]
overtest
commands = {toxinidir}/setup-tests.sh python setup.py test --slowest --testr-args='{posargs}'
[testenv:pep8]
deps = hacking<0.11,>=0.10.0
commands = flake8
[testenv:venv]
deps = .[test,doc]
commands = {posargs}
[testenv:cover]
commands = python setup.py test --coverage --testr-args='{posargs}'
[testenv:docs]
deps = .[test,doc]
commands =
rm -rf doc/source/ref
python setup.py build_sphinx
[testenv:docs-gnocchi.xyz]
deps = .[test,doc]
sphinx_rtd_theme
setenv = SPHINX_HTML_THEME=sphinx_rtd_theme
commands = python setup.py build_sphinx
[testenv:debug]
commands = oslo_debug_helper {posargs}
commands = {toxinidir}/setup-tests.sh oslo_debug_helper {posargs}
[flake8]
# E123, E125 skipped as they are invalid PEP-8.
show-source = True
ignore = E123,E125
builtins = _
ignore =
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build