Nir Soffer has uploaded a new change for review.

Change subject: vdsm-imaged: Support random io to oVirt disks
......................................................................

vdsm-imaged: Support random io to oVirt disks

vdsm-imaged provides direct access to oVirt disks using HTTPS protocol.
Together with oVirt transfer proxy, it allows uploading a disk image
directly into an oVirt disk, or downloading an oVirt disk.

Dependencies

- python-webob - small library simplifying writing WSGI HTTP servers
- py.test      - more convenient unit testing framework

This is work in progress; still missing:

- Accessing storage using dd
- Handling of partial content (Content-Range, Rage)
- Python 3 support (some modules were renamed in Python 3)

Change-Id: If3339fa94ef8464228cd036f4fe8eea61887e337
Signed-off-by: Nir Soffer <[email protected]>
---
A vdsm-imaged/README
A vdsm-imaged/imaged.py
A vdsm-imaged/imaged_test.py
A vdsm-imaged/pki/certs/vdsmcert.pem
A vdsm-imaged/pki/keys/vdsmkey.pem
5 files changed, 671 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.ovirt.org:29418/vdsm refs/changes/24/41824/4

diff --git a/vdsm-imaged/README b/vdsm-imaged/README
new file mode 100644
index 0000000..7206245
--- /dev/null
+++ b/vdsm-imaged/README
@@ -0,0 +1,41 @@
+vdsm-imaged daemon
+------------------
+
+vdsm-imaged provides direct access to oVirt disks using HTTPS protocol.
+Together with oVirt transfer proxy, it allows uploading a disk image
+directly into an oVirt disk, or downloading an oVirt disk.
+
+The goal is to keep vdsm-imaged simple as possible. We use a single
+protocol (HTTPS) for everything, and avoid dependencies on Vdsm.
+
+This daemon provides two services:
+
+- image service   read and write data to/from images. This service is
+                  available via HTTP on port 54322.
+
+- ticket service  manage session tickets authorizing image service
+                  operations. This service is available via HTTP over
+                  unix domain socket.
+
+Transfer session flow
+
+- Client starts an engine transfer session using oVirt REST API.
+- Engine creates session tickets
+- Engine ask Vdsm to start a transfer session with a ticket.
+- Vdsm prepares an image, and add the session ticket to vdsm-imaged.
+- Engine returns signed session tickets to client.
+- Client perform io operations with oVirt transfer proxy using the
+  signed session ticket.
+- oVirt transfer proxy performs random io operations with vdsm-imaged
+  using the session ticket uuid.
+- When client is done, it ends the engine transfer session.
+- Engine ends the vdsm transfer session.
+- Vdsm deletes session ticket from vdsm-imaged.
+
+Session tickets are ephemeral; A client needs to request Engine to renew
+the ticket from time to time, otherwise a ticket will expire and the
+transfer session will be aborted.
+
+Session tickets are not persisted. In case of vdsm-imaged crash or
+reboot, Engine will provide a new session ticket and possibly point
+client to another host to continue the transfer session.
diff --git a/vdsm-imaged/imaged.py b/vdsm-imaged/imaged.py
new file mode 100644
index 0000000..ba96fb6
--- /dev/null
+++ b/vdsm-imaged/imaged.py
@@ -0,0 +1,344 @@
+# vdsm-imaged - vdsm image daemon
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+from contextlib import contextmanager
+from wsgiref import simple_server
+import SocketServer
+import json
+import os
+import re
+import signal
+import ssl
+import sys
+import threading
+import time
+
+import webob
+
+from webob.exc import (
+    HTTPException,
+    HTTPBadRequest,
+    HTTPMethodNotAllowed,
+    HTTPNotFound,
+    HTTPForbidden
+)
+
+from vdsm import uhttp
+
+image_server = None
+ticket_server = None
+tickets = {}
+running = True
+
+
+def main(args):
+    config = Config()
+    signal.signal(signal.SIGINT, terminate)
+    signal.signal(signal.SIGTERM, terminate)
+    start(config)
+    try:
+        while running:
+            time.sleep(30)
+    finally:
+        stop()
+
+
+def terminate(signo, frame):
+    global running
+    running = False
+
+
+def start(config):
+    global image_server, ticket_server
+    assert not (image_server or ticket_server)
+
+    image_server = ThreadedWSGIServer((config.host, config.port),
+                                      WSGIRequestHandler)
+    secure_server(config, image_server)
+    image_app = Application(config, [(r"/images/(.*)", Images)])
+    image_server.set_app(image_app)
+
+    ticket_server = uhttp.UnixWSGIServer(config.socket, UnixWSGIRequestHandler)
+    secure_server(config, ticket_server)
+    ticket_app = Application(config, [(r"/tickets/(.*)", Tickets)])
+    ticket_server.set_app(ticket_app)
+
+    start_server(config, image_server, "image.server")
+    start_server(config, ticket_server, "ticket.server")
+
+
+def stop():
+    global image_server, ticket_server
+    image_server.shutdown()
+    ticket_server.shutdown()
+    image_server = None
+    ticket_server = None
+
+
+def secure_server(config, server):
+    server.socket = ssl.wrap_socket(server.socket, certfile=config.cert_file,
+                                    keyfile=config.key_file, server_side=True)
+
+
+def start_server(config, server, name):
+    def run():
+        server.serve_forever(poll_interval=config.poll_interval)
+
+    t = threading.Thread(target=run, name=name)
+    t.daemon = True
+    t.start()
+
+
+class Config(object):
+
+    pki_dir = "/etc/pki/vdsm"
+    host = ""
+    port = 54322
+    repo_dir = "/var/run/vdsm/storage"
+    poll_interval = 1.0
+    buffer_size = 64 * 1024
+    socket = "/var/run/vdsm/imaged.socket"
+
+    @property
+    def key_file(self):
+        return os.path.join(self.pki_dir, "keys", "vdsmkey.pem")
+
+    @property
+    def cert_file(self):
+        return os.path.join(self.pki_dir, "certs", "vdsmcert.pem")
+
+
+def error_response(e):
+    """
+    Return WSGI application for sending error response using JSON format.
+    """
+    payload = {
+        "code": e.code,
+        "title": e.title,
+        "explanation": e.explanation
+    }
+    if e.detail:
+        payload["detail"] = e.detail
+    return response(status=e.code, payload=payload)
+
+
+def response(status=200, payload=None):
+    """
+    Return WSGI application for sending response in JSON format.
+    """
+    body = json.dumps(payload) if payload else ""
+    return webob.Response(status=status, body=body,
+                          content_type="application/json")
+
+
+class Application(object):
+    """
+    WSGI application dispatching requests based on path and method to request
+    handlers.
+    """
+
+    def __init__(self, config, routes):
+        self.config = config
+        self.routes = [(re.compile(pattern), cls) for pattern, cls in routes]
+
+    def __call__(self, env, start_response):
+        request = webob.Request(env)
+        try:
+            resp = self.dispatch(request)
+        except HTTPException as e:
+            resp = error_response(e)
+        return resp(env, start_response)
+
+    def dispatch(self, request):
+        method_name = request.method.lower()
+        if method_name.startswith("_"):
+            raise HTTPMethodNotAllowed("Invalid method %r" %
+                                       request.method)
+        path = request.path_info
+        for route, handler_class in self.routes:
+            match = route.match(path)
+            if match:
+                handler = handler_class(self.config, request)
+                try:
+                    method = getattr(handler, method_name)
+                except AttributeError:
+                    raise HTTPMethodNotAllowed(
+                        "Method %r not defined for %r" %
+                        (request.method, path))
+                else:
+                    request.path_info_pop()
+                    return method(*match.groups())
+        raise HTTPNotFound("No handler for %r" % path)
+
+
+def get_ticket(ticket_id, op, size):
+    """
+    Return a ticket for the requested operation, authorizing the operation.
+    """
+    try:
+        ticket = tickets[ticket_id]
+    except KeyError:
+        raise HTTPForbidden("No such ticket %r" % ticket_id)
+    if ticket["expires"] >= monotonic_time():
+        raise HTTPForbidden("Ticket %r expired" % ticket_id)
+    if op not in ticket["ops"]:
+        raise HTTPForbidden("Ticket %r forbids %r" % (ticket_id, op))
+    if size > ticket["size"]:
+        raise HTTPForbidden("Content-Length out of allowed range")
+    return ticket
+
+
+@contextmanager
+def register_request(ticket, request_id, request):
+    """
+    Context manager registring a request with a ticket, so requests can be
+    canceled when a ticket is revoked or expired.
+    """
+    requests = ticket.setdefault("requests", {})
+    if request_id in requests:
+        raise HTTPForbidden("Request id %r exists" % request_id)
+    requests[request_id] = request
+    try:
+        yield
+    finally:
+        del requests[request_id]
+
+
+class Images(object):
+    """
+    Request handler for the /images/ resource.
+    """
+
+    def __init__(self, config, request):
+        self.config = config
+        self.request = request
+        self.canceled = False
+
+    def put(self, ticket_id):
+        if not ticket_id:
+            raise HTTPBadRequest("Ticket id is required")
+        request_id = self.request.params.get("id")
+        if not request_id:
+            raise HTTPBadRequest("Request id is required")
+        size = self.request.content_length
+        ticket = get_ticket(ticket_id, "put", size)
+        with register_request(ticket, request_id, self):
+            self._copy_to_image(ticket["path"], size)
+        return response()
+
+    def _copy_to_image(self, path, size):
+        # TODO: Use dd for writing
+        body_file = self.request.body_file
+        with open(path, "r+b") as out:
+            bufsize = self.config.buffer_size
+            bytes_written = 0
+            while bytes_written < size:
+                bytes_to_read = min(size - bytes_written, bufsize)
+                block = body_file.read(bytes_to_read)
+                if self.canceled:
+                    raise HTTPForbidden("Ticket was revoked during copy")
+                out.write(block)
+                if self.canceled:
+                    raise HTTPForbidden("Ticket was revoked during copy")
+                bytes_written += len(block)
+
+
+class Tickets(object):
+    """
+    Request handler for the /tickets/ resource.
+    """
+
+    def __init__(self, config, request):
+        self.config = config
+        self.request = request
+
+    def get(self, ticket_id):
+        if not ticket_id:
+            raise HTTPBadRequest("Ticket id is required")
+        try:
+            ticket = tickets[ticket_id]
+        except KeyError:
+            raise HTTPNotFound("No such ticket %r" % ticket_id)
+        return response(payload=ticket)
+
+    def put(self, ticket_id):
+        # TODO
+        # - reject invalid or expired ticket
+        # - start expire timer
+        if not ticket_id:
+            raise HTTPBadRequest("Ticket id is required")
+        try:
+            ticket = self.request.json
+        except ValueError as e:
+            raise HTTPBadRequest("Invalid ticket: %s" % e)
+        tickets[ticket_id] = ticket
+        return response()
+
+    def patch(self, ticket_id):
+        # TODO: restart expire timer
+        if not ticket_id:
+            raise HTTPBadRequest("Ticket id is required")
+        try:
+            patch = self.request.json
+        except ValueError as e:
+            raise HTTPBadRequest("Invalid patch: %s" % e)
+        try:
+            new_expires = patch["expires"]
+        except KeyError:
+            raise HTTPBadRequest("Missing expires key")
+        tickets[ticket_id]["expires"] = new_expires
+        return response()
+
+    def delete(self, ticket_id):
+        # TODO: cancel requests using deleted tickets
+        ticket_id = self.request.path_info_peek()
+        if ticket_id:
+            try:
+                del tickets[ticket_id]
+            except KeyError:
+                raise HTTPNotFound("No such ticket %r" % ticket_id)
+        else:
+            tickets.clear()
+        return response(status=204)
+
+
+class ThreadedWSGIServer(SocketServer.ThreadingMixIn,
+                         simple_server.WSGIServer):
+    """
+    Threaded WSGI HTTP server.
+    """
+    daemon_threads = True
+
+
+class WSGIRequestHandler(simple_server.WSGIRequestHandler):
+    """
+    WSGI request handler using HTTP/1.1.
+    """
+
+    protocol_version = "HTTP/1.1"
+
+    def address_string(self):
+        """
+        Override to avoid slow and unneeded name lookup.
+        """
+        return self.client_address[0]
+
+
+class UnixWSGIRequestHandler(uhttp.UnixWSGIRequestHandler):
+    """
+    WSGI over unix domain socket request handler using HTTP/1.1.
+    """
+    protocol_version = "HTTP/1.1"
+
+
+def monotonic_time():
+    return os.times()[4]
+
+
+if __name__ == "__main__":
+    main(sys.argv[1:])
diff --git a/vdsm-imaged/imaged_test.py b/vdsm-imaged/imaged_test.py
new file mode 100644
index 0000000..afa5e5e
--- /dev/null
+++ b/vdsm-imaged/imaged_test.py
@@ -0,0 +1,240 @@
+# vdsm-imaged - vdsm image daemon
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+from __future__ import print_function
+
+import os
+import sys
+if os.path.isdir("../lib/vdsm"):
+    sys.path.insert(0, "../lib")
+
+from contextlib import contextmanager, closing
+from pprint import pprint
+import httplib
+import json
+import uuid
+
+from vdsm import uhttp
+
+import imaged
+
+
+def setup_function(f):
+    imaged.tickets.clear()
+
+
+def test_ticket_no_resource():
+    config = Config()
+    with server(config):
+        res = unix_request(config, "GET", "/no/such/resource")
+    assert res.status == 404
+
+
+def test_ticket_no_method():
+    config = Config()
+    with server(config):
+        res = unix_request(config, "POST", "/tickets/")
+    assert res.status == 405
+
+
+def test_ticket_get():
+    config = Config()
+    ticket = create_ticket()
+    imaged.tickets[ticket["uuid"]] = ticket
+    with server(config):
+        res = unix_request(config, "GET", "/tickets/%(uuid)s" % ticket)
+    assert res.status == 200
+    assert json.loads(res.read()) == ticket
+
+
+def test_ticket_get_not_found():
+    config = Config()
+    with server(config):
+        res = unix_request(config, "GET", "/tickets/%s" % uuid.uuid4())
+    assert res.status == 404
+
+
+def test_ticket_put():
+    config = Config()
+    ticket = create_ticket()
+    body = json.dumps(ticket)
+    with server(config):
+        res = unix_request(config, "PUT", "/tickets/%(uuid)s" % ticket, body)
+    assert res.status == 200
+    assert imaged.tickets[ticket["uuid"]] == ticket
+
+
+def test_ticket_put_invalid_json():
+    config = Config()
+    with server(config):
+        res = unix_request(config, "PUT", "/tickets/", "invalid json")
+    assert res.status == 400
+
+
+def test_ticket_extend():
+    config = Config()
+    ticket = create_ticket()
+    imaged.tickets[ticket["uuid"]] = ticket
+    patch = {"expires": ticket["expires"] + 300}
+    body = json.dumps(patch)
+    with server(config):
+        res = unix_request(config, "PATCH", "/tickets/%(uuid)s" % ticket, body)
+    assert res.status == 200
+    assert ticket["expires"] == patch["expires"]
+
+
+def test_ticket_delete_one():
+    config = Config()
+    ticket = create_ticket()
+    imaged.tickets[ticket["uuid"]] = ticket
+    with server(config):
+        res = unix_request(config, "DELETE", "/tickets/%(uuid)s" % ticket)
+    assert res.status == 204
+    assert ticket["uuid"] not in imaged.tickets
+
+
+def test_ticket_delete_one_not_found():
+    config = Config()
+    with server(config):
+        res = unix_request(config, "DELETE", "/tickets/%s" % uuid.uuid4())
+    assert res.status == 404
+
+
+def test_ticket_delete_all():
+    # Example usage: move host to maintenance
+    config = Config()
+    for i in range(5):
+        ticket = create_ticket(path="/var/run/vdsm/storage/foo%s" % i)
+        imaged.tickets[ticket["uuid"]] = ticket
+    with server(config):
+        res = unix_request(config, "DELETE", "/tickets/")
+    assert res.status == 204
+    assert imaged.tickets == {}
+
+
+def test_images_no_resource():
+    config = Config()
+    with server(config):
+        res = http_request(config, "PUT", "/no/such/resource")
+    assert res.status == 404
+
+
+def test_images_no_method():
+    config = Config()
+    with server(config):
+        res = http_request(config, "POST", "/images/")
+    assert res.status == 405
+
+
+def test_images_upload_no_request_id(tmpdir):
+    payload = create_tempfile(tmpdir, "payload", "content")
+    repo = create_repo(tmpdir)
+    config = Config(repo)
+    ticket = create_ticket()
+    imaged.tickets[ticket["uuid"]] = ticket
+    with server(config):
+        res = upload(config, ticket["uuid"], "", str(payload))
+    assert res.status == 400
+
+
+def test_images_upload_no_ticket_id(tmpdir):
+    payload = create_tempfile(tmpdir, "payload", "content")
+    repo = create_repo(tmpdir)
+    config = Config(repo)
+    request_id = str(uuid.uuid4())
+    with server(config):
+        res = upload(config, "", request_id, str(payload))
+    assert res.status == 400
+
+
+def test_images_upload_no_ticket(tmpdir):
+    payload = create_tempfile(tmpdir, "payload", "content")
+    repo = create_repo(tmpdir)
+    config = Config(repo)
+    ticket_id = str(uuid.uuid4())
+    request_id = str(uuid.uuid4())
+    with server(config):
+        res = upload(config, ticket_id, request_id, str(payload))
+    assert res.status == 403
+
+
+class Config(imaged.Config):
+    host = "127.0.0.1"
+    socket = "/tmp/vdsm-imaged.sock"
+    pki_dir = os.path.join(os.path.dirname(__file__), "pki")
+    poll_interval = 0.1
+
+    def __init__(self, repo_dir="/var/tmp"):
+        self.repo_dir = str(repo_dir)
+
+
+def create_ticket(ops=("get", "put"), timeout=300, size=2**64,
+                  path="/var/run/vdsm/storage/foo"):
+    return {
+        "uuid": str(uuid.uuid4()),
+        "expires": int(imaged.monotonic_time()) + timeout,
+        "ops": list(ops),
+        "size": size,
+        "path": path,
+    }
+
+
+def upload(config, ticket_uuid, request_uuid, filename):
+    uri = "/images/%s?id=%s" % (ticket_uuid, request_uuid)
+    with open(filename) as f:
+        return http_request(config, "PUT", uri, f)
+
+
+def http_request(config, method, uri, body=None, headers=None):
+    con = httplib.HTTPSConnection("127.0.0.1", config.port, config.key_file,
+                                  config.cert_file)
+    with closing(con):
+        con.request(method, uri, body=body, headers=headers or {})
+        return response(con)
+
+
+def unix_request(config, method, uri, body=None, headers=None):
+    con = uhttp.UnixHTTPSConnection(config.socket, config.key_file,
+                                    config.cert_file)
+    with closing(con):
+        con.request(method, uri, body=body, headers=headers or {})
+        return response(con)
+
+
+def response(con):
+    res = con.getresponse()
+    pprint((res.status, res.reason))
+    pprint(res.getheaders())
+    if res.status >= 400:
+        print(res.read())
+    return res
+
+
+def create_tempfile(tmpdir, name, data=''):
+    file = tmpdir.join(name)
+    file.write(data)
+    return file
+
+
+def create_repo(tmpdir):
+    return tmpdir.mkdir("storage")
+
+
+def create_volume(repo, domain, image, volume, data=''):
+    volume = repo.mkdir(domain).mkdir("images").mkdir(image).join(volume)
+    volume.write(data)
+    return volume
+
+
+@contextmanager
+def server(config):
+    imaged.start(config)
+    try:
+        yield
+    finally:
+        imaged.stop()
diff --git a/vdsm-imaged/pki/certs/vdsmcert.pem 
b/vdsm-imaged/pki/certs/vdsmcert.pem
new file mode 100644
index 0000000..5bc9352
--- /dev/null
+++ b/vdsm-imaged/pki/certs/vdsmcert.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDEjCCAfoCCQCNg3LPIL2ZWTANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJV
+UzEMMAoGA1UECAwDRm9vMQwwCgYDVQQHDANCYXIxDDAKBgNVBAoMA0RpczESMBAG
+A1UEAwwJMTI3LjAuMC4xMB4XDTE1MDUxOTE4MTEzM1oXDTE2MDUxODE4MTEzM1ow
+SzELMAkGA1UEBhMCVVMxDDAKBgNVBAgMA0ZvbzEMMAoGA1UEBwwDQmFyMQwwCgYD
+VQQKDANEaXMxEjAQBgNVBAMMCTEyNy4wLjAuMTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAMw+K2KcmwVOV0d6XCGWC915+wBFGYNx/vwiDk9CR4jSmGNA
+ufWVxB62qOl++WEenfVMSgM9ZcG7j6b7s1HvPnzpj4iNrfiugAC8tDCukdOZa60D
+xRcrMttdXFAq7McwO2z7klS+oFkjMZ5cDIoGSsf2hweGPDfrfKWrGPBJO4nSnA6c
+UPU43XXntFVWlXOpADVO8R0rRtAlRu4ec8ibiQZ97ZM5xF72YR3RQac18miaerqq
+nyH1QInJQGob3T66uV+Sg36atwqi/TszqTvyGiwLOSQBpMYAL+XbRcrG7+lSZR4J
+y95qVqajT6CaU6YTKNrE8Ne+LKJ8ur7S4JVzoGkCAwEAATANBgkqhkiG9w0BAQUF
+AAOCAQEAZf+UXtg7hrMIv9+libnGMqK/BH4gRUpkFiIhPI2/7lDRM3m7zqaalCH9
+EkrSFSTfV1NNxs12ZQS3OWeZRDXMcUQ0iKXOFC+0vzjejWGiy5P4h1yUFuxOoigO
+5irj6BF6ZvZES5UVSqYGNhHTltyY/oGCdD8KutXZoh2MHCURRR9NVlhmgQGmS9TW
+TsH/ZwYNCFvpgPrX5MsSWEUyggsEk66/TNrCOk9RJ0qZ+Kmm7tPQwvI3ug7Izb5c
+OcGunj0VgM0CIQbf9tnzSFR1ZO5eYka6kRVohwCdecl449K+45IX5pmBT5CXqtxN
+X4JBlJYQGjtZe6McB0AWFjgmpFP5qg==
+-----END CERTIFICATE-----
diff --git a/vdsm-imaged/pki/keys/vdsmkey.pem b/vdsm-imaged/pki/keys/vdsmkey.pem
new file mode 100644
index 0000000..103117c
--- /dev/null
+++ b/vdsm-imaged/pki/keys/vdsmkey.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAzD4rYpybBU5XR3pcIZYL3Xn7AEUZg3H+/CIOT0JHiNKYY0C5
+9ZXEHrao6X75YR6d9UxKAz1lwbuPpvuzUe8+fOmPiI2t+K6AALy0MK6R05lrrQPF
+Fysy211cUCrsxzA7bPuSVL6gWSMxnlwMigZKx/aHB4Y8N+t8pasY8Ek7idKcDpxQ
+9Tjddee0VVaVc6kANU7xHStG0CVG7h5zyJuJBn3tkznEXvZhHdFBpzXyaJp6uqqf
+IfVAiclAahvdPrq5X5KDfpq3CqL9OzOpO/IaLAs5JAGkxgAv5dtFysbv6VJlHgnL
+3mpWpqNPoJpTphMo2sTw174sony6vtLglXOgaQIDAQABAoIBAD+8Csfb/NgcCUpp
+2YQ1kYBMh1IfPgXxtdMyQWrkUPRWW21ljmIfmTLIZ09t6x4ucrZQVyxJpY5eHEbM
+drnTwZkzPTIsnCRlN9aDDGvAngr87kfwTDmdpmIj8SGnM3o5B+JLYu+FCP6n59z1
+9oe/zOg3ew2TCwmcN6pvCPo6sBSij+k4irOkYbCsTKoQzYcaBsKT/RoCVNnrArhX
+e6nt9c0JvSci7iCl5hODDxz30Kwyu9msUTEi0J0AGlhrPRrJQebjKFk+KwDYhJ7O
+OAsqiXWtENnokmsNuxnJXX3U28tD/uwbbt+RH/8h4E4+FCz9mPvmbGSFAIqhD0qe
+hLlPGAECgYEA+5kg66Uk6tPyyPHTqg9Azhhu1CrItF7kfujzlJZPBMS+aoSFqKx8
+dg4MhfTZdweT9iHajNXMr6R4E+Vx2xDM3MLmiUojRVLtkhWZpf1AgwAFqQ4QBEDI
+RpgAXpGfxFHPbFK/1et48EzXXCaRafJDQn860F8S5mXGFVhBkajoRQECgYEAz9Dx
+gF+y8ZsCbzVT4grsYnlFnF1hkobiqkLaLrqb7rUkajlM4jvLnMIRvAJjUjVCJtvu
+RBMnFFv2sQMF8dS5KoILvCl+nSEAUZtOy6lP+2LeKbdRK7WsMvbmsQGriZLIVIkD
+/9m7d2WNa2hUdvOXvuIJlCLHrSMx3x+4O+/QU2kCgYEAxyJQ8xG3oYF1tOPqUrxV
+34lpFtZyGojMs0HvoJhDHJQX3jjbjUBQFiRSdyfvgw1lZ7ctwrBpnE9BlwXQUsH+
+U3CThPA/8FuNm0UrYjyK9eRrln2B4cvgeDdQe4ko+fqCgMsR+N+xzggToGUKTN+p
+qpUG2OuXCvJckIJVC1Oz2wECgYEAsZMYyVj8zvZlXiFzS/OJiZuWn8YrWcloZZQ6
+WzOZip++PgY4bBgsJAawoLZpqBVaMVo8fm7fNcZfRWIP8lSS5H+7B01to1ZPr2vG
+KMDEV0pkC8FY2sCiI+pVtWp36VZDV/i8MiMazSs5bE353qrHP5RmGu6dMJSiSMYR
+5yVEHgECgYBj05Bq5fQl2nM7E6yLbTnAeVQIJzvF+2m/ttdGx0hcLSVHQBjs3+tH
+v9G5V5OIHXgFloT+tbmIIf+D/VuGZmluH8FoEFR7evK/64wu0qQkueTcTrfsjBh3
+0lhpHCfPafk6EYN0yU6Xb4MxNDmCat7NeUFPgnX2epM8nvD8he5lng==
+-----END RSA PRIVATE KEY-----


-- 
To view, visit https://gerrit.ovirt.org/41824
To unsubscribe, visit https://gerrit.ovirt.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: If3339fa94ef8464228cd036f4fe8eea61887e337
Gerrit-PatchSet: 4
Gerrit-Project: vdsm
Gerrit-Branch: master
Gerrit-Owner: Nir Soffer <[email protected]>
Gerrit-Reviewer: Adam Litke <[email protected]>
Gerrit-Reviewer: Allon Mureinik <[email protected]>
Gerrit-Reviewer: Alon Bar-Lev <[email protected]>
Gerrit-Reviewer: Amit Aviram <[email protected]>
Gerrit-Reviewer: Dan Kenigsberg <[email protected]>
Gerrit-Reviewer: Daniel Erez <[email protected]>
Gerrit-Reviewer: Federico Simoncelli <[email protected]>
Gerrit-Reviewer: Francesco Romani <[email protected]>
Gerrit-Reviewer: Greg Padgett <[email protected]>
Gerrit-Reviewer: Jenkins CI
Gerrit-Reviewer: Nir Soffer <[email protected]>
Gerrit-Reviewer: Yaniv Bronhaim <[email protected]>
Gerrit-Reviewer: [email protected]
_______________________________________________
vdsm-patches mailing list
[email protected]
https://lists.fedorahosted.org/mailman/listinfo/vdsm-patches

Reply via email to