Skip to content

parisiale/cpp-pcp-client

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cpp-pcp-client

TODO(ale): update urls

TODO(ale): update URI scheme

Introduction

cpp-pcp-client is a C++ client library for the PCP protocol. It includes a collection of abstractions which can be used to initiate connections to a PCP server, wrapping the PCP message format and performing schema validation for message bodies.

A tutorial on how to create a PCP agent / controller pair with cpp-pcp-client is here.

Building the library

Requirements

  • a C++11 compiler (clang/gcc 4.7)
  • gnumake
  • cmake (2.8.12 or newer)
  • boost (1.54 or newer)

Build

Building the library is simple, just run:

make

Tests can be run with:

make test

Usage

Table of Contents
###Important Data Structures

Before we start to look at creating connections and sending/receiving messages, it is important to look at some of the data structures used by cpp-pcp-client.

leatherman::json_container::JsonContainer

The JsonContainer class is used frequently by the cpp-pcp-client library as a simplified abstraction around complex JSON c++ libraries.

ParsedChunks

The Parsed_Chunks struct is a simplification of a parsed PCP message. It allows for direct access of a message's Envelope, Data and Debug chunks as JsonContainer or string objects.

The ParsedChunks struct is defined as:

    struct ParsedChunks {
        // Envelope
        JsonContainer envelope;

        // Data
        bool got_data;
        ContentType data_type;
        JsonContainer data;
        std::string binary_data;

        // Debug
        std::vector<JsonContainer> debug;
    }
###Creating Connections

The first step to interacting with a PCP server is creating a connection. To achieve this we must first create an instance of the Connector object.

The constructor of the Connector class is defined as:

    Connector(const std::string& server_url,
              const std::string& client_type,
              const std::string& ca_crt_path,
              const std::string& client_crt_path,
              const std::string& client_key_path)

The parameters are described as:

  • client_type - A free form value used to group applications connected to a PCP server. For example, an applications connected with the type potato will all be addressable as the PCP endpoint pcp://*/potato. The only value that you cannot use is "server", which is reserved for PCP servers (please refer to the URI section in the PCP specifications).
  • server_url - The URL of the PCP server. For example, wss://localhost:8090/pcp/.
  • ca_crt_path - The path to your CA certificate file.
  • client_crt_path - The path to a client certificate file generated by your CA.
  • client_key_path - The path to a client public key file generated by you CA.

This means that you can instantiate a Connector object as follows:

    Connector connector { "wss://localhost:8090/pcp/", "controller",
                          "/etc/puppet/ssl/ca/ca_crt.pem",
                          "/etc/puppet/ssl/certs/client_crt.pem",
                          "/etc/puppet/ssl/public_keys/client_key.pem" };

When you have created a Connector object you are ready to connect. The Connector's connect method is defined as:

    void connect(int max_connect_attempts = 0) throws (connection_config_error,
                                                       connection_fatal_error)

The parameters are described as:

  • max_connect_attempts - The amount of times the Connector will try and establish a connection to the PCP server if a problem occurs. It will try to connect indefinately when set to 0. Defaults to 0.

The connect method can throw the following exceptions:

    class connection_config_error : public connection_error

This exception will be thrown if a Connector is misconfigured. Misconfiguration includes specifying an invalid server url or a path to a file that doesn't exist. Note that if this exception has been thrown no attempt at creating a network connection has yet been made.

    class connection_fatal_error : public connection_error

This exception wil be thrown if the connection cannot be established after the Connector has tried max_connect_attempts times.

A connection can be established as follows:

    try {
        connector.connect(5);
    } catch (connection_config_error e) {
        ...
    } catch (connection_fatal_error) {
        ...
    }

If no exceptions are thrown it means that a connection has been sucessfuly established. You can check on the status of a connection with the Connector's isConnected method.

The isConnected method is defined as:

    bool isConnected()

And it can be used as follows:

    if (connector.isConnected()) {
        ...
    } else {
        ...
    }

By default a connection is non persistent. For instance, in case WebSocket is used as the underlying transport layer, ping messages must be sent periodically to keep the connection alive. Also, the connection may drop due to communication errors. You can enable connection persistence by calling the monitorConnection method that will periodically check the state of the underlying connection. It will send keepalive messages to the server and attempt to re-establish the connection in case it has been dropped.

monitorConnection is defined as:

    void monitorConnection(int max_connect_attempts = 0)

The parameters are described as:

  • max_connect_attemps -The number of times the Connector will try to reconnect a connection to the PCP server if a problem occurs. It will try to connect indefinately when set to 0. Defaults to 0.

Note that if the Connector fails to re-establish the connection after the specified number of attempts, a connection_fatal_error will be thrown. Also, calling monitorConnection will block the execution thread as the monitoring task will not be executed on a separate thread. On the other hand, the caller can safely execute monitorConnection on a separate thread since the function returns once the Connector destructor is invoked.

    connector.monitorConnection(5);
### Message Schemas and Callbacks

Every message sent over the PCP server has to specify a value for the data_schema field in the message envelope. These data schema's determine how a message's data section is validated. To process messages received from a PCP server you must first create a schema object for a specific data_schema value.

The constructor for the Schema class is defined as:

    Schema(const std::string& name, ContentType content_type)

The parameters are described as:

  • name - The name of the schema. This should be the same as the value found in a message's data_schema field.
  • content_type - Defines the content type of the schema. Valid options are ContentType::Binary and ContentType::Json

A Schema object can be created as follows:

    Schema cnc_request_schema { "cnc_request", ContentType::Json};

You can now start to add constraints to the Schema. Consider the following JSON-schema:

  {
    "title": "cnc_request",
    "type": "object",
    "properties": {
      "module": {
        "type": "string"
      },
      "action": {
        "type": "string"
      },
    },
    "required": ["module"]
  }

You can reproduce its constraints by using the addConstraint method which is defined as follows:

    void addConstraint(std::string field, TypeConstraint type, bool required)

The parameters are described as follows:

  • field - The name of the field you wish to add the constraint to.
  • type - The type constraint to put on the field. Valid types are TypeConstraint::Bool, TypeConstraint::Int, TypeConstraint::Bool, TypeConstraint::Double, TypeConstraint::Array, TypeConstraint::Object, TypeConstraint::Null and TypeConstraint::Any
  • required - Specify whether the field is required to be present or not. If not specified it will default to false.
    cnc_request_schema.addConstraint("module", TypeConstraint::String, true);
    cnc_request_schema.addConstraint("action", TypeConstraint::String);

With a schema defined we can now start to process messages of that type by registering message specific callbacks. This is done with the Connector's registerMessageCallback method which is defined as follows:

    void registerMessageCallback(const Schema schema, MessageCallback callback)

The parameters are described as follows:

  • schema - A previously created schema object
  • callback - A callback function with the signature void(const ParsedChunks& msg_content)

For example:

    void cnc_requestCallback(const ParsedChunks& msg_content) {
      std::cout << "Message envelope: " << msg_content.envelope.toString() << std::endl;

      if (msg_content.has_data()) {
        if (msg_content.data_type == ContentType::Json) {
          std::cout << "Content Type: JSON" << std::endl;
          std::cout << msg_content.data.toString() << std::endl;
        } else {
          std::cout << "Content Type: Binary" << std::endl;
          std::cout << msg_content.binary_data << std::endl;
        }
      }

      for (const auto& debug_chunk : msg_content) {
        std::cout << "Data Chunk: " << debug_chunk << std::endl;
      }
    }

    ...
    connector.registerMessageCallback(cnc_request_schema, cnc_requestCallback);

Now that the callback has been regsitered, every time a message is received where the data_schema field is cnc_request, the content of the Data chunk will be validated against the schema and if it passes, the above defined function will be called. If a message is received which doesn't have a registered data_schema the message will be ignored.

Using this method of registering schema/callback pairs we can handle each message in a unique manner,

    connector.registerMessageCallback(cnc_request_schema, cnc_requestCallback);
    connector.registerMessageCallback(puppet_request_schema, puppet_requestCallback);
    connector.registerMessageCallback(puppet_db_request_schema, puppet_db_requestCallback);

or you can assign one callback to a lot of different schemas,

    connector.registerMessageCallback(schema_1, genericCallback);
    ...
    connector.registerMessageCallback(schema_n, genericCallback);
    connector.registerMessageCallback(schema_n1, genericCallback);
### Sending Messages

Once you have established a connection to the PCP server you can send messages using the send function. There are two overloads for the function that are defined as:

    void send(std::vector<std::string> targets,
              std::string data_schema,
              unsigned int timeout,
              JsonContainer data_json,
              std::vector<JsonContainer> debug = std::vector<JsonContainer> {})
                        throws (connection_processing_error, connection_not_init_error)

With the parameters are described as follows:

  • targets - A vector of the destinations the message will be sent to
  • data_schema - The Schema that identifies the message type
  • timeout - Duration the message will be valid on the fabric
  • data_json - A JsonContainer representing the data chunk of the message
  • debug - A vector of strings representing the debug chunks of the message (defaults to empty)
    void send(std::vector<std::string> targets,
              std::string data_schema,
              unsigned int timeout,
              std::string data_binary,
              std::vector<JsonContainer> debug = std::vector<JsonContainer> {})
                        throws (connection_processing_error, connection_not_init_error)

With the parameters are described as follows:

  • targets - A vector of the destinations the message will be sent to
  • data_schema - The Schema that identifies the message type
  • timeout - Duration the message will be valid on the fabric
  • data_binary - A string representing the data chunk of the message
  • debug - A vector of strings representing the debug chunks of the message (defaults to empty)

The send methods can throw the following exceptions:

    class connection_processing_error : public connection_error

This exception is thrown when an error occurs during at the underlying WebSocket layer.

    class connection_not_init_error : public connection_error

This exception is thrown when trying to send a message when there is no active connection to the server.

Example usage:

    JsonContainer data {};
    data.set<std::string>("foo", "bar");
    try {
      connector.send({"pcp://*/potato"}, "potato_schema", 42, data);
    } catch (connection_not_init_error e) {
      std::cout << "Cannot send message without being connected to the server" << std::endl;
    } catch (connection_processing_error e) {
      std::cout << "An error occured at the WebSocket layer: " << e.what() << std::endl;
   }
### Data Validation

As mentioned in the Message Schemas and Callbacks, messages received from the PCP server will be matched against a Schema that you defined. The Connector object achieves this functionality by using an instance of the Validator class. It is possible to instantiate your own instance of the Validator class and use schema's to validate other, non message, data structures.

The Validator is limited to a no-args constructor:

    Validator()

You can register a Schema by using the registerSchema method, defined as:

    void registerSchema(const Schema& schema) throws (schema_redefinition_error)

The parameters are described as follows:

  • schema - A schema object that desribes a set of constraints.

When a Schema has been registered you can use the validate method to validate a JsonContainer object. The validate method is defined as follows:

    void validate(JsonContainer& data, std::string schema_name) const throws (validation_error)

The parameters are described as follows:

  • data - A JsonContainer you want to validate.
  • schema_name - The name of the schema you want to validate against.

Example usage:

    Validator validator {};
    Schema s {"test-schema", ContentType::Json };
    s.addConstraint("foo", TypeConstraint::Int);
    validator.registerSchema(s);

    JsonContainer d {};
    d.set<int>("foo", 42);

    try {
      Validator.validate(d, "test-schema");
    } catch (validation_error) {
      std::cout << "Validation failed" << std::endl;
    }

About

Client libraries for the PCP protocol

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C++ 91.1%
  • CMake 3.4%
  • Python 2.9%
  • HTML 1.3%
  • Other 1.3%