diff --git a/api/docs/http_catalog.md b/api/docs/http_catalog.md index 2e2c9651b..3cce78d1a 100644 --- a/api/docs/http_catalog.md +++ b/api/docs/http_catalog.md @@ -1,159 +1,153 @@ Catalog ============= The `catalog` endpoint returns a catalog for the specified node name given the provided facts. Find ---- Retrieve a catalog. POST /:environment/catalog/:nodename GET /:environment/catalog/:nodename ### Supported HTTP Methods POST, GET ### Supported Response Formats PSON ### Notes The POST and GET methods are functionally equivalent. Both provide the 3 parameters specified below: the POST in the request body, the GET in the query string. Puppet originally used GET; POST was added because some web servers have a maximum URI length of 1024 bytes (which is easily exceeded with the `facts` parameter). The examples below use the POST method. ### Parameters Three parameters should be provided to the POST or GET: - `facts_format`: must be `pson` - `facts`: serialized pson of the facts hash. One odd note: due to a long-ago misunderstanding in the code, this is doubly-escaped (it should just be singly-escaped). To keep backward compatibility, the extraneous escaping is still used/supported. - `transaction_uuid`: a transaction uuid identifying the entire transaction (shows up in the report as well) ### Example Response #### Catalog found POST /env/catalog/elmo.mydomain.com facts_format=pson&facts=%7B%22name%22%3A%22elmo.mydomain.com%22%2C%22values%22%3A%7B%22architecture%22%3A%22x86_64%22%7D&transaction_uuid=aff261a2-1a34-4647-8c20-ff662ec11c4c HTTP 200 OK Content-Type: text/pson { - "document_type": "Catalog", - "data": { - "tags": [ - "settings", - "multi_param_class", - "class" - ], - "name": "elmo.mydomain.com", - "version": 1377473054, - "environment": "production", - "resources": [ - { - "type": "Stage", - "title": "main", - "tags": [ - "stage" - ], - "exported": false, - "parameters": { - "name": "main" - } - }, - { - "type": "Class", - "title": "Settings", - "tags": [ - "class", - "settings" - ], - "exported": false - }, - { - "type": "Class", - "title": "main", - "tags": [ - "class" - ], - "exported": false, - "parameters": { - "name": "main" - } - }, - { - "type": "Class", - "title": "Multi_param_class", - "tags": [ - "class", - "multi_param_class" - ], - "line": 10, - "exported": false, - "parameters": { - "one": "hello", - "two": "world" - } - }, - { - "type": "Notify", - "title": "foo", - "tags": [ - "notify", - "foo", - "class", - "multi_param_class" - ], - "line": 4, - "exported": false, - "parameters": { - "message": "One is hello, two is world" - } + "tags": [ + "settings", + "multi_param_class", + "class" + ], + "name": "elmo.mydomain.com", + "version": 1377473054, + "environment": "production", + "resources": [ + { + "type": "Stage", + "title": "main", + "tags": [ + "stage" + ], + "exported": false, + "parameters": { + "name": "main" } - ], - "edges": [ - { - "source": "Stage[main]", - "target": "Class[Settings]" - }, - { - "source": "Stage[main]", - "target": "Class[main]" - }, - { - "source": "Stage[main]", - "target": "Class[Multi_param_class]" - }, - { - "source": "Class[Multi_param_class]", - "target": "Notify[foo]" + }, + { + "type": "Class", + "title": "Settings", + "tags": [ + "class", + "settings" + ], + "exported": false + }, + { + "type": "Class", + "title": "main", + "tags": [ + "class" + ], + "exported": false, + "parameters": { + "name": "main" } - ], - "classes": [ - "settings", - "multi_param_class" - ] - }, - "metadata": { - "api_version": 1 - } + }, + { + "type": "Class", + "title": "Multi_param_class", + "tags": [ + "class", + "multi_param_class" + ], + "line": 10, + "exported": false, + "parameters": { + "one": "hello", + "two": "world" + } + }, + { + "type": "Notify", + "title": "foo", + "tags": [ + "notify", + "foo", + "class", + "multi_param_class" + ], + "line": 4, + "exported": false, + "parameters": { + "message": "One is hello, two is world" + } + } + ], + "edges": [ + { + "source": "Stage[main]", + "target": "Class[Settings]" + }, + { + "source": "Stage[main]", + "target": "Class[main]" + }, + { + "source": "Stage[main]", + "target": "Class[Multi_param_class]" + }, + { + "source": "Class[Multi_param_class]", + "target": "Notify[foo]" + } + ], + "classes": [ + "settings", + "multi_param_class" + ] } Schema ------ In the POST request body (or the GET query), the facts parameter should adhere to the {file:api/schemas/facts.json api/schemas/facts.json} schema. A catalog response body should adhere to the {file:api/schemas/catalog.json api/schemas/catalog.json} schema. diff --git a/api/docs/http_file_metadata.md b/api/docs/http_file_metadata.md index e84c1fe14..d5c716d83 100644 --- a/api/docs/http_file_metadata.md +++ b/api/docs/http_file_metadata.md @@ -1,436 +1,352 @@ File Metadata ============= The `file_metadata` endpoint returns select metadata for a single file or many files. There are find and search variants of the endpoint; the search variant has a trailing 's' so is actually `file_metadatas`. Although the term 'file' is used generically in the endpoint name and documentation, each returned item can be one of the following three types: * file * directory * symbolic link Note that an `:environment` must be specified in the endpoint, but is actually ignored since the puppet file server is not environment-specific. (In fact, the specified `:environment` does even need to be valid.) The endpoint path includes a `:mount` which can be one of three types: * custom file serving mounts as specified in fileserver.conf -- see [the puppet file serving guide](http://docs.puppetlabs.com/guides/file_serving.html#serving-files-from-custom-mount-points) * `modules/` -- a semi-magical mount point which allows access to the `files` subdirectory of `module` -- see [the puppet file serving guide](http://docs.puppetlabs.com/guides/file_serving.html#serving-module-files) * `plugins` -- a highly magical mount point which merges many directories together: used for plugin sync, sub-paths can not be specified, not intended for general consumption Note: pson responses in the examples below are pretty-printed for readability. Find ---- Get file metadata for a single file GET /:environment/file_metadata/:mount/path/to/file ### Supported HTTP Methods GET ### Supported Response Formats PSON ### Parameters None ### Example Response #### File metadata found for a file GET /env/file_metadata/modules/example/just_a_file.txt HTTP/1.1 200 OK Content-Type: text/pson { - "data": { - "checksum": { - "type": "md5", - "value": "{md5}d0a10f45491acc8743bc5a82b228f89e" - }, - "destination": null, - "group": 20, - "links": "manage", - "mode": 420, - "owner": 501, - "path": "/etc/puppet/conf/modules/example/files/just_a_file.txt", - "relative_path": null, - "type": "file" + "checksum": { + "type": "md5", + "value": "{md5}d0a10f45491acc8743bc5a82b228f89e" }, - "document_type": "FileMetadata", - "metadata": { - "api_version": 1 - } + "destination": null, + "group": 20, + "links": "manage", + "mode": 420, + "owner": 501, + "path": "/etc/puppet/conf/modules/example/files/just_a_file.txt", + "relative_path": null, + "type": "file" } #### File metadata found for a directory GET /env/file_metadata/modules/example/subdirectory HTTP/1.1 200 OK Content-Type: text/pson { - "data": { - "checksum": { - "type": "ctime", - "value": "{ctime}2013-10-01 13:16:10 -0700" - }, - "destination": null, - "group": 20, - "links": "manage", - "mode": 493, - "owner": 501, - "path": "/etc/puppet/conf/modules/example/files/subdirectory", - "relative_path": null, - "type": "directory" + "checksum": { + "type": "ctime", + "value": "{ctime}2013-10-01 13:16:10 -0700" }, - "document_type": "FileMetadata", - "metadata": { - "api_version": 1 - } + "destination": null, + "group": 20, + "links": "manage", + "mode": 493, + "owner": 501, + "path": "/etc/puppet/conf/modules/example/files/subdirectory", + "relative_path": null, + "type": "directory" } #### File metadata found for a link GET /env/file_metadata/modules/example/link_to_file.txt HTTP/1.1 200 OK Content-Type: text/pson { - "data": { - "checksum": { - "type": "md5", - "value": "{md5}d0a10f45491acc8743bc5a82b228f89e" - }, - "destination": "/etc/puppet/conf/modules/example/files/just_a_file.txt", - "group": 20, - "links": "manage", - "mode": 493, - "owner": 501, - "path": "/etc/puppet/conf/modules/example/files/link_to_file.txt", - "relative_path": null, - "type": "link" + "checksum": { + "type": "md5", + "value": "{md5}d0a10f45491acc8743bc5a82b228f89e" }, - "document_type": "FileMetadata", - "metadata": { - "api_version": 1 - } + "destination": "/etc/puppet/conf/modules/example/files/just_a_file.txt", + "group": 20, + "links": "manage", + "mode": 493, + "owner": 501, + "path": "/etc/puppet/conf/modules/example/files/link_to_file.txt", + "relative_path": null, + "type": "link" } #### File not found GET /env/file_metadata/modules/example/does_not_exist HTTP/1.1 404 Not Found Not Found: Could not find file_metadata modules/example/does_not_exist Search ------ Get a list of metadata for multiple files GET /env/file_metadatas/foo.txt ### Supported HTTP Methods GET ### Supported Format Accept: pson, text/pson ### Parameters * `recurse` -- should always be set to `yes`; unfortunately the default is `no` which causes renders this a Find operation * `ignore` -- file or directory regex to ignore; can be repeated * `links` -- either `manage` (default) or `follow`. See examples below. ### Example Response #### Basic search GET /env/file_metadatas/modules/example?recurse=yes HTTP 200 OK Content-Type: text/pson [ { - "data": { - "checksum": { - "type": "ctime", - "value": "{ctime}2013-10-01 13:15:59 -0700" - }, - "destination": null, - "group": 20, - "links": "manage", - "mode": 493, - "owner": 501, - "path": "/etc/puppet/conf/modules/example/files", - "relative_path": ".", - "type": "directory" + "checksum": { + "type": "ctime", + "value": "{ctime}2013-10-01 13:15:59 -0700" }, - "document_type": "FileMetadata", - "metadata": { - "api_version": 1 - } + "destination": null, + "group": 20, + "links": "manage", + "mode": 493, + "owner": 501, + "path": "/etc/puppet/conf/modules/example/files", + "relative_path": ".", + "type": "directory" }, { - "data": { - "checksum": { - "type": "md5", - "value": "{md5}d0a10f45491acc8743bc5a82b228f89e" - }, - "destination": null, - "group": 20, - "links": "manage", - "mode": 420, - "owner": 501, - "path": "/etc/puppet/conf/modules/example/files", - "relative_path": "just_a_file.txt", - "type": "file" + "checksum": { + "type": "md5", + "value": "{md5}d0a10f45491acc8743bc5a82b228f89e" }, - "document_type": "FileMetadata", - "metadata": { - "api_version": 1 - } + "destination": null, + "group": 20, + "links": "manage", + "mode": 420, + "owner": 501, + "path": "/etc/puppet/conf/modules/example/files", + "relative_path": "just_a_file.txt", + "type": "file" }, { - "data": { - "checksum": { - "type": "md5", - "value": "{md5}d0a10f45491acc8743bc5a82b228f89e" - }, - "destination": "/etc/puppet/conf/modules/example/files/just_a_file.txt", - "group": 20, - "links": "manage", - "mode": 493, - "owner": 501, - "path": "/etc/puppet/conf/modules/example/files", - "relative_path": "link_to_file.txt", - "type": "link" + "checksum": { + "type": "md5", + "value": "{md5}d0a10f45491acc8743bc5a82b228f89e" }, - "document_type": "FileMetadata", - "metadata": { - "api_version": 1 - } + "destination": "/etc/puppet/conf/modules/example/files/just_a_file.txt", + "group": 20, + "links": "manage", + "mode": 493, + "owner": 501, + "path": "/etc/puppet/conf/modules/example/files", + "relative_path": "link_to_file.txt", + "type": "link" }, { - "data": { - "checksum": { - "type": "ctime", - "value": "{ctime}2013-10-01 13:15:59 -0700" - }, - "destination": null, - "group": 20, - "links": "manage", - "mode": 493, - "owner": 501, - "path": "/etc/puppet/conf/modules/example/files", - "relative_path": "subdirectory", - "type": "directory" + "checksum": { + "type": "ctime", + "value": "{ctime}2013-10-01 13:15:59 -0700" }, - "document_type": "FileMetadata", - "metadata": { - "api_version": 1 - } + "destination": null, + "group": 20, + "links": "manage", + "mode": 493, + "owner": 501, + "path": "/etc/puppet/conf/modules/example/files", + "relative_path": "subdirectory", + "type": "directory" }, { - "data": { - "checksum": { - "type": "md5", - "value": "{md5}d41d8cd98f00b204e9800998ecf8427e" - }, - "destination": null, - "group": 20, - "links": "manage", - "mode": 420, - "owner": 501, - "path": "/etc/puppet/conf/modules/example/files", - "relative_path": "subdirectory/another_file.txt", - "type": "file" + "checksum": { + "type": "md5", + "value": "{md5}d41d8cd98f00b204e9800998ecf8427e" }, - "document_type": "FileMetadata", - "metadata": { - "api_version": 1 - } + "destination": null, + "group": 20, + "links": "manage", + "mode": 420, + "owner": 501, + "path": "/etc/puppet/conf/modules/example/files", + "relative_path": "subdirectory/another_file.txt", + "type": "file" } ] #### Search ignoring 'sub*' and links = manage GET /env/file_metadatas/modules/example?recurse=true&ignore=sub*&links=manage HTTP 200 OK Content-Type: text/pson [ { - "data": { - "checksum": { - "type": "ctime", - "value": "{ctime}2013-10-01 13:15:59 -0700" - }, - "destination": null, - "group": 20, - "links": "manage", - "mode": 493, - "owner": 501, - "path": "/etc/puppet/conf/modules/example/files", - "relative_path": ".", - "type": "directory" + "checksum": { + "type": "ctime", + "value": "{ctime}2013-10-01 13:15:59 -0700" }, - "document_type": "FileMetadata", - "metadata": { - "api_version": 1 - } + "destination": null, + "group": 20, + "links": "manage", + "mode": 493, + "owner": 501, + "path": "/etc/puppet/conf/modules/example/files", + "relative_path": ".", + "type": "directory" }, { - "data": { - "checksum": { - "type": "md5", - "value": "{md5}d0a10f45491acc8743bc5a82b228f89e" - }, - "destination": null, - "group": 20, - "links": "manage", - "mode": 420, - "owner": 501, - "path": "/etc/puppet/conf/modules/example/files", - "relative_path": "just_a_file.txt", - "type": "file" + "checksum": { + "type": "md5", + "value": "{md5}d0a10f45491acc8743bc5a82b228f89e" }, - "document_type": "FileMetadata", - "metadata": { - "api_version": 1 - } + "destination": null, + "group": 20, + "links": "manage", + "mode": 420, + "owner": 501, + "path": "/etc/puppet/conf/modules/example/files", + "relative_path": "just_a_file.txt", + "type": "file" }, { - "data": { - "checksum": { - "type": "md5", - "value": "{md5}d0a10f45491acc8743bc5a82b228f89e" - }, - "destination": "/etc/puppet/conf/modules/example/files/just_a_file.txt", - "group": 20, - "links": "manage", - "mode": 493, - "owner": 501, - "path": "/etc/puppet/conf/modules/example/files", - "relative_path": "link_to_file.txt", - "type": "link" + "checksum": { + "type": "md5", + "value": "{md5}d0a10f45491acc8743bc5a82b228f89e" }, - "document_type": "FileMetadata", - "metadata": { - "api_version": 1 - } + "destination": "/etc/puppet/conf/modules/example/files/just_a_file.txt", + "group": 20, + "links": "manage", + "mode": 493, + "owner": 501, + "path": "/etc/puppet/conf/modules/example/files", + "relative_path": "link_to_file.txt", + "type": "link" } ] #### Search ignoring 'sub*' and links = follow This example is identical to the above example, except for the different links parameter. The result pson, then, is identical to the above example, except for: * the 'links' field is set to "follow" rather than "manage" in all metadata objects * in the 'link_to_file.txt' metadata: * for 'manage' the 'destination' field is the link destination; for 'follow', it's null * for 'manage' the 'type' field is 'link'; for 'follow' it's 'file' * for 'manage' the 'mode', 'owner' and 'group' fields are the link's values; for 'follow' the destination's values ` ` GET /env/file_metadatas/modules/example?recurse=true&ignore=sub*&links=follow HTTP 200 OK Content-Type: text/pson [ { - "data": { - "checksum": { - "type": "ctime", - "value": "{ctime}2013-10-01 13:15:59 -0700" - }, - "destination": null, - "group": 20, - "links": "follow", - "mode": 493, - "owner": 501, - "path": "/etc/puppet/conf/modules/example/files", - "relative_path": ".", - "type": "directory" + "checksum": { + "type": "ctime", + "value": "{ctime}2013-10-01 13:15:59 -0700" }, - "document_type": "FileMetadata", - "metadata": { - "api_version": 1 - } + "destination": null, + "group": 20, + "links": "follow", + "mode": 493, + "owner": 501, + "path": "/etc/puppet/conf/modules/example/files", + "relative_path": ".", + "type": "directory" }, { - "data": { - "checksum": { - "type": "md5", - "value": "{md5}d0a10f45491acc8743bc5a82b228f89e" - }, - "destination": null, - "group": 20, - "links": "follow", - "mode": 420, - "owner": 501, - "path": "/etc/puppet/conf/modules/example/files", - "relative_path": "just_a_file.txt", - "type": "file" + "checksum": { + "type": "md5", + "value": "{md5}d0a10f45491acc8743bc5a82b228f89e" }, - "document_type": "FileMetadata", - "metadata": { - "api_version": 1 - } + "destination": null, + "group": 20, + "links": "follow", + "mode": 420, + "owner": 501, + "path": "/etc/puppet/conf/modules/example/files", + "relative_path": "just_a_file.txt", + "type": "file" }, { - "data": { - "checksum": { - "type": "md5", - "value": "{md5}d0a10f45491acc8743bc5a82b228f89e" - }, - "destination": null, - "group": 20, - "links": "follow", - "mode": 420, - "owner": 501, - "path": "/etc/puppet/conf/modules/example/files", - "relative_path": "link_to_file.txt", - "type": "file" + "checksum": { + "type": "md5", + "value": "{md5}d0a10f45491acc8743bc5a82b228f89e" }, - "document_type": "FileMetadata", - "metadata": { - "api_version": 1 - } + "destination": null, + "group": 20, + "links": "follow", + "mode": 420, + "owner": 501, + "path": "/etc/puppet/conf/modules/example/files", + "relative_path": "link_to_file.txt", + "type": "file" } ] Schema ------ The representation of file metadata conforms to the schema at {file:api/schemas/file_metadata.json api/schemas/file_metadata.json}. Sample Module ------------- The examples above use this (faux) module: /etc/puppet/conf/modules/example/ files/ just_a_file.txt link_to_file.txt -> /etc/puppet/conf/modules/example/files/just_a_file.txt subdirectory/ another_file.txt diff --git a/api/docs/http_node.md b/api/docs/http_node.md index 301424166..d1aac723b 100644 --- a/api/docs/http_node.md +++ b/api/docs/http_node.md @@ -1,57 +1,54 @@ Node ==== The `node` endpoint is used by the puppet agent to get basic information about a node. The returned information includes the node name and environment, and optionally any classes set by an External Node Classifier and a hash of parameters which may include the node's facts. The returned node may have a different environment from the one given in the request if Puppet is configured with an ENC. Find ---- Retrieve data for a node GET /:environment/node/:certname ### Supported HTTP Methods GET ### Supported Response Formats PSON ### Examples > GET /production/node/mycertname HTTP/1.1 > Accept: pson, b64_zlib_yaml, yaml, raw < HTTP/1.1 200 OK < Content-Type: text/pson < Content-Length: 4630 { - "document_type":"Node", - "data":{ - "name":"thinky.corp.puppetlabs.net", - "parameters":{ - "architecture":"amd64", - "kernel":"Linux", - "blockdevices":"sda,sr0", - "clientversion":"3.3.1", - "clientnoop":"false", - "environment":"production", - ... - }, - "environment":"production" - } + "name":"thinky.corp.puppetlabs.net", + "parameters":{ + "architecture":"amd64", + "kernel":"Linux", + "blockdevices":"sda,sr0", + "clientversion":"3.3.1", + "clientnoop":"false", + "environment":"production", + ... + }, + "environment":"production" } Schema ------ Returned node objects conform to the json schema at {file:api/schemas/node.json api/schemas/node.json}. diff --git a/api/schemas/catalog.json b/api/schemas/catalog.json index a29e63d91..57758571b 100644 --- a/api/schemas/catalog.json +++ b/api/schemas/catalog.json @@ -1,121 +1,95 @@ { "$schema": "http://json-schema.org/draft-04/schema#", "title": "Catalog", "description": "A puppet resource catalog", - "type": "object", + "type": "object", "properties": { - "document_type": { - "description": "Only supported value is 'Catalog'", - "type": "string", - "enum": ["Catalog"] + "tags": { + "description": "Tags: regex is from http://docs.puppetlabs.com/puppet/3/reference/lang_reserved.html", + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z0-9_][a-z0-9_:\.\-]*$" + } }, - "metadata": { - "description": "Only contents of metadata is api_version", - "type": "object", - "properties": { - "api_version": { - "description": "Only supported api_version is 1", - "type": "integer", - "enum": [1] - } - }, - "required": ["api_version"], - "additionalProperties": false + "name": { + "type": "string" }, - "data": { - "description": "The catalog data itself", - "type": "object", - "properties": { - "tags": { - "description": "Tags: regex is from http://docs.puppetlabs.com/puppet/3/reference/lang_reserved.html", - "type": "array", - "items": { - "type": "string", - "pattern": "^[a-z0-9_][a-z0-9_:\.\-]*$" - } - }, - "name": { - "type": "string" - }, - "version": { - "type": "integer" - }, - "environment": { - "type": "string" - }, - "resources": { - "description": "The array of resources in the catalog", - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "title": { - "type": "string" - }, - "line": { - "type": "integer" - }, - "file": { - "type": "string" - }, - "exported": { - "type": "boolean" - }, - "tags": { - "description": "Tags: regex is from http://docs.puppetlabs.com/puppet/3/reference/lang_reserved.html", - "type": "array", - "items": { - "type": "string", - "pattern": "^[a-z0-9_][a-z0-9_:\.\-]*$" - } - }, - "parameters": { - "description": "Parameters: regex is from http://docs.puppetlabs.com/puppet/3/reference/lang_reserved.html", - "type": "object", - "patternProperties": { - "^[a-z][a-z0-9_]*$": {} - }, - "additionalProperties": false - } - }, - "required": ["type", "title", "tags", "exported"], - "additionalProperties": false - } - }, - "edges": { - "description": "An array of the containment relationships in the catalog.", - "type": "array", - "items": { + "version": { + "type": "integer" + }, + "environment": { + "type": "string" + }, + "resources": { + "description": "The array of resources in the catalog", + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "title": { + "type": "string" + }, + "line": { + "type": "integer" + }, + "file": { + "type": "string" + }, + "exported": { + "type": "boolean" + }, + "tags": { + "description": "Tags: regex is from http://docs.puppetlabs.com/puppet/3/reference/lang_reserved.html", + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z0-9_][a-z0-9_:\.\-]*$" + } + }, + "parameters": { + "description": "Parameters: regex is from http://docs.puppetlabs.com/puppet/3/reference/lang_reserved.html", "type": "object", - "properties": { - "source": { - "description": "Containing object", - "type": "string" - }, - "target": { - "description": "Contained object", - "type": "string" - } + "patternProperties": { + "^[a-z][a-z0-9_]*$": {} }, - "required": ["source", "target"], "additionalProperties": false } - }, - "classes": { - "type": "array", - "items": { + "required": ["type", "title", "tags", "exported"], + "additionalProperties": false + } + }, + "edges": { + "description": "An array of the containment relationships in the catalog.", + "type": "array", + "items": { + "type": "object", + "properties": { + "source": { + "description": "Containing object", + "type": "string" + }, + "target": { + "description": "Contained object", "type": "string" } - } - }, - "required": ["tags", "name", "version", "environment", "resources", "edges", "classes"], - "additionalProperties": false + }, + "required": ["source", "target"], + "additionalProperties": false + } + + }, + "classes": { + "type": "array", + "items": { + "type": "string" + } } }, - "required": ["document_type", "metadata", "data"], + "required": ["tags", "name", "version", "environment", "resources", "edges", "classes"], "additionalProperties": false } diff --git a/api/schemas/file_metadata.json b/api/schemas/file_metadata.json index 3d744637e..6a42e91e3 100644 --- a/api/schemas/file_metadata.json +++ b/api/schemas/file_metadata.json @@ -1,73 +1,47 @@ { "$schema": "http://json-schema.org/draft-04/schema#", "title": "File Metadata", "description": "Metadata about a file, directory, or symbolic link", "type": "object", "properties": { - "document_type": { - "description": "Only supported value is 'FileMetadata'", - "type": "string", - "enum": ["FileMetadata"] + "path": { + "type": "string" }, - "metadata": { - "description": "Only contents of metadata is api_version", - "type": "object", - "properties": { - "api_version": { - "description": "Only supported api_version is 1", - "type": "integer", - "enum": [1] - } - }, - "required": ["api_version"], - "additionalProperties": false + "relative_path": { + "oneOf": [{"type": "string"}, {"type": "null"}] + }, + "links": { + "enum": ["manage", "follow"] + }, + "owner": { + "type": "integer" + }, + "group": { + "type": "integer" + }, + "mode": { + "type": "integer" + }, + "type": { + "enum": ["file", "directory", "link"] }, - "data": { - "description": "The file metadata itself", + "destination": { + "oneOf": [{"type": "string"}, {"type": "null"}] + }, + "checksum": { "type": "object", "properties": { - "path": { - "type": "string" - }, - "relative_path": { - "oneOf": [{"type": "string"}, {"type": "null"}] - }, - "links": { - "enum": ["manage", "follow"] - }, - "owner": { - "type": "integer" - }, - "group": { - "type": "integer" - }, - "mode": { - "type": "integer" - }, "type": { - "enum": ["file", "directory", "link"] + "enum": ["md5", "sha256", "ctime"] }, - "destination": { - "oneOf": [{"type": "string"}, {"type": "null"}] - }, - "checksum": { - "type": "object", - "properties": { - "type": { - "enum": ["md5", "sha256", "ctime"] - }, - "value": { - "type": "string" - } - }, - "required": ["type", "value"], - "additionalProperties": false + "value": { + "type": "string" } }, - "required": ["path", "relative_path", "links", "owner", "group", "mode", "type", "destination", "checksum"], + "required": ["type", "value"], "additionalProperties": false } }, - "required": ["document_type", "metadata", "data"], + "required": ["path", "relative_path", "links", "owner", "group", "mode", "type", "destination", "checksum"], "additionalProperties": false } diff --git a/api/schemas/node.json b/api/schemas/node.json index e1ef12707..1bfdbdb09 100644 --- a/api/schemas/node.json +++ b/api/schemas/node.json @@ -1,34 +1,23 @@ { "$schema": "http://json-schema.org/draft-04/schema#", "title": "Node", "description": "A Puppet node object", "type": "object", "properties": { - "document_type": { - "type": "string", - "enum": ["Node"] + "environment": { + "type": "string" }, - "data": { - "type": "object", - "properties": { - "environment": { - "type": "string" - }, - "name": { - "type": "string" - }, - "classes": { - "type": "array", - "items": { "type": "string" } - }, - "parameters": { - "type": "object" - } - }, - "required": ["name", "environment"], - "additionalProperties": false + "name": { + "type": "string" + }, + "classes": { + "type": "array", + "items": { "type": "string" } + }, + "parameters": { + "type": "object" } }, - "required": ["document_type", "data"], + "required": ["name", "environment"], "additionalProperties": false } diff --git a/lib/puppet/external/pson/common.rb b/lib/puppet/external/pson/common.rb index 832d3f7ee..c45b51417 100644 --- a/lib/puppet/external/pson/common.rb +++ b/lib/puppet/external/pson/common.rb @@ -1,385 +1,376 @@ require 'puppet/external/pson/version' module PSON class << self # If _object_ is string-like parse the string and return the parsed result # as a Ruby data structure. Otherwise generate a PSON text from the Ruby # data structure object and return it. # # The _opts_ argument is passed through to generate/parse respectively, see # generate and parse for their documentation. def [](object, opts = {}) if object.respond_to? :to_str PSON.parse(object.to_str, opts => {}) else PSON.generate(object, opts => {}) end end # Returns the PSON parser class, that is used by PSON. This might be either # PSON::Ext::Parser or PSON::Pure::Parser. attr_reader :parser # Set the PSON parser class _parser_ to be used by PSON. def parser=(parser) # :nodoc: @parser = parser remove_const :Parser if const_defined? :Parser const_set :Parser, parser end - def registered_document_types - @registered_document_types ||= {} - end - - # Register a class-constant for deserializaion. - def register_document_type(name,klass) - registered_document_types[name.to_s] = klass - end - # Return the constant located at _path_. # Anything may be registered as a path by calling register_path, above. # Otherwise, the format of _path_ has to be either ::A::B::C or A::B::C. # In either of these cases A has to be defined in Object (e.g. the path # must be an absolute namespace path. If the constant doesn't exist at # the given path, an ArgumentError is raised. def deep_const_get(path) # :nodoc: path = path.to_s - registered_document_types[path] || path.split(/::/).inject(Object) do |p, c| + path.split(/::/).inject(Object) do |p, c| case when c.empty? then p when p.const_defined?(c) then p.const_get(c) else raise ArgumentError, "can't find const for unregistered document type #{path}" end end end # Set the module _generator_ to be used by PSON. def generator=(generator) # :nodoc: @generator = generator generator_methods = generator::GeneratorMethods for const in generator_methods.constants klass = deep_const_get(const) modul = generator_methods.const_get(const) klass.class_eval do instance_methods(false).each do |m| m.to_s == 'to_pson' and remove_method m end include modul end end self.state = generator::State const_set :State, self.state end # Returns the PSON generator modul, that is used by PSON. This might be # either PSON::Ext::Generator or PSON::Pure::Generator. attr_reader :generator # Returns the PSON generator state class, that is used by PSON. This might # be either PSON::Ext::Generator::State or PSON::Pure::Generator::State. attr_accessor :state # This is create identifier, that is used to decide, if the _pson_create_ # hook of a class should be called. It defaults to 'document_type'. attr_accessor :create_id end self.create_id = 'document_type' NaN = (-1.0) ** 0.5 Infinity = 1.0/0 MinusInfinity = -Infinity # The base exception for PSON errors. class PSONError < StandardError; end # This exception is raised, if a parser error occurs. class ParserError < PSONError; end # This exception is raised, if the nesting of parsed datastructures is too # deep. class NestingError < ParserError; end # This exception is raised, if a generator or unparser error occurs. class GeneratorError < PSONError; end # For backwards compatibility UnparserError = GeneratorError # If a circular data structure is encountered while unparsing # this exception is raised. class CircularDatastructure < GeneratorError; end # This exception is raised, if the required unicode support is missing on the # system. Usually this means, that the iconv library is not installed. class MissingUnicodeSupport < PSONError; end module_function # Parse the PSON string _source_ into a Ruby data structure and return it. # # _opts_ can have the following # keys: # * *max_nesting*: The maximum depth of nesting allowed in the parsed data # structures. Disable depth checking with :max_nesting => false, it defaults # to 19. # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in # defiance of RFC 4627 to be parsed by the Parser. This option defaults # to false. # * *create_additions*: If set to false, the Parser doesn't create # additions even if a matching class and create_id was found. This option # defaults to true. def parse(source, opts = {}) PSON.parser.new(source, opts).parse end # Parse the PSON string _source_ into a Ruby data structure and return it. # The bang version of the parse method, defaults to the more dangerous values # for the _opts_ hash, so be sure only to parse trusted _source_ strings. # # _opts_ can have the following keys: # * *max_nesting*: The maximum depth of nesting allowed in the parsed data # structures. Enable depth checking with :max_nesting => anInteger. The parse! # methods defaults to not doing max depth checking: This can be dangerous, # if someone wants to fill up your stack. # * *allow_nan*: If set to true, allow NaN, Infinity, and -Infinity in # defiance of RFC 4627 to be parsed by the Parser. This option defaults # to true. # * *create_additions*: If set to false, the Parser doesn't create # additions even if a matching class and create_id was found. This option # defaults to true. def parse!(source, opts = {}) opts = { :max_nesting => false, :allow_nan => true }.update(opts) PSON.parser.new(source, opts).parse end # Unparse the Ruby data structure _obj_ into a single line PSON string and # return it. _state_ is # * a PSON::State object, # * or a Hash like object (responding to to_hash), # * an object convertible into a hash by a to_h method, # that is used as or to configure a State object. # # It defaults to a state object, that creates the shortest possible PSON text # in one line, checks for circular data structures and doesn't allow NaN, # Infinity, and -Infinity. # # A _state_ hash can have the following keys: # * *indent*: a string used to indent levels (default: ''), # * *space*: a string that is put after, a : or , delimiter (default: ''), # * *space_before*: a string that is put before a : pair delimiter (default: ''), # * *object_nl*: a string that is put at the end of a PSON object (default: ''), # * *array_nl*: a string that is put at the end of a PSON array (default: ''), # * *check_circular*: true if checking for circular data structures # should be done (the default), false otherwise. # * *allow_nan*: true if NaN, Infinity, and -Infinity should be # generated, otherwise an exception is thrown, if these values are # encountered. This options defaults to false. # * *max_nesting*: The maximum depth of nesting allowed in the data # structures from which PSON is to be generated. Disable depth checking # with :max_nesting => false, it defaults to 19. # # See also the fast_generate for the fastest creation method with the least # amount of sanity checks, and the pretty_generate method for some # defaults for a pretty output. def generate(obj, state = nil) if state state = State.from_state(state) else state = State.new end obj.to_pson(state) end # :stopdoc: # I want to deprecate these later, so I'll first be silent about them, and # later delete them. alias unparse generate module_function :unparse # :startdoc: # Unparse the Ruby data structure _obj_ into a single line PSON string and # return it. This method disables the checks for circles in Ruby objects, and # also generates NaN, Infinity, and, -Infinity float values. # # *WARNING*: Be careful not to pass any Ruby data structures with circles as # _obj_ argument, because this will cause PSON to go into an infinite loop. def fast_generate(obj) obj.to_pson(nil) end # :stopdoc: # I want to deprecate these later, so I'll first be silent about them, and later delete them. alias fast_unparse fast_generate module_function :fast_unparse # :startdoc: # Unparse the Ruby data structure _obj_ into a PSON string and return it. The # returned string is a prettier form of the string returned by #unparse. # # The _opts_ argument can be used to configure the generator, see the # generate method for a more detailed explanation. def pretty_generate(obj, opts = nil) state = PSON.state.new( :indent => ' ', :space => ' ', :object_nl => "\n", :array_nl => "\n", :check_circular => true ) if opts if opts.respond_to? :to_hash opts = opts.to_hash elsif opts.respond_to? :to_h opts = opts.to_h else raise TypeError, "can't convert #{opts.class} into Hash" end state.configure(opts) end obj.to_pson(state) end # :stopdoc: # I want to deprecate these later, so I'll first be silent about them, and later delete them. alias pretty_unparse pretty_generate module_function :pretty_unparse # :startdoc: # Load a ruby data structure from a PSON _source_ and return it. A source can # either be a string-like object, an IO like object, or an object responding # to the read method. If _proc_ was given, it will be called with any nested # Ruby object as an argument recursively in depth first order. # # This method is part of the implementation of the load/dump interface of # Marshal and YAML. def load(source, proc = nil) if source.respond_to? :to_str source = source.to_str elsif source.respond_to? :to_io source = source.to_io.read else source = source.read end result = parse(source, :max_nesting => false, :allow_nan => true) recurse_proc(result, &proc) if proc result end def recurse_proc(result, &proc) case result when Array result.each { |x| recurse_proc x, &proc } proc.call result when Hash result.each { |x, y| recurse_proc x, &proc; recurse_proc y, &proc } proc.call result else proc.call result end end private :recurse_proc module_function :recurse_proc alias restore load module_function :restore # Dumps _obj_ as a PSON string, i.e. calls generate on the object and returns # the result. # # If anIO (an IO like object or an object that responds to the write method) # was given, the resulting PSON is written to it. # # If the number of nested arrays or objects exceeds _limit_ an ArgumentError # exception is raised. This argument is similar (but not exactly the # same!) to the _limit_ argument in Marshal.dump. # # This method is part of the implementation of the load/dump interface of # Marshal and YAML. def dump(obj, anIO = nil, limit = nil) if anIO and limit.nil? anIO = anIO.to_io if anIO.respond_to?(:to_io) unless anIO.respond_to?(:write) limit = anIO anIO = nil end end limit ||= 0 result = generate(obj, :allow_nan => true, :max_nesting => limit) if anIO anIO.write result anIO else result end rescue PSON::NestingError raise ArgumentError, "exceed depth limit", $!.backtrace end # Provide a smarter wrapper for changing string encoding that works with # both Ruby 1.8 (iconv) and 1.9 (String#encode). Thankfully they seem to # have compatible input syntax, at least for the encodings we touch. if String.method_defined?("encode") def encode(to, from, string) string.encode(to, from) end else require 'iconv' def encode(to, from, string) Iconv.conv(to, from, string) end end end module ::Kernel private # Outputs _objs_ to STDOUT as PSON strings in the shortest form, that is in # one line. def j(*objs) objs.each do |obj| puts PSON::generate(obj, :allow_nan => true, :max_nesting => false) end nil end # Ouputs _objs_ to STDOUT as PSON strings in a pretty format, with # indentation and over many lines. def jj(*objs) objs.each do |obj| puts PSON::pretty_generate(obj, :allow_nan => true, :max_nesting => false) end nil end # If _object_ is string-like parse the string and return the parsed result as # a Ruby data structure. Otherwise generate a PSON text from the Ruby data # structure object and return it. # # The _opts_ argument is passed through to generate/parse respectively, see # generate and parse for their documentation. def PSON(object, opts = {}) if object.respond_to? :to_str PSON.parse(object.to_str, opts) else PSON.generate(object, opts) end end end class ::Class # Returns true, if this class can be used to create an instance # from a serialised PSON string. The class has to implement a class # method _pson_create_ that expects a hash as first parameter, which includes # the required data. def pson_creatable? respond_to?(:pson_create) end end diff --git a/lib/puppet/file_serving/base.rb b/lib/puppet/file_serving/base.rb index c2e2c1eff..15901572d 100644 --- a/lib/puppet/file_serving/base.rb +++ b/lib/puppet/file_serving/base.rb @@ -1,95 +1,85 @@ require 'puppet/file_serving' require 'puppet/util' require 'puppet/util/methodhelper' # The base class for Content and Metadata; provides common # functionality like the behaviour around links. class Puppet::FileServing::Base include Puppet::Util::MethodHelper # This is for external consumers to store the source that was used # to retrieve the metadata. attr_accessor :source # Does our file exist? def exist? stat return true rescue return false end # Return the full path to our file. Fails if there's no path set. def full_path(dummy_argument=:work_arround_for_ruby_GC_bug) if relative_path.nil? or relative_path == "" or relative_path == "." full_path = path else full_path = File.join(path, relative_path) end if Puppet.features.microsoft_windows? # Replace multiple slashes as long as they aren't at the beginning of a filename full_path.gsub(%r{(./)/+}, '\1') else full_path.gsub(%r{//+}, '/') end end def initialize(path, options = {}) self.path = path @links = :manage set_options(options) end # Determine how we deal with links. attr_reader :links def links=(value) value = value.to_sym value = :manage if value == :ignore raise(ArgumentError, ":links can only be set to :manage or :follow") unless [:manage, :follow].include?(value) @links = value end # Set our base path. attr_reader :path def path=(path) raise ArgumentError.new("Paths must be fully qualified") unless Puppet::FileServing::Base.absolute?(path) @path = path end # Set a relative path; this is used for recursion, and sets # the file's path relative to the initial recursion point. attr_reader :relative_path def relative_path=(path) raise ArgumentError.new("Relative paths must not be fully qualified") if Puppet::FileServing::Base.absolute?(path) @relative_path = path end # Stat our file, using the appropriate link-sensitive method. def stat @stat_method ||= self.links == :manage ? :lstat : :stat Puppet::FileSystem.send(@stat_method, full_path) end def to_data_hash { 'path' => @path, 'relative_path' => @relative_path, 'links' => @links } end - def to_pson_data_hash - { - # No 'document_type' since we don't send these bare - 'data' => to_data_hash, - 'metadata' => { - 'api_version' => 1 - } - } - end - def self.absolute?(path) Puppet::Util.absolute_path?(path, :posix) or (Puppet.features.microsoft_windows? and Puppet::Util.absolute_path?(path, :windows)) end end diff --git a/lib/puppet/file_serving/metadata.rb b/lib/puppet/file_serving/metadata.rb index 6e8c78634..54644d78b 100644 --- a/lib/puppet/file_serving/metadata.rb +++ b/lib/puppet/file_serving/metadata.rb @@ -1,199 +1,184 @@ require 'puppet' require 'puppet/indirector' require 'puppet/file_serving' require 'puppet/file_serving/base' require 'puppet/util/checksums' # A class that handles retrieving file metadata. class Puppet::FileServing::Metadata < Puppet::FileServing::Base include Puppet::Util::Checksums extend Puppet::Indirector indirects :file_metadata, :terminus_class => :selector attr_reader :path, :owner, :group, :mode, :checksum_type, :checksum, :ftype, :destination PARAM_ORDER = [:mode, :ftype, :owner, :group] def checksum_type=(type) raise(ArgumentError, "Unsupported checksum type #{type}") unless respond_to?("#{type}_file") @checksum_type = type end class MetaStat extend Forwardable def initialize(stat, source_permissions = nil) @stat = stat @source_permissions_ignore = source_permissions == :ignore end def owner @source_permissions_ignore ? Process.euid : @stat.uid end def group @source_permissions_ignore ? Process.egid : @stat.gid end def mode @source_permissions_ignore ? 0644 : @stat.mode end def_delegators :@stat, :ftype end class WindowsStat < MetaStat if Puppet.features.microsoft_windows? require 'puppet/util/windows/security' end def initialize(stat, path, source_permissions = nil) super(stat, source_permissions) @path = path end { :owner => 'S-1-5-32-544', :group => 'S-1-0-0', :mode => 0644 }.each do |method, default_value| define_method method do return default_value if @source_permissions_ignore # this code remains for when source_permissions is not set to :ignore begin Puppet::Util::Windows::Security.send("get_#{method}", @path) || default_value rescue Puppet::Util::Windows::Error => detail # Very carefully catch only this specific error that result from # trying to read permissions on a symlinked file that is on a volume # that does not support ACLs. # # Unfortunately readlink method will not return the target path when # the given path is not the symlink. # # For instance, consider: # symlink c:\link points to c:\target # FileSystem.readlink('c:/link') returns 'c:/target' # FileSystem.readlink('c:/link/file') will NOT return 'c:/target/file' # # Since detecting this up front is costly, since the path in question # needs to be recursively split and tested at each depth in the path, # we catch the standard error that will result from trying to read a # file that doesn't have a DACL - 1336 is ERROR_INVALID_DACL # # Note that this affects any manually created symlinks as well as # paths like puppet:///modules return default_value if detail.code == 1336 # Also handle a VirtualBox bug where ERROR_INVALID_FUNCTION is # returned when following a symlink to a volume that is not NTFS. # It appears that the VirtualBox file system is not propagating # the standard Win32 error code above like it should. # # Apologies to all who enter this code path at a later date if detail.code == 1 && Facter.value(:virtual) == 'virtualbox' return default_value end raise end end end end def collect_stat(path, source_permissions) stat = stat() if Puppet.features.microsoft_windows? WindowsStat.new(stat, path, source_permissions) else MetaStat.new(stat, source_permissions) end end # Retrieve the attributes for this file, relative to a base directory. # Note that Puppet::FileSystem.stat(path) raises Errno::ENOENT # if the file is absent and this method does not catch that exception. def collect(source_permissions = nil) real_path = full_path stat = collect_stat(real_path, source_permissions) @owner = stat.owner @group = stat.group @ftype = stat.ftype # We have to mask the mode, yay. @mode = stat.mode & 007777 case stat.ftype when "file" @checksum = ("{#{@checksum_type}}") + send("#{@checksum_type}_file", real_path).to_s when "directory" # Always just timestamp the directory. @checksum_type = "ctime" @checksum = ("{#{@checksum_type}}") + send("#{@checksum_type}_file", path).to_s when "link" @destination = Puppet::FileSystem.readlink(real_path) @checksum = ("{#{@checksum_type}}") + send("#{@checksum_type}_file", real_path).to_s rescue nil else raise ArgumentError, "Cannot manage files of type #{stat.ftype}" end end def initialize(path,data={}) @owner = data.delete('owner') @group = data.delete('group') @mode = data.delete('mode') if checksum = data.delete('checksum') @checksum_type = checksum['type'] @checksum = checksum['value'] end @checksum_type ||= Puppet[:digest_algorithm] @ftype = data.delete('type') @destination = data.delete('destination') super(path,data) end def to_data_hash super.update( { 'owner' => owner, 'group' => group, 'mode' => mode, 'checksum' => { 'type' => checksum_type, 'value' => checksum }, 'type' => ftype, 'destination' => destination, } ) end def self.from_data_hash(data) new(data.delete('path'), data) end - PSON.register_document_type('FileMetadata',self) - def to_pson_data_hash - { - 'document_type' => 'FileMetadata', - 'data' => to_data_hash, - 'metadata' => { - 'api_version' => 1 - } - } - end - - def to_pson(*args) - to_pson_data_hash.to_pson(*args) - end - def self.from_pson(data) Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") self.from_data_hash(data) end end diff --git a/lib/puppet/indirector/request.rb b/lib/puppet/indirector/request.rb index 04c55f9a3..39b623046 100644 --- a/lib/puppet/indirector/request.rb +++ b/lib/puppet/indirector/request.rb @@ -1,318 +1,308 @@ require 'cgi' require 'uri' require 'puppet/indirector' require 'puppet/util/pson' require 'puppet/network/resolver' # This class encapsulates all of the information you need to make an # Indirection call, and as a result also handles REST calls. It's somewhat # analogous to an HTTP Request object, except tuned for our Indirector. class Puppet::Indirector::Request + # FormatSupport for serialization methods + include Puppet::Network::FormatSupport + attr_accessor :key, :method, :options, :instance, :node, :ip, :authenticated, :ignore_cache, :ignore_terminus attr_accessor :server, :port, :uri, :protocol attr_reader :indirection_name # trusted_information is specifically left out because we can't serialize it # and keep it "trusted" OPTION_ATTRIBUTES = [:ip, :node, :authenticated, :ignore_terminus, :ignore_cache, :instance, :environment] - ::PSON.register_document_type('IndirectorRequest',self) - def self.from_data_hash(data) raise ArgumentError, "No indirection name provided in data" unless indirection_name = data['type'] raise ArgumentError, "No method name provided in data" unless method = data['method'] raise ArgumentError, "No key provided in data" unless key = data['key'] request = new(indirection_name, method, key, nil, data['attributes']) if instance = data['instance'] klass = Puppet::Indirector::Indirection.instance(request.indirection_name).model if instance.is_a?(klass) request.instance = instance else request.instance = klass.from_data_hash(instance) end end request end def self.from_pson(json) Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") self.from_data_hash(json) end def to_data_hash result = { 'type' => indirection_name, 'method' => method, 'key' => key } attributes = {} OPTION_ATTRIBUTES.each do |key| next unless value = send(key) attributes[key] = value end options.each do |opt, value| attributes[opt] = value end result['attributes'] = attributes unless attributes.empty? result['instance'] = instance if instance result end - def to_pson_data_hash - { - 'document_type' => 'IndirectorRequest', - 'data' => to_data_hash, - } - end - - def to_pson(*args) - to_pson_data_hash.to_pson(*args) - end - # Is this an authenticated request? def authenticated? # Double negative, so we just get true or false ! ! authenticated end def environment # If environment has not been set directly, we should use the application's # current environment @environment ||= Puppet.lookup(:current_environment) end def environment=(env) @environment = if env.is_a?(Puppet::Node::Environment) env elsif (current_environment = Puppet.lookup(:current_environment)).name == env current_environment else Puppet.lookup(:environments).get(env) || raise(Puppet::Environments::EnvironmentNotFound, env) end end def escaped_key URI.escape(key) end # LAK:NOTE This is a messy interface to the cache, and it's only # used by the Configurer class. I decided it was better to implement # it now and refactor later, when we have a better design, than # to spend another month coming up with a design now that might # not be any better. def ignore_cache? ignore_cache end def ignore_terminus? ignore_terminus end def initialize(indirection_name, method, key, instance, options = {}) @instance = instance options ||= {} self.indirection_name = indirection_name self.method = method options = options.inject({}) { |hash, ary| hash[ary[0].to_sym] = ary[1]; hash } set_attributes(options) @options = options if key # If the request key is a URI, then we need to treat it specially, # because it rewrites the key. We could otherwise strip server/port/etc # info out in the REST class, but it seemed bad design for the REST # class to rewrite the key. if key.to_s =~ /^\w+:\// and not Puppet::Util.absolute_path?(key.to_s) # it's a URI set_uri_key(key) else @key = key end end @key = @instance.name if ! @key and @instance end # Look up the indirection based on the name provided. def indirection Puppet::Indirector::Indirection.instance(indirection_name) end def indirection_name=(name) @indirection_name = name.to_sym end def model raise ArgumentError, "Could not find indirection '#{indirection_name}'" unless i = indirection i.model end # Are we trying to interact with multiple resources, or just one? def plural? method == :search end # Create the query string, if options are present. def query_string return "" if options.nil? || options.empty? # For backward compatibility with older (pre-3.3) masters, # this puppet option allows serialization of query parameter # arrays as yaml. This can be removed when we remove yaml # support entirely. if Puppet.settings[:legacy_query_parameter_serialization] replace_arrays_with_yaml end "?" + encode_params(expand_into_parameters(options.to_a)) end def replace_arrays_with_yaml options.each do |key, value| case value when Array options[key] = YAML.dump(value) end end end def expand_into_parameters(data) data.inject([]) do |params, key_value| key, value = key_value expanded_value = case value when Array value.collect { |val| [key, val] } else [key_value] end params.concat(expand_primitive_types_into_parameters(expanded_value)) end end def expand_primitive_types_into_parameters(data) data.inject([]) do |params, key_value| key, value = key_value case value when nil params when true, false, String, Symbol, Fixnum, Bignum, Float params << [key, value] else raise ArgumentError, "HTTP REST queries cannot handle values of type '#{value.class}'" end end end def encode_params(params) params.collect do |key, value| "#{key}=#{CGI.escape(value.to_s)}" end.join("&") end def to_hash result = options.dup OPTION_ATTRIBUTES.each do |attribute| if value = send(attribute) result[attribute] = value end end result end def to_s return(uri ? uri : "/#{indirection_name}/#{key}") end def do_request(srv_service=:puppet, default_server=Puppet.settings[:server], default_port=Puppet.settings[:masterport], &block) # We were given a specific server to use, so just use that one. # This happens if someone does something like specifying a file # source using a puppet:// URI with a specific server. return yield(self) if !self.server.nil? if Puppet.settings[:use_srv_records] Puppet::Network::Resolver.each_srv_record(Puppet.settings[:srv_domain], srv_service) do |srv_server, srv_port| begin self.server = srv_server self.port = srv_port return yield(self) rescue SystemCallError => e Puppet.warning "Error connecting to #{srv_server}:#{srv_port}: #{e.message}" end end end # ... Fall back onto the default server. Puppet.debug "No more servers left, falling back to #{default_server}:#{default_port}" if Puppet.settings[:use_srv_records] self.server = default_server self.port = default_port return yield(self) end def remote? self.node or self.ip end private def set_attributes(options) OPTION_ATTRIBUTES.each do |attribute| if options.include?(attribute.to_sym) send(attribute.to_s + "=", options[attribute]) options.delete(attribute) end end end # Parse the key as a URI, setting attributes appropriately. def set_uri_key(key) @uri = key begin uri = URI.parse(URI.escape(key)) rescue => detail raise ArgumentError, "Could not understand URL #{key}: #{detail}", detail.backtrace end # Just short-circuit these to full paths if uri.scheme == "file" @key = Puppet::Util.uri_to_path(uri) return end @server = uri.host if uri.host # If the URI class can look up the scheme, it will provide a port, # otherwise it will default to '0'. if uri.port.to_i == 0 and uri.scheme == "puppet" @port = Puppet.settings[:masterport].to_i else @port = uri.port.to_i end @protocol = uri.scheme if uri.scheme == 'puppet' @key = URI.unescape(uri.path.sub(/^\//, '')) return end env, indirector, @key = URI.unescape(uri.path.sub(/^\//, '')).split('/',3) @key ||= '' self.environment = env unless env == '' end end diff --git a/lib/puppet/network/formats.rb b/lib/puppet/network/formats.rb index e636b30e8..65b178518 100644 --- a/lib/puppet/network/formats.rb +++ b/lib/puppet/network/formats.rb @@ -1,216 +1,214 @@ require 'puppet/network/format_handler' Puppet::Network::FormatHandler.create_serialized_formats(:msgpack, :weight => 20, :mime => "application/x-msgpack", :required_methods => [:render_method, :intern_method], :intern_method => :from_data_hash) do confine :feature => :msgpack def intern(klass, text) data = MessagePack.unpack(text) return data if data.is_a?(klass) klass.from_data_hash(data) end def intern_multiple(klass, text) MessagePack.unpack(text).collect do |data| klass.from_data_hash(data) end end def render_multiple(instances) instances.to_msgpack end end Puppet::Network::FormatHandler.create_serialized_formats(:yaml) do def intern(klass, text) data = YAML.load(text, :safe => true, :deserialize_symbols => true) data_to_instance(klass, data) end def intern_multiple(klass, text) data = YAML.load(text, :safe => true, :deserialize_symbols => true) unless data.respond_to?(:collect) raise Puppet::Network::FormatHandler::FormatError, "Serialized YAML did not contain a collection of instances when calling intern_multiple" end data.collect do |datum| data_to_instance(klass, datum) end end def data_to_instance(klass, data) return data if data.is_a?(klass) unless data.is_a? Hash raise Puppet::Network::FormatHandler::FormatError, "Serialized YAML did not contain a valid instance of #{klass}" end klass.from_data_hash(data) end def render(instance) instance.to_yaml end # Yaml monkey-patches Array, so this works. def render_multiple(instances) instances.to_yaml end def supported?(klass) true end end # This is a "special" format which is used for the moment only when sending facts # as REST GET parameters (see Puppet::Configurer::FactHandler). # This format combines a yaml serialization, then zlib compression and base64 encoding. Puppet::Network::FormatHandler.create_serialized_formats(:b64_zlib_yaml) do require 'base64' def use_zlib? Puppet.features.zlib? && Puppet[:zlib] end def requiring_zlib if use_zlib? yield else raise Puppet::Error, "the zlib library is not installed or is disabled." end end def intern(klass, text) requiring_zlib do Puppet::Network::FormatHandler.format(:yaml).intern(klass, decode(text)) end end def intern_multiple(klass, text) requiring_zlib do Puppet::Network::FormatHandler.format(:yaml).intern_multiple(klass, decode(text)) end end def render(instance) encode(instance.to_yaml) end def render_multiple(instances) encode(instances.to_yaml) end def supported?(klass) true end def decode(data) Zlib::Inflate.inflate(Base64.decode64(data)) end def encode(text) requiring_zlib do Base64.encode64(Zlib::Deflate.deflate(text, Zlib::BEST_COMPRESSION)) end end end Puppet::Network::FormatHandler.create(:s, :mime => "text/plain", :extension => "txt") # A very low-weight format so it'll never get chosen automatically. Puppet::Network::FormatHandler.create(:raw, :mime => "application/x-raw", :weight => 1) do def intern_multiple(klass, text) raise NotImplementedError end def render_multiple(instances) raise NotImplementedError end # LAK:NOTE The format system isn't currently flexible enough to handle # what I need to support raw formats just for individual instances (rather # than both individual and collections), but we don't yet have enough data # to make a "correct" design. # So, we hack it so it works for singular but fail if someone tries it # on plurals. def supported?(klass) true end end Puppet::Network::FormatHandler.create_serialized_formats(:pson, :weight => 10, :required_methods => [:render_method, :intern_method], :intern_method => :from_data_hash) do def intern(klass, text) data_to_instance(klass, PSON.parse(text)) end def intern_multiple(klass, text) PSON.parse(text).collect do |data| data_to_instance(klass, data) end end # PSON monkey-patches Array, so this works. def render_multiple(instances) instances.to_pson end - # If they pass class information, we want to ignore it. By default, - # we'll include class information but we won't rely on it - we don't - # want class names to be required because we then can't change our - # internal class names, which is bad. + # If they pass class information, we want to ignore it. + # This is required for compatibility with Puppet 3.x def data_to_instance(klass, data) if data.is_a?(Hash) and d = data['data'] data = d end return data if data.is_a?(klass) klass.from_data_hash(data) end end # This is really only ever going to be used for Catalogs. Puppet::Network::FormatHandler.create_serialized_formats(:dot, :required_methods => [:render_method]) Puppet::Network::FormatHandler.create(:console, :mime => 'text/x-console-text', :weight => 0) do def json @json ||= Puppet::Network::FormatHandler.format(:pson) end def render(datum) # String to String return datum if datum.is_a? String return datum if datum.is_a? Numeric # Simple hash to table if datum.is_a? Hash and datum.keys.all? { |x| x.is_a? String or x.is_a? Numeric } output = '' column_a = datum.empty? ? 2 : datum.map{ |k,v| k.to_s.length }.max + 2 datum.sort_by { |k,v| k.to_s } .each do |key, value| output << key.to_s.ljust(column_a) output << json.render(value). chomp.gsub(/\n */) { |x| x + (' ' * column_a) } output << "\n" end return output end # Print one item per line for arrays if datum.is_a? Array output = '' datum.each do |item| output << item.to_s output << "\n" end return output end # ...or pretty-print the inspect outcome. return json.render(datum) end def render_multiple(data) data.collect(&:render).join("\n") end end diff --git a/lib/puppet/node.rb b/lib/puppet/node.rb index d61d49385..f40f04289 100644 --- a/lib/puppet/node.rb +++ b/lib/puppet/node.rb @@ -1,184 +1,171 @@ require 'puppet/indirector' # A class for managing nodes, including their facts and environment. class Puppet::Node require 'puppet/node/facts' require 'puppet/node/environment' # Set up indirection, so that nodes can be looked for in # the node sources. extend Puppet::Indirector # Use the node source as the indirection terminus. indirects :node, :terminus_setting => :node_terminus, :doc => "Where to find node information. A node is composed of its name, its facts, and its environment." attr_accessor :name, :classes, :source, :ipaddress, :parameters, :trusted_data, :environment_name attr_reader :time, :facts - ::PSON.register_document_type('Node',self) - def self.from_data_hash(data) raise ArgumentError, "No name provided in serialized data" unless name = data['name'] node = new(name) node.classes = data['classes'] node.parameters = data['parameters'] node.environment_name = data['environment'] node end def self.from_pson(pson) Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") self.from_data_hash(pson) end def to_data_hash result = { 'name' => name, 'environment' => environment.name, } result['classes'] = classes unless classes.empty? result['parameters'] = parameters unless parameters.empty? result end - def to_pson_data_hash(*args) - { - 'document_type' => "Node", - 'data' => to_data_hash, - } - end - - def to_pson(*args) - to_pson_data_hash.to_pson(*args) - end - def environment if @environment @environment else if env = parameters["environment"] self.environment = env elsif environment_name self.environment = environment_name else # This should not be :current_environment, this is the default # for a node when it has not specified its environment # Tt will be used to establish what the current environment is. # self.environment = Puppet.lookup(:environments).get(Puppet[:environment]) end @environment end end def environment=(env) if env.is_a?(String) or env.is_a?(Symbol) @environment = Puppet.lookup(:environments).get(env) else @environment = env end end def has_environment_instance? !@environment.nil? end def initialize(name, options = {}) raise ArgumentError, "Node names cannot be nil" unless name @name = name if classes = options[:classes] if classes.is_a?(String) @classes = [classes] else @classes = classes end else @classes = [] end @parameters = options[:parameters] || {} @facts = options[:facts] if env = options[:environment] self.environment = env end @time = Time.now end # Merge the node facts with parameters from the node source. def fact_merge if @facts = Puppet::Node::Facts.indirection.find(name, :environment => environment) @facts.sanitize merge(@facts.values) end rescue => detail error = Puppet::Error.new("Could not retrieve facts for #{name}: #{detail}") error.set_backtrace(detail.backtrace) raise error end # Merge any random parameters into our parameter list. def merge(params) params.each do |name, value| @parameters[name] = value unless @parameters.include?(name) end @parameters["environment"] ||= self.environment.name.to_s end # Calculate the list of names we might use for looking # up our node. This is only used for AST nodes. def names return [name] if Puppet.settings[:strict_hostname_checking] names = [] names += split_name(name) if name.include?(".") # First, get the fqdn unless fqdn = parameters["fqdn"] if parameters["hostname"] and parameters["domain"] fqdn = parameters["hostname"] + "." + parameters["domain"] else Puppet.warning "Host is missing hostname and/or domain: #{name}" end end # Now that we (might) have the fqdn, add each piece to the name # list to search, in order of longest to shortest. names += split_name(fqdn) if fqdn # And make sure the node name is first, since that's the most # likely usage. # The name is usually the Certificate CN, but it can be # set to the 'facter' hostname instead. if Puppet[:node_name] == 'cert' names.unshift name else names.unshift parameters["hostname"] end names.uniq end def split_name(name) list = name.split(".") tmp = [] list.each_with_index do |short, i| tmp << list[0..i].join(".") end tmp.reverse end # Ensures the data is frozen # def trusted_data=(data) Puppet.warning("Trusted node data modified for node #{name}") unless @trusted_data.nil? @trusted_data = data.freeze end end diff --git a/lib/puppet/relationship.rb b/lib/puppet/relationship.rb index d0a3e2455..d360ed75c 100644 --- a/lib/puppet/relationship.rb +++ b/lib/puppet/relationship.rb @@ -1,103 +1,98 @@ # subscriptions are permanent associations determining how different # objects react to an event require 'puppet/util/pson' # This is Puppet's class for modeling edges in its configuration graph. # It used to be a subclass of GRATR::Edge, but that class has weird hash # overrides that dramatically slow down the graphing. class Puppet::Relationship extend Puppet::Util::Pson + + # FormatSupport for serialization methods + include Puppet::Network::FormatSupport + attr_accessor :source, :target, :callback attr_reader :event def self.from_data_hash(data) source = data["source"] target = data["target"] args = {} if event = data["event"] args[:event] = event end if callback = data["callback"] args[:callback] = callback end new(source, target, args) end def self.from_pson(pson) Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") self.from_data_hash(pson) end def event=(event) raise ArgumentError, "You must pass a callback for non-NONE events" if event != :NONE and ! callback @event = event end def initialize(source, target, options = {}) @source, @target = source, target options = (options || {}).inject({}) { |h,a| h[a[0].to_sym] = a[1]; h } [:callback, :event].each do |option| if value = options[option] send(option.to_s + "=", value) end end end # Does the passed event match our event? This is where the meaning # of :NONE comes from. def match?(event) if self.event.nil? or event == :NONE or self.event == :NONE return false elsif self.event == :ALL_EVENTS or event == self.event return true else return false end end def label result = {} result[:callback] = callback if callback result[:event] = event if event result end def ref "#{source} => #{target}" end def inspect "{ #{source} => #{target} }" end def to_data_hash data = { 'source' => source.to_s, 'target' => target.to_s } ["event", "callback"].each do |attr| next unless value = send(attr) data[attr] = value end data end - # This doesn't include document type as it is part of a catalog - def to_pson_data_hash - to_data_hash - end - - def to_pson(*args) - to_data_hash.to_pson(*args) - end - def to_s ref end end diff --git a/lib/puppet/resource.rb b/lib/puppet/resource.rb index 8abb31b0e..0734c139a 100644 --- a/lib/puppet/resource.rb +++ b/lib/puppet/resource.rb @@ -1,632 +1,623 @@ require 'puppet' require 'puppet/util/tagging' require 'puppet/util/pson' require 'puppet/parameter' # The simplest resource class. Eventually it will function as the # base class for all resource-like behaviour. # # @api public class Puppet::Resource # This stub class is only needed for serialization compatibility with 0.25.x. # Specifically, it exists to provide a compatibility API when using YAML # serialized objects loaded from StoreConfigs. Reference = Puppet::Resource include Puppet::Util::Tagging extend Puppet::Util::Pson include Enumerable attr_accessor :file, :line, :catalog, :exported, :virtual, :validate_parameters, :strict attr_reader :type, :title require 'puppet/indirector' extend Puppet::Indirector indirects :resource, :terminus_class => :ral ATTRIBUTES = [:file, :line, :exported] def self.from_data_hash(data) raise ArgumentError, "No resource type provided in serialized data" unless type = data['type'] raise ArgumentError, "No resource title provided in serialized data" unless title = data['title'] resource = new(type, title) if params = data['parameters'] params.each { |param, value| resource[param] = value } end if tags = data['tags'] tags.each { |tag| resource.tag(tag) } end ATTRIBUTES.each do |a| if value = data[a.to_s] resource.send(a.to_s + "=", value) end end resource end def self.from_pson(pson) Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") self.from_data_hash(pson) end def inspect "#{@type}[#{@title}]#{to_hash.inspect}" end def to_data_hash data = ([:type, :title, :tags] + ATTRIBUTES).inject({}) do |hash, param| next hash unless value = self.send(param) hash[param.to_s] = value hash end data["exported"] ||= false params = self.to_hash.inject({}) do |hash, ary| param, value = ary # Don't duplicate the title as the namevar next hash if param == namevar and value == title hash[param] = Puppet::Resource.value_to_pson_data(value) hash end data["parameters"] = params unless params.empty? data end - # This doesn't include document type as it is part of a catalog - def to_pson_data_hash - to_data_hash - end - def self.value_to_pson_data(value) if value.is_a? Array value.map{|v| value_to_pson_data(v) } elsif value.is_a? Puppet::Resource value.to_s else value end end def yaml_property_munge(x) case x when Hash x.inject({}) { |h,kv| k,v = kv h[k] = self.class.value_to_pson_data(v) h } else self.class.value_to_pson_data(x) end end YAML_ATTRIBUTES = [:@file, :@line, :@exported, :@type, :@title, :@tags, :@parameters] # Explicitly list the instance variables that should be serialized when # converting to YAML. # # @api private # @return [Array] The intersection of our explicit variable list and # all of the instance variables defined on this class. def to_yaml_properties YAML_ATTRIBUTES & super end - def to_pson(*args) - to_data_hash.to_pson(*args) - end - # Proxy these methods to the parameters hash. It's likely they'll # be overridden at some point, but this works for now. %w{has_key? keys length delete empty? <<}.each do |method| define_method(method) do |*args| parameters.send(method, *args) end end # Set a given parameter. Converts all passed names # to lower-case symbols. def []=(param, value) validate_parameter(param) if validate_parameters parameters[parameter_name(param)] = value end # Return a given parameter's value. Converts all passed names # to lower-case symbols. def [](param) parameters[parameter_name(param)] end def ==(other) return false unless other.respond_to?(:title) and self.type == other.type and self.title == other.title return false unless to_hash == other.to_hash true end # Compatibility method. def builtin? builtin_type? end # Is this a builtin resource type? def builtin_type? resource_type.is_a?(Class) end # Iterate over each param/value pair, as required for Enumerable. def each parameters.each { |p,v| yield p, v } end def include?(parameter) super || parameters.keys.include?( parameter_name(parameter) ) end %w{exported virtual strict}.each do |m| define_method(m+"?") do self.send(m) end end def class? @is_class ||= @type == "Class" end def stage? @is_stage ||= @type.to_s.downcase == "stage" end # Cache to reduce respond_to? lookups @@nondeprecating_type = {} # Construct a resource from data. # # Constructs a resource instance with the given `type` and `title`. Multiple # type signatures are possible for these arguments and most will result in an # expensive call to {Puppet::Node::Environment#known_resource_types} in order # to resolve `String` and `Symbol` Types to actual Ruby classes. # # @param type [Symbol, String] The name of the Puppet Type, as a string or # symbol. The actual Type will be looked up using # {Puppet::Node::Environment#known_resource_types}. This lookup is expensive. # @param type [String] The full resource name in the form of # `"Type[Title]"`. This method of calling should only be used when # `title` is `nil`. # @param type [nil] If a `nil` is passed, the title argument must be a string # of the form `"Type[Title]"`. # @param type [Class] A class that inherits from `Puppet::Type`. This method # of construction is much more efficient as it skips calls to # {Puppet::Node::Environment#known_resource_types}. # # @param title [String, :main, nil] The title of the resource. If type is `nil`, may also # be the full resource name in the form of `"Type[Title]"`. # # @api public def initialize(type, title = nil, attributes = {}) @parameters = {} if type.is_a?(Class) && type < Puppet::Type # Set the resource type to avoid an expensive `known_resource_types` # lookup. self.resource_type = type # From this point on, the constructor behaves the same as if `type` had # been passed as a symbol. type = type.name end # Set things like strictness first. attributes.each do |attr, value| next if attr == :parameters send(attr.to_s + "=", value) end @type, @title = extract_type_and_title(type, title) @type = munge_type_name(@type) if self.class? @title = :main if @title == "" @title = munge_type_name(@title) end if params = attributes[:parameters] extract_parameters(params) end if resource_type and ! @@nondeprecating_type[resource_type] if resource_type.respond_to?(:deprecate_params) resource_type.deprecate_params(title, attributes[:parameters]) else @@nondeprecating_type[resource_type] = true end end tag(self.type) tag(self.title) if valid_tag?(self.title) @reference = self # for serialization compatibility with 0.25.x if strict? and ! resource_type if self.class? raise ArgumentError, "Could not find declared class #{title}" else raise ArgumentError, "Invalid resource type #{type}" end end end def ref to_s end # Find our resource. def resolve catalog ? catalog.resource(to_s) : nil end # The resource's type implementation # @return [Puppet::Type, Puppet::Resource::Type] # @api private def resource_type @rstype ||= case type when "Class"; environment.known_resource_types.hostclass(title == :main ? "" : title) when "Node"; environment.known_resource_types.node(title) else Puppet::Type.type(type) || environment.known_resource_types.definition(type) end end # Set the resource's type implementation # @param type [Puppet::Type, Puppet::Resource::Type] # @api private def resource_type=(type) @rstype = type end def environment @environment ||= if catalog catalog.environment_instance else Puppet.lookup(:current_environment) { Puppet::Node::Environment::NONE } end end def environment=(environment) @environment = environment end # Produce a simple hash of our parameters. def to_hash parse_title.merge parameters end def to_s "#{type}[#{title}]" end def uniqueness_key # Temporary kludge to deal with inconsistant use patters h = self.to_hash h[namevar] ||= h[:name] h[:name] ||= h[namevar] h.values_at(*key_attributes.sort_by { |k| k.to_s }) end def key_attributes resource_type.respond_to?(:key_attributes) ? resource_type.key_attributes : [:name] end # Convert our resource to yaml for Hiera purposes. def to_hierayaml # Collect list of attributes to align => and move ensure first attr = parameters.keys attr_max = attr.inject(0) { |max,k| k.to_s.length > max ? k.to_s.length : max } attr.sort! if attr.first != :ensure && attr.include?(:ensure) attr.delete(:ensure) attr.unshift(:ensure) end attributes = attr.collect { |k| v = parameters[k] " %-#{attr_max}s: %s\n" % [k, Puppet::Parameter.format_value_for_display(v)] }.join " %s:\n%s" % [self.title, attributes] end # Convert our resource to Puppet code. def to_manifest # Collect list of attributes to align => and move ensure first attr = parameters.keys attr_max = attr.inject(0) { |max,k| k.to_s.length > max ? k.to_s.length : max } attr.sort! if attr.first != :ensure && attr.include?(:ensure) attr.delete(:ensure) attr.unshift(:ensure) end attributes = attr.collect { |k| v = parameters[k] " %-#{attr_max}s => %s,\n" % [k, Puppet::Parameter.format_value_for_display(v)] }.join "%s { '%s':\n%s}" % [self.type.to_s.downcase, self.title, attributes] end def to_ref ref end # Convert our resource to a RAL resource instance. Creates component # instances for resource types that don't exist. def to_ral typeklass = Puppet::Type.type(self.type) || Puppet::Type.type(:component) typeklass.new(self) end def name # this is potential namespace conflict # between the notion of an "indirector name" # and a "resource name" [ type, title ].join('/') end def missing_arguments resource_type.arguments.select do |param, default| param = param.to_sym parameters[param].nil? || parameters[param].value == :undef end end private :missing_arguments # Consult external data bindings for class parameter values which must be # namespaced in the backend. # # Example: # # class foo($port=0){ ... } # # We make a request to the backend for the key 'foo::port' not 'foo' # def lookup_external_default_for(param, scope) # Only lookup parameters for host classes return nil unless resource_type.type == :hostclass name = "#{resource_type.name}::#{param}" lookup_with_databinding(name, scope) end private :lookup_external_default_for def lookup_with_databinding(name, scope) begin Puppet::DataBinding.indirection.find( name, :environment => scope.environment.to_s, :variables => scope) rescue Puppet::DataBinding::LookupError => e raise Puppet::Error.new("Error from DataBinding '#{Puppet[:data_binding_terminus]}' while looking up '#{name}': #{e.message}", e) end end private :lookup_with_databinding def set_default_parameters(scope) return [] unless resource_type and resource_type.respond_to?(:arguments) unless is_a?(Puppet::Parser::Resource) fail Puppet::DevError, "Cannot evaluate default parameters for #{self} - not a parser resource" end missing_arguments.collect do |param, default| external_value = lookup_external_default_for(param, scope) if external_value.nil? && default.nil? next elsif external_value.nil? value = default.safeevaluate(scope) else value = external_value end self[param.to_sym] = value param end.compact end def copy_as_resource result = Puppet::Resource.new(type, title) result.file = self.file result.line = self.line result.exported = self.exported result.virtual = self.virtual result.tag(*self.tags) result.environment = environment result.instance_variable_set(:@rstype, resource_type) to_hash.each do |p, v| if v.is_a?(Puppet::Resource) v = Puppet::Resource.new(v.type, v.title) elsif v.is_a?(Array) # flatten resource references arrays v = v.flatten if v.flatten.find { |av| av.is_a?(Puppet::Resource) } v = v.collect do |av| av = Puppet::Resource.new(av.type, av.title) if av.is_a?(Puppet::Resource) av end end if Puppet[:parser] == 'current' # If the value is an array with only one value, then # convert it to a single value. This is largely so that # the database interaction doesn't have to worry about # whether it returns an array or a string. # # This behavior is not done in the future parser, but we can't issue a # deprecation warning either since there isn't anything that a user can # do about it. result[p] = if v.is_a?(Array) and v.length == 1 v[0] else v end else result[p] = v end end result end def valid_parameter?(name) resource_type.valid_parameter?(name) end # Verify that all required arguments are either present or # have been provided with defaults. # Must be called after 'set_default_parameters'. We can't join the methods # because Type#set_parameters needs specifically ordered behavior. def validate_complete return unless resource_type and resource_type.respond_to?(:arguments) resource_type.arguments.each do |param, default| param = param.to_sym fail Puppet::ParseError, "Must pass #{param} to #{self}" unless parameters.include?(param) end # Perform optional type checking if Puppet[:parser] == 'future' # Perform type checking arg_types = resource_type.argument_types # Parameters is a map from name, to parameter, and the parameter again has name and value parameters.each do |name, value| next unless t = arg_types[name.to_s] # untyped, and parameters are symbols here (aargh, strings in the type) unless Puppet::Pops::Types::TypeCalculator.instance?(t, value.value) inferred_type = Puppet::Pops::Types::TypeCalculator.infer(value.value) actual = Puppet::Pops::Types::TypeCalculator.generalize!(inferred_type) fail Puppet::ParseError, "Expected parameter '#{name}' of '#{self}' to have type #{t.to_s}, got #{actual.to_s}" end end end end def validate_parameter(name) raise ArgumentError, "Invalid parameter #{name}" unless valid_parameter?(name) end def prune_parameters(options = {}) properties = resource_type.properties.map(&:name) dup.collect do |attribute, value| if value.to_s.empty? or Array(value).empty? delete(attribute) elsif value.to_s == "absent" and attribute.to_s != "ensure" delete(attribute) end parameters_to_include = options[:parameters_to_include] || [] delete(attribute) unless properties.include?(attribute) || parameters_to_include.include?(attribute) end self end private # Produce a canonical method name. def parameter_name(param) param = param.to_s.downcase.to_sym if param == :name and namevar param = namevar end param end # The namevar for our resource type. If the type doesn't exist, # always use :name. def namevar if builtin_type? and t = resource_type and t.key_attributes.length == 1 t.key_attributes.first else :name end end def extract_parameters(params) params.each do |param, value| validate_parameter(param) if strict? self[param] = value end end def extract_type_and_title(argtype, argtitle) if (argtype.nil? || argtype == :component || argtype == :whit) && argtitle =~ /^([^\[\]]+)\[(.+)\]$/m then [ $1, $2 ] elsif argtitle.nil? && argtype =~ /^([^\[\]]+)\[(.+)\]$/m then [ $1, $2 ] elsif argtitle then [ argtype, argtitle ] elsif argtype.is_a?(Puppet::Type) then [ argtype.class.name, argtype.title ] elsif argtype.is_a?(Hash) then raise ArgumentError, "Puppet::Resource.new does not take a hash as the first argument. "+ "Did you mean (#{(argtype[:type] || argtype["type"]).inspect}, #{(argtype[:title] || argtype["title"]).inspect }) ?" else raise ArgumentError, "No title provided and #{argtype.inspect} is not a valid resource reference" end end def munge_type_name(value) return :main if value == :main return "Class" if value == "" or value.nil? or value.to_s.downcase == "component" value.to_s.split("::").collect { |s| s.capitalize }.join("::") end def parse_title h = {} type = resource_type if type.respond_to? :title_patterns type.title_patterns.each { |regexp, symbols_and_lambdas| if captures = regexp.match(title.to_s) symbols_and_lambdas.zip(captures[1..-1]).each do |symbol_and_lambda,capture| symbol, proc = symbol_and_lambda # Many types pass "identity" as the proc; we might as well give # them a shortcut to delivering that without the extra cost. # # Especially because the global type defines title_patterns and # uses the identity patterns. # # This was worth about 8MB of memory allocation saved in my # testing, so is worth the complexity for the API. if proc then h[symbol] = proc.call(capture) else h[symbol] = capture end end return h end } # If we've gotten this far, then none of the provided title patterns # matched. Since there's no way to determine the title then the # resource should fail here. raise Puppet::Error, "No set of title patterns matched the title \"#{title}\"." else return { :name => title.to_s } end end def parameters # @parameters could have been loaded from YAML, causing it to be nil (by # bypassing initialize). @parameters ||= {} end end diff --git a/lib/puppet/resource/catalog.rb b/lib/puppet/resource/catalog.rb index 0ec2a4a64..1c8e1d7a7 100644 --- a/lib/puppet/resource/catalog.rb +++ b/lib/puppet/resource/catalog.rb @@ -1,554 +1,539 @@ require 'puppet/node' require 'puppet/indirector' require 'puppet/transaction' require 'puppet/util/pson' require 'puppet/util/tagging' require 'puppet/graph' # This class models a node catalog. It is the thing meant to be passed # from server to client, and it contains all of the information in the # catalog, including the resources and the relationships between them. # # @api public class Puppet::Resource::Catalog < Puppet::Graph::SimpleGraph class DuplicateResourceError < Puppet::Error include Puppet::ExternalFileError end extend Puppet::Indirector indirects :catalog, :terminus_setting => :catalog_terminus include Puppet::Util::Tagging extend Puppet::Util::Pson # The host name this is a catalog for. attr_accessor :name # The catalog version. Used for testing whether a catalog # is up to date. attr_accessor :version # How long this catalog took to retrieve. Used for reporting stats. attr_accessor :retrieval_duration # Whether this is a host catalog, which behaves very differently. # In particular, reports are sent, graphs are made, and state is # stored in the state database. If this is set incorrectly, then you often # end up in infinite loops, because catalogs are used to make things # that the host catalog needs. attr_accessor :host_config # Whether this catalog was retrieved from the cache, which affects # whether it is written back out again. attr_accessor :from_cache # Some metadata to help us compile and generally respond to the current state. attr_accessor :client_version, :server_version # A String representing the environment for this catalog attr_accessor :environment # The actual environment instance that was used during compilation attr_accessor :environment_instance # Add classes to our class list. def add_class(*classes) classes.each do |klass| @classes << klass end # Add the class names as tags, too. tag(*classes) end def title_key_for_ref( ref ) ref =~ /^([-\w:]+)\[(.*)\]$/m [$1, $2] end def add_resource(*resources) resources.each do |resource| add_one_resource(resource) end end # @param resource [A Resource] a resource in the catalog # @return [A Resource, nil] the resource that contains the given resource # @api public def container_of(resource) adjacent(resource, :direction => :in)[0] end def add_one_resource(resource) title_key = title_key_for_ref(resource.ref) if @resource_table[title_key] fail_on_duplicate_type_and_title(resource, title_key) end add_resource_to_table(resource, title_key) create_resource_aliases(resource) resource.catalog = self if resource.respond_to?(:catalog=) add_resource_to_graph(resource) end private :add_one_resource def add_resource_to_table(resource, title_key) @resource_table[title_key] = resource @resources << title_key end private :add_resource_to_table def add_resource_to_graph(resource) add_vertex(resource) @relationship_graph.add_vertex(resource) if @relationship_graph end private :add_resource_to_graph def create_resource_aliases(resource) if resource.respond_to?(:isomorphic?) and resource.isomorphic? and resource.name != resource.title self.alias(resource, resource.uniqueness_key) end end private :create_resource_aliases # Create an alias for a resource. def alias(resource, key) resource.ref =~ /^(.+)\[/ class_name = $1 || resource.class.name newref = [class_name, key].flatten if key.is_a? String ref_string = "#{class_name}[#{key}]" return if ref_string == resource.ref end # LAK:NOTE It's important that we directly compare the references, # because sometimes an alias is created before the resource is # added to the catalog, so comparing inside the below if block # isn't sufficient. if existing = @resource_table[newref] return if existing == resource resource_declaration = " at #{resource.file}:#{resource.line}" if resource.file and resource.line existing_declaration = " at #{existing.file}:#{existing.line}" if existing.file and existing.line msg = "Cannot alias #{resource.ref} to #{key.inspect}#{resource_declaration}; resource #{newref.inspect} already declared#{existing_declaration}" raise ArgumentError, msg end @resource_table[newref] = resource @aliases[resource.ref] ||= [] @aliases[resource.ref] << newref end # Apply our catalog to the local host. # @param options [Hash{Symbol => Object}] a hash of options # @option options [Puppet::Transaction::Report] :report # The report object to log this transaction to. This is optional, # and the resulting transaction will create a report if not # supplied. # @option options [Array[String]] :tags # Tags used to filter the transaction. If supplied then only # resources tagged with any of these tags will be evaluated. # @option options [Boolean] :ignoreschedules # Ignore schedules when evaluating resources # @option options [Boolean] :for_network_device # Whether this catalog is for a network device # # @return [Puppet::Transaction] the transaction created for this # application # # @api public def apply(options = {}) Puppet::Util::Storage.load if host_config? transaction = create_transaction(options) begin transaction.report.as_logging_destination do transaction.evaluate end rescue Puppet::Error => detail Puppet.log_exception(detail, "Could not apply complete catalog: #{detail}") rescue => detail Puppet.log_exception(detail, "Got an uncaught exception of type #{detail.class}: #{detail}") ensure # Don't try to store state unless we're a host config # too recursive. Puppet::Util::Storage.store if host_config? end yield transaction if block_given? transaction end # The relationship_graph form of the catalog. This contains all of the # dependency edges that are used for determining order. # # @param given_prioritizer [Puppet::Graph::Prioritizer] The prioritization # strategy to use when constructing the relationship graph. Defaults the # being determined by the `ordering` setting. # @return [Puppet::Graph::RelationshipGraph] # @api public def relationship_graph(given_prioritizer = nil) if @relationship_graph.nil? @relationship_graph = Puppet::Graph::RelationshipGraph.new(given_prioritizer || prioritizer) @relationship_graph.populate_from(self) end @relationship_graph end def clear(remove_resources = true) super() # We have to do this so that the resources clean themselves up. @resource_table.values.each { |resource| resource.remove } if remove_resources @resource_table.clear @resources = [] if @relationship_graph @relationship_graph.clear @relationship_graph = nil end end def classes @classes.dup end # Create a new resource and register it in the catalog. def create_resource(type, options) unless klass = Puppet::Type.type(type) raise ArgumentError, "Unknown resource type #{type}" end return unless resource = klass.new(options) add_resource(resource) resource end # Make sure all of our resources are "finished". def finalize make_default_resources @resource_table.values.each { |resource| resource.finish } write_graph(:resources) end def host_config? host_config end def initialize(name = nil, environment = Puppet::Node::Environment::NONE) super() @name = name @classes = [] @resource_table = {} @resources = [] @relationship_graph = nil @host_config = true @environment_instance = environment @environment = environment.to_s @aliases = {} if block_given? yield(self) finalize end end # Make the default objects necessary for function. def make_default_resources # We have to add the resources to the catalog, or else they won't get cleaned up after # the transaction. # First create the default scheduling objects Puppet::Type.type(:schedule).mkdefaultschedules.each { |res| add_resource(res) unless resource(res.ref) } # And filebuckets if bucket = Puppet::Type.type(:filebucket).mkdefaultbucket add_resource(bucket) unless resource(bucket.ref) end end # Remove the resource from our catalog. Notice that we also call # 'remove' on the resource, at least until resource classes no longer maintain # references to the resource instances. def remove_resource(*resources) resources.each do |resource| title_key = title_key_for_ref(resource.ref) @resource_table.delete(title_key) if aliases = @aliases[resource.ref] aliases.each { |res_alias| @resource_table.delete(res_alias) } @aliases.delete(resource.ref) end remove_vertex!(resource) if vertex?(resource) @relationship_graph.remove_vertex!(resource) if @relationship_graph and @relationship_graph.vertex?(resource) @resources.delete(title_key) resource.remove end end # Look a resource up by its reference (e.g., File[/etc/passwd]). def resource(type, title = nil) # Always create a resource reference, so that it always # canonicalizes how we are referring to them. if title res = Puppet::Resource.new(type, title) else # If they didn't provide a title, then we expect the first # argument to be of the form 'Class[name]', which our # Reference class canonicalizes for us. res = Puppet::Resource.new(nil, type) end res.catalog = self title_key = [res.type, res.title.to_s] uniqueness_key = [res.type, res.uniqueness_key].flatten @resource_table[title_key] || @resource_table[uniqueness_key] end def resource_refs resource_keys.collect{ |type, name| name.is_a?( String ) ? "#{type}[#{name}]" : nil}.compact end def resource_keys @resource_table.keys end def resources @resources.collect do |key| @resource_table[key] end end def self.from_data_hash(data) result = new(data['name'], Puppet::Node::Environment::NONE) if tags = data['tags'] result.tag(*tags) end if version = data['version'] result.version = version end if environment = data['environment'] result.environment = environment result.environment_instance = Puppet::Node::Environment.remote(environment.to_sym) end if resources = data['resources'] result.add_resource(*resources.collect do |res| Puppet::Resource.from_data_hash(res) end) end if edges = data['edges'] edges.each do |edge_hash| edge = Puppet::Relationship.from_data_hash(edge_hash) unless source = result.resource(edge.source) raise ArgumentError, "Could not intern from data: Could not find relationship source #{edge.source.inspect} for #{edge.target.to_s}" end edge.source = source unless target = result.resource(edge.target) raise ArgumentError, "Could not intern from data: Could not find relationship target #{edge.target.inspect} for #{edge.source.to_s}" end edge.target = target result.add_edge(edge) end end if classes = data['classes'] result.add_class(*classes) end result end def self.from_pson(data) Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") self.from_data_hash(data) end def to_data_hash { 'tags' => tags, 'name' => name, 'version' => version, 'environment' => environment.to_s, - 'resources' => @resources.collect { |v| @resource_table[v].to_pson_data_hash }, - 'edges' => edges. collect { |e| e.to_pson_data_hash }, + 'resources' => @resources.collect { |v| @resource_table[v].to_data_hash }, + 'edges' => edges. collect { |e| e.to_data_hash }, 'classes' => classes } end - PSON.register_document_type('Catalog',self) - def to_pson_data_hash - { - 'document_type' => 'Catalog', - 'data' => to_data_hash, - 'metadata' => { - 'api_version' => 1 - } - } - end - - def to_pson(*args) - to_pson_data_hash.to_pson(*args) - end - # Convert our catalog into a RAL catalog. def to_ral to_catalog :to_ral end # Convert our catalog into a catalog of Puppet::Resource instances. def to_resource to_catalog :to_resource end # filter out the catalog, applying +block+ to each resource. # If the block result is false, the resource will # be kept otherwise it will be skipped def filter(&block) to_catalog :to_resource, &block end # Store the classes in the classfile. def write_class_file ::File.open(Puppet[:classfile], "w") do |f| f.puts classes.join("\n") end rescue => detail Puppet.err "Could not create class file #{Puppet[:classfile]}: #{detail}" end # Store the list of resources we manage def write_resource_file ::File.open(Puppet[:resourcefile], "w") do |f| to_print = resources.map do |resource| next unless resource.managed? if resource.name_var "#{resource.type}[#{resource[resource.name_var]}]" else "#{resource.ref.downcase}" end end.compact f.puts to_print.join("\n") end rescue => detail Puppet.err "Could not create resource file #{Puppet[:resourcefile]}: #{detail}" end # Produce the graph files if requested. def write_graph(name) # We only want to graph the main host catalog. return unless host_config? super end private def prioritizer @prioritizer ||= case Puppet[:ordering] when "title-hash" Puppet::Graph::TitleHashPrioritizer.new when "manifest" Puppet::Graph::SequentialPrioritizer.new when "random" Puppet::Graph::RandomPrioritizer.new else raise Puppet::DevError, "Unknown ordering type #{Puppet[:ordering]}" end end def create_transaction(options) transaction = Puppet::Transaction.new(self, options[:report], prioritizer) transaction.tags = options[:tags] if options[:tags] transaction.ignoreschedules = true if options[:ignoreschedules] transaction.for_network_device = options[:network_device] transaction end # Verify that the given resource isn't declared elsewhere. def fail_on_duplicate_type_and_title(resource, title_key) # Short-circuit the common case, return unless existing_resource = @resource_table[title_key] # If we've gotten this far, it's a real conflict msg = "Duplicate declaration: #{resource.ref} is already declared" msg << " in file #{existing_resource.file}:#{existing_resource.line}" if existing_resource.file and existing_resource.line msg << "; cannot redeclare" raise DuplicateResourceError.new(msg, resource.file, resource.line) end # An abstracted method for converting one catalog into another type of catalog. # This pretty much just converts all of the resources from one class to another, using # a conversion method. def to_catalog(convert) result = self.class.new(self.name, self.environment_instance) result.version = self.version map = {} resources.each do |resource| next if virtual_not_exported?(resource) next if block_given? and yield resource newres = resource.copy_as_resource newres.catalog = result if convert != :to_resource newres = newres.to_ral end # We can't guarantee that resources don't munge their names # (like files do with trailing slashes), so we have to keep track # of what a resource got converted to. map[resource.ref] = newres result.add_resource newres end message = convert.to_s.gsub "_", " " edges.each do |edge| # Skip edges between virtual resources. next if virtual_not_exported?(edge.source) next if block_given? and yield edge.source next if virtual_not_exported?(edge.target) next if block_given? and yield edge.target unless source = map[edge.source.ref] raise Puppet::DevError, "Could not find resource #{edge.source.ref} when converting #{message} resources" end unless target = map[edge.target.ref] raise Puppet::DevError, "Could not find resource #{edge.target.ref} when converting #{message} resources" end result.add_edge(source, target, edge.label) end map.clear result.add_class(*self.classes) result.tag(*self.tags) result end def virtual_not_exported?(resource) resource.virtual && !resource.exported end end diff --git a/spec/lib/matchers/json.rb b/spec/lib/matchers/json.rb index dbeebf992..e49a31705 100644 --- a/spec/lib/matchers/json.rb +++ b/spec/lib/matchers/json.rb @@ -1,167 +1,137 @@ module JSONMatchers class SetJsonAttribute def initialize(attributes) @attributes = attributes end def format @format ||= Puppet::Network::FormatHandler.format('pson') end def json(instance) PSON.parse(instance.to_pson) end def attr_value(attrs, instance) attrs = attrs.dup - hash = json(instance)['data'] + hash = json(instance) while attrs.length > 0 name = attrs.shift hash = hash[name] end hash end def to(value) @value = value self end def matches?(instance) result = attr_value(@attributes, instance) if @value result == @value else ! result.nil? end end def failure_message_for_should(instance) if @value "expected #{instance.inspect} to set #{@attributes.inspect} to #{@value.inspect}; got #{attr_value(@attributes, instance).inspect}" else "expected #{instance.inspect} to set #{@attributes.inspect} but was nil" end end def failure_message_for_should_not(instance) if @value "expected #{instance.inspect} not to set #{@attributes.inspect} to #{@value.inspect}" else "expected #{instance.inspect} not to set #{@attributes.inspect} to nil" end end end - class SetJsonDocumentTypeTo - def initialize(type) - @type = type - end - - def format - @format ||= Puppet::Network::FormatHandler.format('pson') - end - - def matches?(instance) - json(instance)['document_type'] == @type - end - - def json(instance) - PSON.parse(instance.to_pson) - end - - def failure_message_for_should(instance) - "expected #{instance.inspect} to set document_type to #{@type.inspect}; got #{json(instance)['document_type'].inspect}" - end - - def failure_message_for_should_not(instance) - "expected #{instance.inspect} not to set document_type to #{@type.inspect}" - end - end - class ReadJsonAttribute def initialize(attribute) @attribute = attribute end def format @format ||= Puppet::Network::FormatHandler.format('pson') end def from(value) @json = value self end def as(as) @value = as self end def matches?(klass) raise "Must specify json with 'from'" unless @json @instance = format.intern(klass, @json) if @value @instance.send(@attribute) == @value else ! @instance.send(@attribute).nil? end end def failure_message_for_should(klass) if @value "expected #{klass} to read #{@attribute} from #{@json} as #{@value.inspect}; got #{@instance.send(@attribute).inspect}" else "expected #{klass} to read #{@attribute} from #{@json} but was nil" end end def failure_message_for_should_not(klass) if @value "expected #{klass} not to set #{@attribute} to #{@value}" else "expected #{klass} not to set #{@attribute} to nil" end end end if !Puppet.features.microsoft_windows? require 'json' require 'json-schema' class SchemaMatcher JSON_META_SCHEMA = JSON.parse(File.read('api/schemas/json-meta-schema.json')) def initialize(schema) @schema = schema end def matches?(json) JSON::Validator.validate!(JSON_META_SCHEMA, @schema) JSON::Validator.validate!(@schema, json) end end end def validate_against(schema_file) if Puppet.features.microsoft_windows? pending("Schema checks cannot be done on windows because of json-schema problems") else schema = JSON.parse(File.read(schema_file)) SchemaMatcher.new(schema) end end def set_json_attribute(*attributes) SetJsonAttribute.new(attributes) end - def set_json_document_type_to(type) - SetJsonDocumentTypeTo.new(type) - end - def read_json_attribute(attribute) ReadJsonAttribute.new(attribute) end end diff --git a/spec/lib/puppet/indirector_testing.rb b/spec/lib/puppet/indirector_testing.rb index 0fd4ef044..9bea43206 100644 --- a/spec/lib/puppet/indirector_testing.rb +++ b/spec/lib/puppet/indirector_testing.rb @@ -1,37 +1,28 @@ require 'puppet/indirector' require 'puppet/util/pson' class Puppet::IndirectorTesting extend Puppet::Indirector indirects :indirector_testing # We should have some way to identify if we got a valid object back with the # current values, no? attr_accessor :value alias_method :name, :value alias_method :name=, :value= def initialize(value) self.value = value end def self.from_raw(raw) new(raw) end - PSON.register_document_type('IndirectorTesting',self) def self.from_data_hash(data) new(data['value']) end def to_data_hash { 'value' => value } end - - def to_pson - { - 'document_type' => 'IndirectorTesting', - 'data' => self.to_data_hash, - 'metadata' => { 'api_version' => 1 } - }.to_pson - end end diff --git a/spec/unit/file_serving/metadata_spec.rb b/spec/unit/file_serving/metadata_spec.rb index 1fde4eb73..6c9b5633f 100755 --- a/spec/unit/file_serving/metadata_spec.rb +++ b/spec/unit/file_serving/metadata_spec.rb @@ -1,432 +1,424 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/file_serving/metadata' require 'matchers/json' describe Puppet::FileServing::Metadata do let(:foobar) { File.expand_path('/foo/bar') } it "should be a subclass of Base" do Puppet::FileServing::Metadata.superclass.should equal(Puppet::FileServing::Base) end it "should indirect file_metadata" do Puppet::FileServing::Metadata.indirection.name.should == :file_metadata end it "should have a method that triggers attribute collection" do Puppet::FileServing::Metadata.new(foobar).should respond_to(:collect) end it "should support pson serialization" do Puppet::FileServing::Metadata.new(foobar).should respond_to(:to_pson) end - it "should support to_pson_data_hash" do - Puppet::FileServing::Metadata.new(foobar).should respond_to(:to_pson_data_hash) - end - it "should support deserialization" do Puppet::FileServing::Metadata.should respond_to(:from_data_hash) end describe "when serializing" do let(:metadata) { Puppet::FileServing::Metadata.new(foobar) } - it "should serialize as FileMetadata" do - metadata.to_pson_data_hash['document_type'].should == "FileMetadata" - end - it "the data should include the path, relative_path, links, owner, group, mode, checksum, type, and destination" do - metadata.to_pson_data_hash['data'].keys.sort.should == %w{ path relative_path links owner group mode checksum type destination }.sort + metadata.to_data_hash.keys.sort.should == %w{ path relative_path links owner group mode checksum type destination }.sort end it "should pass the path in the hash verbatim" do - metadata.to_pson_data_hash['data']['path'].should == metadata.path + metadata.to_data_hash['path'].should == metadata.path end it "should pass the relative_path in the hash verbatim" do - metadata.to_pson_data_hash['data']['relative_path'].should == metadata.relative_path + metadata.to_data_hash['relative_path'].should == metadata.relative_path end it "should pass the links in the hash verbatim" do - metadata.to_pson_data_hash['data']['links'].should == metadata.links + metadata.to_data_hash['links'].should == metadata.links end it "should pass the path owner in the hash verbatim" do - metadata.to_pson_data_hash['data']['owner'].should == metadata.owner + metadata.to_data_hash['owner'].should == metadata.owner end it "should pass the group in the hash verbatim" do - metadata.to_pson_data_hash['data']['group'].should == metadata.group + metadata.to_data_hash['group'].should == metadata.group end it "should pass the mode in the hash verbatim" do - metadata.to_pson_data_hash['data']['mode'].should == metadata.mode + metadata.to_data_hash['mode'].should == metadata.mode end it "should pass the ftype in the hash verbatim as the 'type'" do - metadata.to_pson_data_hash['data']['type'].should == metadata.ftype + metadata.to_data_hash['type'].should == metadata.ftype end it "should pass the destination verbatim" do - metadata.to_pson_data_hash['data']['destination'].should == metadata.destination + metadata.to_data_hash['destination'].should == metadata.destination end it "should pass the checksum in the hash as a nested hash" do - metadata.to_pson_data_hash['data']['checksum'].should be_is_a(Hash) + metadata.to_data_hash['checksum'].should be_is_a(Hash) end it "should pass the checksum_type in the hash verbatim as the checksum's type" do - metadata.to_pson_data_hash['data']['checksum']['type'].should == metadata.checksum_type + metadata.to_data_hash['checksum']['type'].should == metadata.checksum_type end it "should pass the checksum in the hash verbatim as the checksum's value" do - metadata.to_pson_data_hash['data']['checksum']['value'].should == metadata.checksum + metadata.to_data_hash['checksum']['value'].should == metadata.checksum end end end describe Puppet::FileServing::Metadata, :uses_checksums => true do include JSONMatchers include PuppetSpec::Files shared_examples_for "metadata collector" do let(:metadata) do data = described_class.new(path) data.collect data end describe "when collecting attributes" do describe "when managing files" do let(:path) { tmpfile('file_serving_metadata') } before :each do FileUtils.touch(path) end it "should set the owner to the file's current owner" do metadata.owner.should == owner end it "should set the group to the file's current group" do metadata.group.should == group end it "should set the mode to the file's masked mode" do set_mode(33261, path) metadata.mode.should == 0755 end describe "checksumming" do with_digest_algorithms do before :each do File.open(path, "wb") {|f| f.print(plaintext)} end it "should default to a checksum of the proper type with the file's current checksum" do metadata.checksum.should == "{#{digest_algorithm}}#{checksum}" end it "should give a mtime checksum when checksum_type is set" do time = Time.now metadata.checksum_type = "mtime" metadata.expects(:mtime_file).returns(@time) metadata.collect metadata.checksum.should == "{mtime}#{@time}" end end end it "should validate against the schema" do expect(metadata.to_pson).to validate_against('api/schemas/file_metadata.json') end end describe "when managing directories" do let(:path) { tmpdir('file_serving_metadata_dir') } let(:time) { Time.now } before :each do metadata.expects(:ctime_file).returns(time) end it "should only use checksums of type 'ctime' for directories" do metadata.collect metadata.checksum.should == "{ctime}#{time}" end it "should only use checksums of type 'ctime' for directories even if checksum_type set" do metadata.checksum_type = "mtime" metadata.expects(:mtime_file).never metadata.collect metadata.checksum.should == "{ctime}#{time}" end it "should validate against the schema" do metadata.collect expect(metadata.to_pson).to validate_against('api/schemas/file_metadata.json') end end end end describe "WindowsStat", :if => Puppet.features.microsoft_windows? do include PuppetSpec::Files it "should return default owner, group and mode when the given path has an invalid DACL (such as a non-NTFS volume)" do invalid_error = Puppet::Util::Windows::Error.new('Invalid DACL', 1336) path = tmpfile('foo') FileUtils.touch(path) Puppet::Util::Windows::Security.stubs(:get_owner).with(path).raises(invalid_error) Puppet::Util::Windows::Security.stubs(:get_group).with(path).raises(invalid_error) Puppet::Util::Windows::Security.stubs(:get_mode).with(path).raises(invalid_error) stat = Puppet::FileSystem.stat(path) win_stat = Puppet::FileServing::Metadata::WindowsStat.new(stat, path) win_stat.owner.should == 'S-1-5-32-544' win_stat.group.should == 'S-1-0-0' win_stat.mode.should == 0644 end it "should still raise errors that are not the result of an 'Invalid DACL'" do invalid_error = ArgumentError.new('bar') path = tmpfile('bar') FileUtils.touch(path) Puppet::Util::Windows::Security.stubs(:get_owner).with(path).raises(invalid_error) Puppet::Util::Windows::Security.stubs(:get_group).with(path).raises(invalid_error) Puppet::Util::Windows::Security.stubs(:get_mode).with(path).raises(invalid_error) stat = Puppet::FileSystem.stat(path) win_stat = Puppet::FileServing::Metadata::WindowsStat.new(stat, path) expect { win_stat.owner }.to raise_error(ArgumentError) expect { win_stat.group }.to raise_error(ArgumentError) expect { win_stat.mode }.to raise_error(ArgumentError) end end shared_examples_for "metadata collector symlinks" do let(:metadata) do data = described_class.new(path) data.collect data end describe "when collecting attributes" do describe "when managing links" do # 'path' is a link that points to 'target' let(:path) { tmpfile('file_serving_metadata_link') } let(:target) { tmpfile('file_serving_metadata_target') } let(:fmode) { Puppet::FileSystem.lstat(path).mode & 0777 } before :each do File.open(target, "wb") {|f| f.print('some content')} set_mode(0644, target) Puppet::FileSystem.symlink(target, path) end it "should read links instead of returning their checksums" do metadata.destination.should == target end it "should validate against the schema" do expect(metadata.to_pson).to validate_against('api/schemas/file_metadata.json') end end end describe Puppet::FileServing::Metadata, " when finding the file to use for setting attributes" do let(:path) { tmpfile('file_serving_metadata_find_file') } before :each do File.open(path, "wb") {|f| f.print('some content')} set_mode(0755, path) end it "should accept a base path to which the file should be relative" do dir = tmpdir('metadata_dir') metadata = described_class.new(dir) metadata.relative_path = 'relative_path' FileUtils.touch(metadata.full_path) metadata.collect end it "should use the set base path if one is not provided" do metadata.collect end it "should raise an exception if the file does not exist" do File.delete(path) proc { metadata.collect}.should raise_error(Errno::ENOENT) end it "should validate against the schema" do expect(metadata.to_pson).to validate_against('api/schemas/file_metadata.json') end end end describe "on POSIX systems", :if => Puppet.features.posix? do let(:owner) {10} let(:group) {20} before :each do File::Stat.any_instance.stubs(:uid).returns owner File::Stat.any_instance.stubs(:gid).returns group end it_should_behave_like "metadata collector" it_should_behave_like "metadata collector symlinks" def set_mode(mode, path) File.chmod(mode, path) end end describe "on Windows systems", :if => Puppet.features.microsoft_windows? do let(:owner) {'S-1-1-50'} let(:group) {'S-1-1-51'} before :each do require 'puppet/util/windows/security' Puppet::Util::Windows::Security.stubs(:get_owner).returns owner Puppet::Util::Windows::Security.stubs(:get_group).returns group end it_should_behave_like "metadata collector" it_should_behave_like "metadata collector symlinks" if Puppet.features.manages_symlinks? describe "if ACL metadata cannot be collected" do let(:path) { tmpdir('file_serving_metadata_acl') } let(:metadata) do data = described_class.new(path) data.collect data end let (:invalid_dacl_error) do Puppet::Util::Windows::Error.new('Invalid DACL', 1336) end it "should default owner" do Puppet::Util::Windows::Security.stubs(:get_owner).returns nil metadata.owner.should == 'S-1-5-32-544' end it "should default group" do Puppet::Util::Windows::Security.stubs(:get_group).returns nil metadata.group.should == 'S-1-0-0' end it "should default mode" do Puppet::Util::Windows::Security.stubs(:get_mode).returns nil metadata.mode.should == 0644 end describe "when the path raises an Invalid ACL error" do # these simulate the behavior of a symlink file whose target does not support ACLs it "should default owner" do Puppet::Util::Windows::Security.stubs(:get_owner).raises(invalid_dacl_error) metadata.owner.should == 'S-1-5-32-544' end it "should default group" do Puppet::Util::Windows::Security.stubs(:get_group).raises(invalid_dacl_error) metadata.group.should == 'S-1-0-0' end it "should default mode" do Puppet::Util::Windows::Security.stubs(:get_mode).raises(invalid_dacl_error) metadata.mode.should == 0644 end end end def set_mode(mode, path) Puppet::Util::Windows::Security.set_mode(mode, path) end end end describe Puppet::FileServing::Metadata, " when pointing to a link", :if => Puppet.features.manages_symlinks?, :uses_checksums => true do with_digest_algorithms do describe "when links are managed" do before do path = "/base/path/my/file" @file = Puppet::FileServing::Metadata.new(path, :links => :manage) stat = stub("stat", :uid => 1, :gid => 2, :ftype => "link", :mode => 0755) stub_file = stub(:readlink => "/some/other/path", :lstat => stat) Puppet::FileSystem.expects(:lstat).with(path).at_least_once.returns stat Puppet::FileSystem.expects(:readlink).with(path).at_least_once.returns "/some/other/path" @file.stubs("#{digest_algorithm}_file".intern).returns(checksum) # Remove these when :managed links are no longer checksumed. if Puppet.features.microsoft_windows? win_stat = stub('win_stat', :owner => 'snarf', :group => 'thundercats', :ftype => 'link', :mode => 0755) Puppet::FileServing::Metadata::WindowsStat.stubs(:new).returns win_stat end end it "should store the destination of the link in :destination if links are :manage" do @file.collect @file.destination.should == "/some/other/path" end pending "should not collect the checksum if links are :manage" do # We'd like this to be true, but we need to always collect the checksum because in the server/client/server round trip we lose the distintion between manage and follow. @file.collect @file.checksum.should be_nil end it "should collect the checksum if links are :manage" do # see pending note above @file.collect @file.checksum.should == "{#{digest_algorithm}}#{checksum}" end end describe "when links are followed" do before do path = "/base/path/my/file" @file = Puppet::FileServing::Metadata.new(path, :links => :follow) stat = stub("stat", :uid => 1, :gid => 2, :ftype => "file", :mode => 0755) Puppet::FileSystem.expects(:stat).with(path).at_least_once.returns stat Puppet::FileSystem.expects(:readlink).never if Puppet.features.microsoft_windows? win_stat = stub('win_stat', :owner => 'snarf', :group => 'thundercats', :ftype => 'file', :mode => 0755) Puppet::FileServing::Metadata::WindowsStat.stubs(:new).returns win_stat end @file.stubs("#{digest_algorithm}_file".intern).returns(checksum) end it "should not store the destination of the link in :destination if links are :follow" do @file.collect @file.destination.should be_nil end it "should collect the checksum if links are :follow" do @file.collect @file.checksum.should == "{#{digest_algorithm}}#{checksum}" end end end end diff --git a/spec/unit/indirector/json_spec.rb b/spec/unit/indirector/json_spec.rb index 53431fc8e..656d156c0 100755 --- a/spec/unit/indirector/json_spec.rb +++ b/spec/unit/indirector/json_spec.rb @@ -1,193 +1,192 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet_spec/files' require 'puppet/indirector/indirector_testing/json' describe Puppet::Indirector::JSON do include PuppetSpec::Files subject { Puppet::IndirectorTesting::JSON.new } let :model do Puppet::IndirectorTesting end let :indirection do model.indirection end context "#path" do before :each do Puppet[:server_datadir] = '/sample/datadir/master' Puppet[:client_datadir] = '/sample/datadir/client' end it "uses the :server_datadir setting if this is the master" do Puppet.run_mode.stubs(:master?).returns(true) expected = File.join(Puppet[:server_datadir], 'indirector_testing', 'testing.json') subject.path('testing').should == expected end it "uses the :client_datadir setting if this is not the master" do Puppet.run_mode.stubs(:master?).returns(false) expected = File.join(Puppet[:client_datadir], 'indirector_testing', 'testing.json') subject.path('testing').should == expected end it "overrides the default extension with a supplied value" do Puppet.run_mode.stubs(:master?).returns(true) expected = File.join(Puppet[:server_datadir], 'indirector_testing', 'testing.not-json') subject.path('testing', '.not-json').should == expected end ['../foo', '..\\foo', './../foo', '.\\..\\foo', '/foo', '//foo', '\\foo', '\\\\goo', "test\0/../bar", "test\0\\..\\bar", "..\\/bar", "/tmp/bar", "/tmp\\bar", "tmp\\bar", " / bar", " /../ bar", " \\..\\ bar", "c:\\foo", "c:/foo", "\\\\?\\UNC\\bar", "\\\\foo\\bar", "\\\\?\\c:\\foo", "//?/UNC/bar", "//foo/bar", "//?/c:/foo", ].each do |input| it "should resist directory traversal attacks (#{input.inspect})" do expect { subject.path(input) }.to raise_error ArgumentError, 'invalid key' end end end context "handling requests" do before :each do Puppet.run_mode.stubs(:master?).returns(true) Puppet[:server_datadir] = tmpdir('jsondir') FileUtils.mkdir_p(File.join(Puppet[:server_datadir], 'indirector_testing')) end let :file do subject.path(request.key) end def with_content(text) FileUtils.mkdir_p(File.dirname(file)) File.open(file, 'w') {|f| f.puts text } yield if block_given? end it "data saves and then loads again correctly" do subject.save(indirection.request(:save, 'example', model.new('banana'))) subject.find(indirection.request(:find, 'example', nil)).value.should == 'banana' end context "#find" do let :request do indirection.request(:find, 'example', nil) end it "returns nil if the file doesn't exist" do subject.find(request).should be_nil end it "raises a descriptive error when the file can't be read" do with_content(model.new('foo').to_pson) do # I don't like this, but there isn't a credible alternative that # also works on Windows, so a stub it is. At least the expectation # will fail if the implementation changes. Sorry to the next dev. File.expects(:read).with(file).raises(Errno::EPERM) expect { subject.find(request) }. to raise_error Puppet::Error, /Could not read JSON/ end end it "raises a descriptive error when the file content is invalid" do with_content("this is totally invalid JSON") do expect { subject.find(request) }. to raise_error Puppet::Error, /Could not parse JSON data/ end end it "should return an instance of the indirected object when valid" do with_content(model.new(1).to_pson) do instance = subject.find(request) instance.should be_an_instance_of model instance.value.should == 1 end end end context "#save" do let :instance do model.new(4) end let :request do indirection.request(:find, 'example', instance) end it "should save the instance of the request as JSON to disk" do subject.save(request) content = File.read(file) - content.should =~ /"document_type"\s*:\s*"IndirectorTesting"/ content.should =~ /"value"\s*:\s*4/ end it "should create the indirection directory if required" do target = File.join(Puppet[:server_datadir], 'indirector_testing') Dir.rmdir(target) subject.save(request) File.should be_directory(target) end end context "#destroy" do let :request do indirection.request(:find, 'example', nil) end it "removes an existing file" do with_content('hello') do subject.destroy(request) end Puppet::FileSystem.exist?(file).should be_false end it "silently succeeds when files don't exist" do Puppet::FileSystem.unlink(file) rescue nil subject.destroy(request).should be_true end it "raises an informative error for other failures" do Puppet::FileSystem.stubs(:unlink).with(file).raises(Errno::EPERM, 'fake permission problem') with_content('hello') do expect { subject.destroy(request) }.to raise_error(Puppet::Error) end Puppet::FileSystem.unstub(:unlink) # thanks, mocha end end end context "#search" do before :each do Puppet.run_mode.stubs(:master?).returns(true) Puppet[:server_datadir] = tmpdir('jsondir') FileUtils.mkdir_p(File.join(Puppet[:server_datadir], 'indirector_testing')) end def request(glob) indirection.request(:search, glob, nil) end def create_file(name, value = 12) File.open(subject.path(name, ''), 'w') do |f| f.puts Puppet::IndirectorTesting.new(value).to_pson end end it "returns an empty array when nothing matches the key as a glob" do subject.search(request('*')).should == [] end it "returns an array with one item if one item matches" do create_file('foo.json', 'foo') create_file('bar.json', 'bar') subject.search(request('f*')).map(&:value).should == ['foo'] end it "returns an array of items when more than one item matches" do create_file('foo.json', 'foo') create_file('bar.json', 'bar') create_file('baz.json', 'baz') subject.search(request('b*')).map(&:value).should =~ ['bar', 'baz'] end it "only items with the .json extension" do create_file('foo.json', 'foo-json') create_file('foo.pson', 'foo-pson') create_file('foo.json~', 'foo-backup') subject.search(request('f*')).map(&:value).should == ['foo-json'] end end end diff --git a/spec/unit/indirector/request_spec.rb b/spec/unit/indirector/request_spec.rb index e3edfd670..7f369d1b0 100755 --- a/spec/unit/indirector/request_spec.rb +++ b/spec/unit/indirector/request_spec.rb @@ -1,603 +1,593 @@ #! /usr/bin/env ruby require 'spec_helper' require 'matchers/json' require 'puppet/indirector/request' require 'puppet/util/pson' describe Puppet::Indirector::Request do include JSONMatchers - describe "when registering the document type" do - it "should register its document type with JSON" do - PSON.registered_document_types["IndirectorRequest"].should equal(Puppet::Indirector::Request) - end - end - describe "when initializing" do it "should always convert the indirection name to a symbol" do Puppet::Indirector::Request.new("ind", :method, "mykey", nil).indirection_name.should == :ind end it "should use provided value as the key if it is a string" do Puppet::Indirector::Request.new(:ind, :method, "mykey", nil).key.should == "mykey" end it "should use provided value as the key if it is a symbol" do Puppet::Indirector::Request.new(:ind, :method, :mykey, nil).key.should == :mykey end it "should use the name of the provided instance as its key if an instance is provided as the key instead of a string" do instance = mock 'instance', :name => "mykey" request = Puppet::Indirector::Request.new(:ind, :method, nil, instance) request.key.should == "mykey" request.instance.should equal(instance) end it "should support options specified as a hash" do expect { Puppet::Indirector::Request.new(:ind, :method, :key, nil, :one => :two) }.to_not raise_error end it "should support nil options" do expect { Puppet::Indirector::Request.new(:ind, :method, :key, nil, nil) }.to_not raise_error end it "should support unspecified options" do expect { Puppet::Indirector::Request.new(:ind, :method, :key, nil) }.to_not raise_error end it "should use an empty options hash if nil was provided" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, nil).options.should == {} end it "should default to a nil node" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).node.should be_nil end it "should set its node attribute if provided in the options" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, :node => "foo.com").node.should == "foo.com" end it "should default to a nil ip" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).ip.should be_nil end it "should set its ip attribute if provided in the options" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, :ip => "192.168.0.1").ip.should == "192.168.0.1" end it "should default to being unauthenticated" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).should_not be_authenticated end it "should set be marked authenticated if configured in the options" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, :authenticated => "eh").should be_authenticated end it "should keep its options as a hash even if a node is specified" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, :node => "eh").options.should be_instance_of(Hash) end it "should keep its options as a hash even if another option is specified" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, :foo => "bar").options.should be_instance_of(Hash) end it "should treat options other than :ip, :node, and :authenticated as options rather than attributes" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, :server => "bar").options[:server].should == "bar" end it "should normalize options to use symbols as keys" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, "foo" => "bar").options[:foo].should == "bar" end describe "and the request key is a URI" do let(:file) { File.expand_path("/my/file with spaces") } describe "and the URI is a 'file' URI" do before do @request = Puppet::Indirector::Request.new(:ind, :method, "#{URI.unescape(Puppet::Util.path_to_uri(file).to_s)}", nil) end it "should set the request key to the unescaped full file path" do @request.key.should == file end it "should not set the protocol" do @request.protocol.should be_nil end it "should not set the port" do @request.port.should be_nil end it "should not set the server" do @request.server.should be_nil end end it "should set the protocol to the URI scheme" do Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff", nil).protocol.should == "http" end it "should set the server if a server is provided" do Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff", nil).server.should == "host" end it "should set the server and port if both are provided" do Puppet::Indirector::Request.new(:ind, :method, "http://host:543/stuff", nil).port.should == 543 end it "should default to the masterport if the URI scheme is 'puppet'" do Puppet[:masterport] = "321" Puppet::Indirector::Request.new(:ind, :method, "puppet://host/stuff", nil).port.should == 321 end it "should use the provided port if the URI scheme is not 'puppet'" do Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff", nil).port.should == 80 end it "should set the request key to the unescaped key part path from the URI" do Puppet::Indirector::Request.new(:ind, :method, "http://host/environment/terminus/stuff with spaces", nil).key.should == "stuff with spaces" end it "should set the :uri attribute to the full URI" do Puppet::Indirector::Request.new(:ind, :method, "http:///stu ff", nil).uri.should == 'http:///stu ff' end it "should not parse relative URI" do Puppet::Indirector::Request.new(:ind, :method, "foo/bar", nil).uri.should be_nil end it "should not parse opaque URI" do Puppet::Indirector::Request.new(:ind, :method, "mailto:joe", nil).uri.should be_nil end end it "should allow indication that it should not read a cached instance" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, :ignore_cache => true).should be_ignore_cache end it "should default to not ignoring the cache" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).should_not be_ignore_cache end it "should allow indication that it should not not read an instance from the terminus" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, :ignore_terminus => true).should be_ignore_terminus end it "should default to not ignoring the terminus" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).should_not be_ignore_terminus end end it "should look use the Indirection class to return the appropriate indirection" do ind = mock 'indirection' Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns ind request = Puppet::Indirector::Request.new(:myind, :method, :key, nil) request.indirection.should equal(ind) end it "should use its indirection to look up the appropriate model" do ind = mock 'indirection' Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns ind request = Puppet::Indirector::Request.new(:myind, :method, :key, nil) ind.expects(:model).returns "mymodel" request.model.should == "mymodel" end it "should fail intelligently when asked to find a model but the indirection cannot be found" do Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns nil request = Puppet::Indirector::Request.new(:myind, :method, :key, nil) expect { request.model }.to raise_error(ArgumentError) end it "should have a method for determining if the request is plural or singular" do Puppet::Indirector::Request.new(:myind, :method, :key, nil).should respond_to(:plural?) end it "should be considered plural if the method is 'search'" do Puppet::Indirector::Request.new(:myind, :search, :key, nil).should be_plural end it "should not be considered plural if the method is not 'search'" do Puppet::Indirector::Request.new(:myind, :find, :key, nil).should_not be_plural end it "should use its uri, if it has one, as its string representation" do Puppet::Indirector::Request.new(:myind, :find, "foo://bar/baz", nil).to_s.should == "foo://bar/baz" end it "should use its indirection name and key, if it has no uri, as its string representation" do Puppet::Indirector::Request.new(:myind, :find, "key", nil) == "/myind/key" end it "should be able to return the URI-escaped key" do Puppet::Indirector::Request.new(:myind, :find, "my key", nil).escaped_key.should == URI.escape("my key") end it "should set its environment to an environment instance when a string is specified as its environment" do env = Puppet::Node::Environment.create(:foo, []) Puppet.override(:environments => Puppet::Environments::Static.new(env)) do Puppet::Indirector::Request.new(:myind, :find, "my key", nil, :environment => "foo").environment.should == env end end it "should use any passed in environment instances as its environment" do env = Puppet::Node::Environment.create(:foo, []) Puppet::Indirector::Request.new(:myind, :find, "my key", nil, :environment => env).environment.should equal(env) end it "should use the current environment when none is provided" do configured = Puppet::Node::Environment.create(:foo, []) Puppet[:environment] = "foo" expect(Puppet::Indirector::Request.new(:myind, :find, "my key", nil).environment).to eq(Puppet.lookup(:current_environment)) end it "should support converting its options to a hash" do Puppet::Indirector::Request.new(:myind, :find, "my key", nil ).should respond_to(:to_hash) end it "should include all of its attributes when its options are converted to a hash" do Puppet::Indirector::Request.new(:myind, :find, "my key", nil, :node => 'foo').to_hash[:node].should == 'foo' end describe "when building a query string from its options" do def a_request_with_options(options) Puppet::Indirector::Request.new(:myind, :find, "my key", nil, options) end def the_parsed_query_string_from(request) CGI.parse(request.query_string.sub(/^\?/, '')) end it "should return an empty query string if there are no options" do request = a_request_with_options(nil) request.query_string.should == "" end it "should return an empty query string if the options are empty" do request = a_request_with_options({}) request.query_string.should == "" end it "should prefix the query string with '?'" do request = a_request_with_options(:one => "two") request.query_string.should =~ /^\?/ end it "should include all options in the query string, separated by '&'" do request = a_request_with_options(:one => "two", :three => "four") the_parsed_query_string_from(request).should == { "one" => ["two"], "three" => ["four"] } end it "should ignore nil options" do request = a_request_with_options(:one => "two", :three => nil) the_parsed_query_string_from(request).should == { "one" => ["two"] } end it "should convert 'true' option values into strings" do request = a_request_with_options(:one => true) the_parsed_query_string_from(request).should == { "one" => ["true"] } end it "should convert 'false' option values into strings" do request = a_request_with_options(:one => false) the_parsed_query_string_from(request).should == { "one" => ["false"] } end it "should convert to a string all option values that are integers" do request = a_request_with_options(:one => 50) the_parsed_query_string_from(request).should == { "one" => ["50"] } end it "should convert to a string all option values that are floating point numbers" do request = a_request_with_options(:one => 1.2) the_parsed_query_string_from(request).should == { "one" => ["1.2"] } end it "should CGI-escape all option values that are strings" do request = a_request_with_options(:one => "one two") the_parsed_query_string_from(request).should == { "one" => ["one two"] } end it "should convert an array of values into multiple entries for the same key" do request = a_request_with_options(:one => %w{one two}) the_parsed_query_string_from(request).should == { "one" => ["one", "two"] } end it "should convert an array of values into a single yaml entry when in legacy mode" do Puppet[:legacy_query_parameter_serialization] = true request = a_request_with_options(:one => %w{one two}) the_parsed_query_string_from(request).should == { "one" => ["--- \n - one\n - two"] } end it "should stringify simple data types inside an array" do request = a_request_with_options(:one => ['one', nil]) the_parsed_query_string_from(request).should == { "one" => ["one"] } end it "should error if an array contains another array" do request = a_request_with_options(:one => ['one', ["not allowed"]]) expect { request.query_string }.to raise_error(ArgumentError) end it "should error if an array contains illegal data" do request = a_request_with_options(:one => ['one', { :not => "allowed" }]) expect { request.query_string }.to raise_error(ArgumentError) end it "should convert to a string and CGI-escape all option values that are symbols" do request = a_request_with_options(:one => :"sym bol") the_parsed_query_string_from(request).should == { "one" => ["sym bol"] } end it "should fail if options other than booleans or strings are provided" do request = a_request_with_options(:one => { :one => :two }) expect { request.query_string }.to raise_error(ArgumentError) end end describe "when converting to json" do before do @request = Puppet::Indirector::Request.new(:facts, :find, "foo", nil) end - it "should produce a hash with the document_type set to 'request'" do - @request.should set_json_document_type_to("IndirectorRequest") - end - it "should set the 'key'" do @request.should set_json_attribute("key").to("foo") end it "should include an attribute for its indirection name" do @request.should set_json_attribute("type").to("facts") end it "should include a 'method' attribute set to its method" do @request.should set_json_attribute("method").to("find") end it "should add all attributes under the 'attributes' attribute" do @request.ip = "127.0.0.1" @request.should set_json_attribute("attributes", "ip").to("127.0.0.1") end it "should add all options under the 'attributes' attribute" do @request.options["opt"] = "value" - PSON.parse(@request.to_pson)["data"]['attributes']['opt'].should == "value" + PSON.parse(@request.to_pson)['attributes']['opt'].should == "value" end it "should include the instance if provided" do facts = Puppet::Node::Facts.new("foo") @request.instance = facts - PSON.parse(@request.to_pson)["data"]['instance'].should be_instance_of(Hash) + PSON.parse(@request.to_pson)['instance'].should be_instance_of(Hash) end end describe "when converting from json" do before do @request = Puppet::Indirector::Request.new(:facts, :find, "foo", nil) @klass = Puppet::Indirector::Request @format = Puppet::Network::FormatHandler.format('pson') end def from_json(json) @format.intern(Puppet::Indirector::Request, json) end it "should set the 'key'" do from_json(@request.to_pson).key.should == "foo" end it "should fail if no key is provided" do json = PSON.parse(@request.to_pson) - json['data'].delete("key") + json.delete("key") expect { from_json(json.to_pson) }.to raise_error(ArgumentError) end it "should set its indirector name" do from_json(@request.to_pson).indirection_name.should == :facts end it "should fail if no type is provided" do json = PSON.parse(@request.to_pson) - json['data'].delete("type") + json.delete("type") expect { from_json(json.to_pson) }.to raise_error(ArgumentError) end it "should set its method" do from_json(@request.to_pson).method.should == "find" end it "should fail if no method is provided" do json = PSON.parse(@request.to_pson) - json['data'].delete("method") + json.delete("method") expect { from_json(json.to_pson) }.to raise_error(ArgumentError) end it "should initialize with all attributes and options" do @request.ip = "127.0.0.1" @request.options["opt"] = "value" result = from_json(@request.to_pson) result.options[:opt].should == "value" result.ip.should == "127.0.0.1" end it "should set its instance as an instance if one is provided" do facts = Puppet::Node::Facts.new("foo") @request.instance = facts result = from_json(@request.to_pson) result.instance.should be_instance_of(Puppet::Node::Facts) end end context '#do_request' do before :each do @request = Puppet::Indirector::Request.new(:myind, :find, "my key", nil) end context 'when not using SRV records' do before :each do Puppet.settings[:use_srv_records] = false end it "yields the request with the default server and port when no server or port were specified on the original request" do count = 0 rval = @request.do_request(:puppet, 'puppet.example.com', '90210') do |got| count += 1 got.server.should == 'puppet.example.com' got.port.should == '90210' 'Block return value' end count.should == 1 rval.should == 'Block return value' end end context 'when using SRV records' do before :each do Puppet.settings[:use_srv_records] = true Puppet.settings[:srv_domain] = 'example.com' end it "yields the request with the original server and port unmodified" do @request.server = 'puppet.example.com' @request.port = '90210' count = 0 rval = @request.do_request do |got| count += 1 got.server.should == 'puppet.example.com' got.port.should == '90210' 'Block return value' end count.should == 1 rval.should == 'Block return value' end context "when SRV returns servers" do before :each do @dns_mock = mock('dns') Resolv::DNS.expects(:new).returns(@dns_mock) @port = 7205 @host = '_x-puppet._tcp.example.com' @srv_records = [Resolv::DNS::Resource::IN::SRV.new(0, 0, @port, @host)] @dns_mock.expects(:getresources). with("_x-puppet._tcp.#{Puppet.settings[:srv_domain]}", Resolv::DNS::Resource::IN::SRV). returns(@srv_records) end it "yields a request using the server and port from the SRV record" do count = 0 rval = @request.do_request do |got| count += 1 got.server.should == '_x-puppet._tcp.example.com' got.port.should == 7205 @block_return end count.should == 1 rval.should == @block_return end it "should fall back to the default server when the block raises a SystemCallError" do count = 0 second_pass = nil rval = @request.do_request(:puppet, 'puppet', 8140) do |got| count += 1 if got.server == '_x-puppet._tcp.example.com' then raise SystemCallError, "example failure" else second_pass = got end @block_return end second_pass.server.should == 'puppet' second_pass.port.should == 8140 count.should == 2 rval.should == @block_return end end end end describe "#remote?" do def request(options = {}) Puppet::Indirector::Request.new('node', 'find', 'localhost', nil, options) end it "should not be unless node or ip is set" do request.should_not be_remote end it "should be remote if node is set" do request(:node => 'example.com').should be_remote end it "should be remote if ip is set" do request(:ip => '127.0.0.1').should be_remote end it "should be remote if node and ip are set" do request(:node => 'example.com', :ip => '127.0.0.1').should be_remote end end end diff --git a/spec/unit/node_spec.rb b/spec/unit/node_spec.rb index 2de2b8279..406df2765 100755 --- a/spec/unit/node_spec.rb +++ b/spec/unit/node_spec.rb @@ -1,321 +1,313 @@ #! /usr/bin/env ruby require 'spec_helper' require 'matchers/json' describe Puppet::Node do include JSONMatchers let(:environment) { Puppet::Node::Environment.create(:bar, []) } let(:env_loader) { Puppet::Environments::Static.new(environment) } - it "should register its document type as Node" do - PSON.registered_document_types["Node"].should equal(Puppet::Node) - end - describe "when managing its environment" do it "should use any set environment" do Puppet.override(:environments => env_loader) do Puppet::Node.new("foo", :environment => "bar").environment.should == environment end end it "should support providing an actual environment instance" do Puppet::Node.new("foo", :environment => environment).environment.name.should == :bar end it "should determine its environment from its parameters if no environment is set" do Puppet.override(:environments => env_loader) do Puppet::Node.new("foo", :parameters => {"environment" => :bar}).environment.should == environment end end it "should use the configured environment if no environment is provided" do Puppet[:environment] = environment.name.to_s Puppet.override(:environments => env_loader) do Puppet::Node.new("foo").environment.should == environment end end it "should allow the environment to be set after initialization" do node = Puppet::Node.new("foo") node.environment = :bar node.environment.name.should == :bar end it "should allow its environment to be set by parameters after initialization" do node = Puppet::Node.new("foo") node.parameters["environment"] = :bar node.environment.name.should == :bar end end it "can survive a round-trip through YAML" do facts = Puppet::Node::Facts.new("hello", "one" => "c", "two" => "b") node = Puppet::Node.new("hello", :environment => 'kjhgrg', :classes => ['erth', 'aiu'], :parameters => {"hostname"=>"food"} ) new_node = Puppet::Node.convert_from('yaml', node.render('yaml')) new_node.environment.should == node.environment new_node.parameters.should == node.parameters new_node.classes.should == node.classes new_node.name.should == node.name end it "can round-trip through pson" do facts = Puppet::Node::Facts.new("hello", "one" => "c", "two" => "b") node = Puppet::Node.new("hello", :environment => 'kjhgrg', :classes => ['erth', 'aiu'], :parameters => {"hostname"=>"food"} ) new_node = Puppet::Node.convert_from('pson', node.render('pson')) new_node.environment.should == node.environment new_node.parameters.should == node.parameters new_node.classes.should == node.classes new_node.name.should == node.name end it "validates against the node json schema", :unless => Puppet.features.microsoft_windows? do facts = Puppet::Node::Facts.new("hello", "one" => "c", "two" => "b") node = Puppet::Node.new("hello", :environment => 'kjhgrg', :classes => ['erth', 'aiu'], :parameters => {"hostname"=>"food"} ) expect(node.to_pson).to validate_against('api/schemas/node.json') end it "when missing optional parameters validates against the node json schema", :unless => Puppet.features.microsoft_windows? do facts = Puppet::Node::Facts.new("hello", "one" => "c", "two" => "b") node = Puppet::Node.new("hello", :environment => 'kjhgrg' ) expect(node.to_pson).to validate_against('api/schemas/node.json') end describe "when converting to json" do before do @node = Puppet::Node.new("mynode") end it "should provide its name" do @node.should set_json_attribute('name').to("mynode") end - it "should produce a hash with the document_type set to 'Node'" do - @node.should set_json_document_type_to("Node") - end - it "should include the classes if set" do @node.classes = %w{a b c} @node.should set_json_attribute("classes").to(%w{a b c}) end it "should not include the classes if there are none" do @node.should_not set_json_attribute('classes') end it "should include parameters if set" do @node.parameters = {"a" => "b", "c" => "d"} @node.should set_json_attribute('parameters').to({"a" => "b", "c" => "d"}) end it "should not include the parameters if there are none" do @node.should_not set_json_attribute('parameters') end it "should include the environment" do @node.environment = "production" @node.should set_json_attribute('environment').to('production') end end describe "when converting from json" do before do @node = Puppet::Node.new("mynode") @format = Puppet::Network::FormatHandler.format('pson') end def from_json(json) @format.intern(Puppet::Node, json) end it "should set its name" do Puppet::Node.should read_json_attribute('name').from(@node.to_pson).as("mynode") end it "should include the classes if set" do @node.classes = %w{a b c} Puppet::Node.should read_json_attribute('classes').from(@node.to_pson).as(%w{a b c}) end it "should include parameters if set" do @node.parameters = {"a" => "b", "c" => "d"} Puppet::Node.should read_json_attribute('parameters').from(@node.to_pson).as({"a" => "b", "c" => "d"}) end it "deserializes environment to environment_name as a string" do @node.environment = environment Puppet::Node.should read_json_attribute('environment_name').from(@node.to_pson).as('bar') end end end describe Puppet::Node, "when initializing" do before do @node = Puppet::Node.new("testnode") end it "should set the node name" do @node.name.should == "testnode" end it "should not allow nil node names" do proc { Puppet::Node.new(nil) }.should raise_error(ArgumentError) end it "should default to an empty parameter hash" do @node.parameters.should == {} end it "should default to an empty class array" do @node.classes.should == [] end it "should note its creation time" do @node.time.should be_instance_of(Time) end it "should accept parameters passed in during initialization" do params = {"a" => "b"} @node = Puppet::Node.new("testing", :parameters => params) @node.parameters.should == params end it "should accept classes passed in during initialization" do classes = %w{one two} @node = Puppet::Node.new("testing", :classes => classes) @node.classes.should == classes end it "should always return classes as an array" do @node = Puppet::Node.new("testing", :classes => "myclass") @node.classes.should == ["myclass"] end end describe Puppet::Node, "when merging facts" do before do @node = Puppet::Node.new("testnode") Puppet::Node::Facts.indirection.stubs(:find).with(@node.name, instance_of(Hash)).returns(Puppet::Node::Facts.new(@node.name, "one" => "c", "two" => "b")) end it "should fail intelligently if it cannot find facts" do Puppet::Node::Facts.indirection.expects(:find).with(@node.name, instance_of(Hash)).raises "foo" lambda { @node.fact_merge }.should raise_error(Puppet::Error) end it "should prefer parameters already set on the node over facts from the node" do @node = Puppet::Node.new("testnode", :parameters => {"one" => "a"}) @node.fact_merge @node.parameters["one"].should == "a" end it "should add passed parameters to the parameter list" do @node = Puppet::Node.new("testnode", :parameters => {"one" => "a"}) @node.fact_merge @node.parameters["two"].should == "b" end it "should accept arbitrary parameters to merge into its parameters" do @node = Puppet::Node.new("testnode", :parameters => {"one" => "a"}) @node.merge "two" => "three" @node.parameters["two"].should == "three" end it "should add the environment to the list of parameters" do Puppet[:environment] = "one" @node = Puppet::Node.new("testnode", :environment => "one") @node.merge "two" => "three" @node.parameters["environment"].should == "one" end it "should not set the environment if it is already set in the parameters" do Puppet[:environment] = "one" @node = Puppet::Node.new("testnode", :environment => "one") @node.merge "environment" => "two" @node.parameters["environment"].should == "two" end end describe Puppet::Node, "when indirecting" do it "should default to the 'plain' node terminus" do Puppet::Node.indirection.reset_terminus_class Puppet::Node.indirection.terminus_class.should == :plain end end describe Puppet::Node, "when generating the list of names to search through" do before do @node = Puppet::Node.new("foo.domain.com", :parameters => {"hostname" => "yay", "domain" => "domain.com"}) end it "should return an array of names" do @node.names.should be_instance_of(Array) end describe "and the node name is fully qualified" do it "should contain an entry for each part of the node name" do @node.names.should be_include("foo.domain.com") @node.names.should be_include("foo.domain") @node.names.should be_include("foo") end end it "should include the node's fqdn" do @node.names.should be_include("yay.domain.com") end it "should combine and include the node's hostname and domain if no fqdn is available" do @node.names.should be_include("yay.domain.com") end it "should contain an entry for each name available by stripping a segment of the fqdn" do @node.parameters["fqdn"] = "foo.deep.sub.domain.com" @node.names.should be_include("foo.deep.sub.domain") @node.names.should be_include("foo.deep.sub") end describe "and :node_name is set to 'cert'" do before do Puppet[:strict_hostname_checking] = false Puppet[:node_name] = "cert" end it "should use the passed-in key as the first value" do @node.names[0].should == "foo.domain.com" end describe "and strict hostname checking is enabled" do it "should only use the passed-in key" do Puppet[:strict_hostname_checking] = true @node.names.should == ["foo.domain.com"] end end end describe "and :node_name is set to 'facter'" do before do Puppet[:strict_hostname_checking] = false Puppet[:node_name] = "facter" end it "should use the node's 'hostname' fact as the first value" do @node.names[0].should == "yay" end end end diff --git a/spec/unit/resource/catalog_spec.rb b/spec/unit/resource/catalog_spec.rb index 80933dc41..5bc38c0bf 100755 --- a/spec/unit/resource/catalog_spec.rb +++ b/spec/unit/resource/catalog_spec.rb @@ -1,884 +1,866 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet_spec/compiler' require 'matchers/json' describe Puppet::Resource::Catalog, "when compiling" do include JSONMatchers include PuppetSpec::Files before do @basepath = make_absolute("/somepath") # stub this to not try to create state.yaml Puppet::Util::Storage.stubs(:store) end # audit only resources are unmanaged # as are resources without properties with should values it "should write its managed resources' types, namevars" do catalog = Puppet::Resource::Catalog.new("host") resourcefile = tmpfile('resourcefile') Puppet[:resourcefile] = resourcefile res = Puppet::Type.type('file').new(:title => File.expand_path('/tmp/sam'), :ensure => 'present') res.file = 'site.pp' res.line = 21 res2 = Puppet::Type.type('exec').new(:title => 'bob', :command => "#{File.expand_path('/bin/rm')} -rf /") res2.file = File.expand_path('/modules/bob/manifests/bob.pp') res2.line = 42 res3 = Puppet::Type.type('file').new(:title => File.expand_path('/tmp/susan'), :audit => 'all') res3.file = 'site.pp' res3.line = 63 res4 = Puppet::Type.type('file').new(:title => File.expand_path('/tmp/lilly')) res4.file = 'site.pp' res4.line = 84 comp_res = Puppet::Type.type('component').new(:title => 'Class[Main]') catalog.add_resource(res, res2, res3, res4, comp_res) catalog.write_resource_file File.readlines(resourcefile).map(&:chomp).should =~ [ "file[#{File.expand_path('/tmp/sam')}]", "exec[#{File.expand_path('/bin/rm')} -rf /]" ] end it "should log an error if unable to write to the resource file" do catalog = Puppet::Resource::Catalog.new("host") Puppet[:resourcefile] = File.expand_path('/not/writable/file') catalog.add_resource(Puppet::Type.type('file').new(:title => File.expand_path('/tmp/foo'))) catalog.write_resource_file @logs.size.should == 1 @logs.first.message.should =~ /Could not create resource file/ @logs.first.level.should == :err end it "should be able to write its list of classes to the class file" do @catalog = Puppet::Resource::Catalog.new("host") @catalog.add_class "foo", "bar" Puppet[:classfile] = File.expand_path("/class/file") fh = mock 'filehandle' File.expects(:open).with(Puppet[:classfile], "w").yields fh fh.expects(:puts).with "foo\nbar" @catalog.write_class_file end it "should have a client_version attribute" do @catalog = Puppet::Resource::Catalog.new("host") @catalog.client_version = 5 @catalog.client_version.should == 5 end it "should have a server_version attribute" do @catalog = Puppet::Resource::Catalog.new("host") @catalog.server_version = 5 @catalog.server_version.should == 5 end describe "when compiling" do it "should accept tags" do config = Puppet::Resource::Catalog.new("mynode") config.tag("one") config.should be_tagged("one") end it "should accept multiple tags at once" do config = Puppet::Resource::Catalog.new("mynode") config.tag("one", "two") config.should be_tagged("one") config.should be_tagged("two") end it "should convert all tags to strings" do config = Puppet::Resource::Catalog.new("mynode") config.tag("one", :two) config.should be_tagged("one") config.should be_tagged("two") end it "should tag with both the qualified name and the split name" do config = Puppet::Resource::Catalog.new("mynode") config.tag("one::two") config.should be_tagged("one") config.should be_tagged("one::two") end it "should accept classes" do config = Puppet::Resource::Catalog.new("mynode") config.add_class("one") config.classes.should == %w{one} config.add_class("two", "three") config.classes.should == %w{one two three} end it "should tag itself with passed class names" do config = Puppet::Resource::Catalog.new("mynode") config.add_class("one") config.should be_tagged("one") end end describe "when converting to a RAL catalog" do before do @original = Puppet::Resource::Catalog.new("mynode") @original.tag(*%w{one two three}) @original.add_class *%w{four five six} @top = Puppet::Resource.new :class, 'top' @topobject = Puppet::Resource.new :file, @basepath+'/topobject' @middle = Puppet::Resource.new :class, 'middle' @middleobject = Puppet::Resource.new :file, @basepath+'/middleobject' @bottom = Puppet::Resource.new :class, 'bottom' @bottomobject = Puppet::Resource.new :file, @basepath+'/bottomobject' @resources = [@top, @topobject, @middle, @middleobject, @bottom, @bottomobject] @original.add_resource(*@resources) @original.add_edge(@top, @topobject) @original.add_edge(@top, @middle) @original.add_edge(@middle, @middleobject) @original.add_edge(@middle, @bottom) @original.add_edge(@bottom, @bottomobject) @catalog = @original.to_ral end it "should add all resources as RAL instances" do @resources.each do |resource| # Warning: a failure here will result in "global resource iteration is # deprecated" being raised, because the rspec rendering to get the # result tries to call `each` on the resource, and that raises. @catalog.resource(resource.ref).must be_a_kind_of(Puppet::Type) end end it "should copy the tag list to the new catalog" do @catalog.tags.sort.should == @original.tags.sort end it "should copy the class list to the new catalog" do @catalog.classes.should == @original.classes end it "should duplicate the original edges" do @original.edges.each do |edge| @catalog.edge?(@catalog.resource(edge.source.ref), @catalog.resource(edge.target.ref)).should be_true end end it "should set itself as the catalog for each converted resource" do @catalog.vertices.each { |v| v.catalog.object_id.should equal(@catalog.object_id) } end # This tests #931. it "should not lose track of resources whose names vary" do changer = Puppet::Resource.new :file, @basepath+'/test/', :parameters => {:ensure => :directory} config = Puppet::Resource::Catalog.new('test') config.add_resource(changer) config.add_resource(@top) config.add_edge(@top, changer) catalog = config.to_ral catalog.resource("File[#{@basepath}/test/]").must equal(catalog.resource("File[#{@basepath}/test]")) end after do # Remove all resource instances. @catalog.clear(true) end end describe "when filtering" do before :each do @original = Puppet::Resource::Catalog.new("mynode") @original.tag(*%w{one two three}) @original.add_class *%w{four five six} @r1 = stub_everything 'r1', :ref => "File[/a]" @r1.stubs(:respond_to?).with(:ref).returns(true) @r1.stubs(:copy_as_resource).returns(@r1) @r1.stubs(:is_a?).with(Puppet::Resource).returns(true) @r2 = stub_everything 'r2', :ref => "File[/b]" @r2.stubs(:respond_to?).with(:ref).returns(true) @r2.stubs(:copy_as_resource).returns(@r2) @r2.stubs(:is_a?).with(Puppet::Resource).returns(true) @resources = [@r1,@r2] @original.add_resource(@r1,@r2) end it "should transform the catalog to a resource catalog" do @original.expects(:to_catalog).with { |h,b| h == :to_resource } @original.filter end it "should scan each catalog resource in turn and apply filtering block" do @resources.each { |r| r.expects(:test?) } @original.filter do |r| r.test? end end it "should filter out resources which produce true when the filter block is evaluated" do @original.filter do |r| r == @r1 end.resource("File[/a]").should be_nil end it "should not consider edges against resources that were filtered out" do @original.add_edge(@r1,@r2) @original.filter do |r| r == @r1 end.edge?(@r1,@r2).should_not be end end describe "when functioning as a resource container" do before do @catalog = Puppet::Resource::Catalog.new("host") @one = Puppet::Type.type(:notify).new :name => "one" @two = Puppet::Type.type(:notify).new :name => "two" @dupe = Puppet::Type.type(:notify).new :name => "one" end it "should provide a method to add one or more resources" do @catalog.add_resource @one, @two @catalog.resource(@one.ref).must equal(@one) @catalog.resource(@two.ref).must equal(@two) end it "should add resources to the relationship graph if it exists" do relgraph = @catalog.relationship_graph @catalog.add_resource @one relgraph.should be_vertex(@one) end it "should set itself as the resource's catalog if it is not a relationship graph" do @one.expects(:catalog=).with(@catalog) @catalog.add_resource @one end it "should make all vertices available by resource reference" do @catalog.add_resource(@one) @catalog.resource(@one.ref).must equal(@one) @catalog.vertices.find { |r| r.ref == @one.ref }.must equal(@one) end it "tracks the container through edges" do @catalog.add_resource(@two) @catalog.add_resource(@one) @catalog.add_edge(@one, @two) @catalog.container_of(@two).must == @one end it "a resource without a container is contained in nil" do @catalog.add_resource(@one) @catalog.container_of(@one).must be_nil end it "should canonize how resources are referred to during retrieval when both type and title are provided" do @catalog.add_resource(@one) @catalog.resource("notify", "one").must equal(@one) end it "should canonize how resources are referred to during retrieval when just the title is provided" do @catalog.add_resource(@one) @catalog.resource("notify[one]", nil).must equal(@one) end describe 'with a duplicate resource' do def resource_at(type, name, file, line) resource = Puppet::Resource.new(type, name) resource.file = file resource.line = line Puppet::Type.type(type).new(resource) end let(:orig) { resource_at(:notify, 'duplicate-title', '/path/to/orig/file', 42) } let(:dupe) { resource_at(:notify, 'duplicate-title', '/path/to/dupe/file', 314) } it "should print the locations of the original duplicated resource" do @catalog.add_resource(orig) expect { @catalog.add_resource(dupe) }.to raise_error { |error| error.should be_a Puppet::Resource::Catalog::DuplicateResourceError error.message.should match %r[Duplicate declaration: Notify\[duplicate-title\] is already declared] error.message.should match %r[in file /path/to/orig/file:42] error.message.should match %r[cannot redeclare] error.message.should match %r[at /path/to/dupe/file:314] } end end it "should remove all resources when asked" do @catalog.add_resource @one @catalog.add_resource @two @one.expects :remove @two.expects :remove @catalog.clear(true) end it "should support a mechanism for finishing resources" do @one.expects :finish @two.expects :finish @catalog.add_resource @one @catalog.add_resource @two @catalog.finalize end it "should make default resources when finalizing" do @catalog.expects(:make_default_resources) @catalog.finalize end it "should add default resources to the catalog upon creation" do @catalog.make_default_resources @catalog.resource(:schedule, "daily").should_not be_nil end it "should optionally support an initialization block and should finalize after such blocks" do @one.expects :finish @two.expects :finish config = Puppet::Resource::Catalog.new("host") do |conf| conf.add_resource @one conf.add_resource @two end end it "should inform the resource that it is the resource's catalog" do @one.expects(:catalog=).with(@catalog) @catalog.add_resource @one end it "should be able to find resources by reference" do @catalog.add_resource @one @catalog.resource(@one.ref).must equal(@one) end it "should be able to find resources by reference or by type/title tuple" do @catalog.add_resource @one @catalog.resource("notify", "one").must equal(@one) end it "should have a mechanism for removing resources" do @catalog.add_resource(@one) @catalog.resource(@one.ref).must be @catalog.vertex?(@one).must be_true @catalog.remove_resource(@one) @catalog.resource(@one.ref).must be_nil @catalog.vertex?(@one).must be_false end it "should have a method for creating aliases for resources" do @catalog.add_resource @one @catalog.alias(@one, "other") @catalog.resource("notify", "other").must equal(@one) end it "should ignore conflicting aliases that point to the aliased resource" do @catalog.alias(@one, "other") lambda { @catalog.alias(@one, "other") }.should_not raise_error end it "should create aliases for isomorphic resources whose names do not match their titles" do resource = Puppet::Type::File.new(:title => "testing", :path => @basepath+"/something") @catalog.add_resource(resource) @catalog.resource(:file, @basepath+"/something").must equal(resource) end it "should not create aliases for non-isomorphic resources whose names do not match their titles" do resource = Puppet::Type.type(:exec).new(:title => "testing", :command => "echo", :path => %w{/bin /usr/bin /usr/local/bin}) @catalog.add_resource(resource) # Yay, I've already got a 'should' method @catalog.resource(:exec, "echo").object_id.should == nil.object_id end # This test is the same as the previous, but the behaviour should be explicit. it "should alias using the class name from the resource reference, not the resource class name" do @catalog.add_resource @one @catalog.alias(@one, "other") @catalog.resource("notify", "other").must equal(@one) end it "should fail to add an alias if the aliased name already exists" do @catalog.add_resource @one proc { @catalog.alias @two, "one" }.should raise_error(ArgumentError) end it "should not fail when a resource has duplicate aliases created" do @catalog.add_resource @one proc { @catalog.alias @one, "one" }.should_not raise_error end it "should not create aliases that point back to the resource" do @catalog.alias(@one, "one") @catalog.resource(:notify, "one").must be_nil end it "should be able to look resources up by their aliases" do @catalog.add_resource @one @catalog.alias @one, "two" @catalog.resource(:notify, "two").must equal(@one) end it "should remove resource aliases when the target resource is removed" do @catalog.add_resource @one @catalog.alias(@one, "other") @one.expects :remove @catalog.remove_resource(@one) @catalog.resource("notify", "other").must be_nil end it "should add an alias for the namevar when the title and name differ on isomorphic resource types" do resource = Puppet::Type.type(:file).new :path => @basepath+"/something", :title => "other", :content => "blah" resource.expects(:isomorphic?).returns(true) @catalog.add_resource(resource) @catalog.resource(:file, "other").must equal(resource) @catalog.resource(:file, @basepath+"/something").ref.should == resource.ref end it "should not add an alias for the namevar when the title and name differ on non-isomorphic resource types" do resource = Puppet::Type.type(:file).new :path => @basepath+"/something", :title => "other", :content => "blah" resource.expects(:isomorphic?).returns(false) @catalog.add_resource(resource) @catalog.resource(:file, resource.title).must equal(resource) # We can't use .should here, because the resources respond to that method. raise "Aliased non-isomorphic resource" if @catalog.resource(:file, resource.name) end it "should provide a method to create additional resources that also registers the resource" do args = {:name => "/yay", :ensure => :file} resource = stub 'file', :ref => "File[/yay]", :catalog= => @catalog, :title => "/yay", :[] => "/yay" Puppet::Type.type(:file).expects(:new).with(args).returns(resource) @catalog.create_resource :file, args @catalog.resource("File[/yay]").must equal(resource) end describe "when adding resources with multiple namevars" do before :each do Puppet::Type.newtype(:multiple) do newparam(:color, :namevar => true) newparam(:designation, :namevar => true) def self.title_patterns [ [ /^(\w+) (\w+)$/, [ [:color, lambda{|x| x}], [:designation, lambda{|x| x}] ] ] ] end end end it "should add an alias using the uniqueness key" do @resource = Puppet::Type.type(:multiple).new(:title => "some resource", :color => "red", :designation => "5") @catalog.add_resource(@resource) @catalog.resource(:multiple, "some resource").must == @resource @catalog.resource("Multiple[some resource]").must == @resource @catalog.resource("Multiple[red 5]").must == @resource end it "should conflict with a resource with the same uniqueness key" do @resource = Puppet::Type.type(:multiple).new(:title => "some resource", :color => "red", :designation => "5") @other = Puppet::Type.type(:multiple).new(:title => "another resource", :color => "red", :designation => "5") @catalog.add_resource(@resource) expect { @catalog.add_resource(@other) }.to raise_error(ArgumentError, /Cannot alias Multiple\[another resource\] to \["red", "5"\].*resource \["Multiple", "red", "5"\] already declared/) end it "should conflict when its uniqueness key matches another resource's title" do path = make_absolute("/tmp/foo") @resource = Puppet::Type.type(:file).new(:title => path) @other = Puppet::Type.type(:file).new(:title => "another file", :path => path) @catalog.add_resource(@resource) expect { @catalog.add_resource(@other) }.to raise_error(ArgumentError, /Cannot alias File\[another file\] to \["#{Regexp.escape(path)}"\].*resource \["File", "#{Regexp.escape(path)}"\] already declared/) end it "should conflict when its uniqueness key matches the uniqueness key derived from another resource's title" do @resource = Puppet::Type.type(:multiple).new(:title => "red leader") @other = Puppet::Type.type(:multiple).new(:title => "another resource", :color => "red", :designation => "leader") @catalog.add_resource(@resource) expect { @catalog.add_resource(@other) }.to raise_error(ArgumentError, /Cannot alias Multiple\[another resource\] to \["red", "leader"\].*resource \["Multiple", "red", "leader"\] already declared/) end end end describe "when applying" do before :each do @catalog = Puppet::Resource::Catalog.new("host") @transaction = Puppet::Transaction.new(@catalog, nil, Puppet::Graph::RandomPrioritizer.new) Puppet::Transaction.stubs(:new).returns(@transaction) @transaction.stubs(:evaluate) @transaction.stubs(:for_network_device=) Puppet.settings.stubs(:use) end it "should create and evaluate a transaction" do @transaction.expects(:evaluate) @catalog.apply end it "should return the transaction" do @catalog.apply.should equal(@transaction) end it "should yield the transaction if a block is provided" do @catalog.apply do |trans| trans.should equal(@transaction) end end it "should default to being a host catalog" do @catalog.host_config.should be_true end it "should be able to be set to a non-host_config" do @catalog.host_config = false @catalog.host_config.should be_false end it "should pass supplied tags on to the transaction" do @transaction.expects(:tags=).with(%w{one two}) @catalog.apply(:tags => %w{one two}) end it "should set ignoreschedules on the transaction if specified in apply()" do @transaction.expects(:ignoreschedules=).with(true) @catalog.apply(:ignoreschedules => true) end describe "host catalogs" do # super() doesn't work in the setup method for some reason before do @catalog.host_config = true Puppet::Util::Storage.stubs(:store) end it "should initialize the state database before applying a catalog" do Puppet::Util::Storage.expects(:load) # Short-circuit the apply, so we know we're loading before the transaction Puppet::Transaction.expects(:new).raises ArgumentError proc { @catalog.apply }.should raise_error(ArgumentError) end it "should sync the state database after applying" do Puppet::Util::Storage.expects(:store) @transaction.stubs :any_failed? => false @catalog.apply end end describe "non-host catalogs" do before do @catalog.host_config = false end it "should never send reports" do Puppet[:report] = true Puppet[:summarize] = true @catalog.apply end it "should never modify the state database" do Puppet::Util::Storage.expects(:load).never Puppet::Util::Storage.expects(:store).never @catalog.apply end end end describe "when creating a relationship graph" do before do @catalog = Puppet::Resource::Catalog.new("host") end it "should get removed when the catalog is cleaned up" do @catalog.relationship_graph.expects(:clear) @catalog.clear @catalog.instance_variable_get("@relationship_graph").should be_nil end end describe "when writing dot files" do before do @catalog = Puppet::Resource::Catalog.new("host") @name = :test @file = File.join(Puppet[:graphdir], @name.to_s + ".dot") end it "should only write when it is a host catalog" do File.expects(:open).with(@file).never @catalog.host_config = false Puppet[:graph] = true @catalog.write_graph(@name) end end describe "when indirecting" do before do @real_indirection = Puppet::Resource::Catalog.indirection @indirection = stub 'indirection', :name => :catalog end it "should use the value of the 'catalog_terminus' setting to determine its terminus class" do # Puppet only checks the terminus setting the first time you ask # so this returns the object to the clean state # at the expense of making this test less pure Puppet::Resource::Catalog.indirection.reset_terminus_class Puppet.settings[:catalog_terminus] = "rest" Puppet::Resource::Catalog.indirection.terminus_class.should == :rest end it "should allow the terminus class to be set manually" do Puppet::Resource::Catalog.indirection.terminus_class = :rest Puppet::Resource::Catalog.indirection.terminus_class.should == :rest end after do @real_indirection.reset_terminus_class end end describe "when converting to yaml" do before do @catalog = Puppet::Resource::Catalog.new("me") @catalog.add_edge("one", "two") end it "should be able to be dumped to yaml" do YAML.dump(@catalog).should be_instance_of(String) end end describe "when converting from yaml" do before do @catalog = Puppet::Resource::Catalog.new("me") @catalog.add_edge("one", "two") text = YAML.dump(@catalog) @newcatalog = YAML.load(text) end it "should get converted back to a catalog" do @newcatalog.should be_instance_of(Puppet::Resource::Catalog) end it "should have all vertices" do @newcatalog.vertex?("one").should be_true @newcatalog.vertex?("two").should be_true end it "should have all edges" do @newcatalog.edge?("one", "two").should be_true end end end describe Puppet::Resource::Catalog, "when converting a resource catalog to pson" do include JSONMatchers include PuppetSpec::Compiler it "should validate an empty catalog against the schema" do empty_catalog = compile_to_catalog("") expect(empty_catalog.to_pson).to validate_against('api/schemas/catalog.json') end it "should validate a noop catalog against the schema" do noop_catalog = compile_to_catalog("create_resources('file', {})") expect(noop_catalog.to_pson).to validate_against('api/schemas/catalog.json') end it "should validate a single resource catalog against the schema" do catalog = compile_to_catalog("create_resources('file', {'/etc/foo'=>{'ensure'=>'present'}})") expect(catalog.to_pson).to validate_against('api/schemas/catalog.json') end it "should validate a virtual resource catalog against the schema" do catalog = compile_to_catalog("create_resources('@file', {'/etc/foo'=>{'ensure'=>'present'}})\nrealize(File['/etc/foo'])") expect(catalog.to_pson).to validate_against('api/schemas/catalog.json') end it "should validate a single exported resource catalog against the schema" do catalog = compile_to_catalog("create_resources('@@file', {'/etc/foo'=>{'ensure'=>'present'}})") expect(catalog.to_pson).to validate_against('api/schemas/catalog.json') end it "should validate a two resource catalog against the schema" do catalog = compile_to_catalog("create_resources('notify', {'foo'=>{'message'=>'one'}, 'bar'=>{'message'=>'two'}})") expect(catalog.to_pson).to validate_against('api/schemas/catalog.json') end it "should validate a two parameter class catalog against the schema" do catalog = compile_to_catalog(<<-MANIFEST) class multi_param_class ($one, $two) { notify {'foo': message => "One is $one, two is $two", } } class {'multi_param_class': one => 'hello', two => 'world', } MANIFEST expect(catalog.to_pson).to validate_against('api/schemas/catalog.json') end end describe Puppet::Resource::Catalog, "when converting to pson" do before do @catalog = Puppet::Resource::Catalog.new("myhost") end def pson_output_should - @catalog.class.expects(:pson_create).with { |hash| yield hash }.returns(:something) - end - - # LAK:NOTE For all of these tests, we convert back to the resource so we can - # trap the actual data structure then. - it "should set its document_type to 'Catalog'" do - pson_output_should { |hash| hash['document_type'] == "Catalog" } - - PSON.parse @catalog.to_pson - end - - it "should set its data as a hash" do - pson_output_should { |hash| hash['data'].is_a?(Hash) } - PSON.parse @catalog.to_pson + @catalog.class.expects(:from_data_hash).with { |hash| yield hash }.returns(:something) end [:name, :version, :classes].each do |param| it "should set its #{param} to the #{param} of the resource" do @catalog.send(param.to_s + "=", "testing") unless @catalog.send(param) - pson_output_should { |hash| hash['data'][param.to_s].should == @catalog.send(param) } - PSON.parse @catalog.to_pson + pson_output_should { |hash| hash[param.to_s].should == @catalog.send(param) } + Puppet::Resource::Catalog.from_data_hash PSON.parse @catalog.to_pson end end it "should convert its resources to a PSON-encoded array and store it as the 'resources' data" do - one = stub 'one', :to_pson_data_hash => "one_resource", :ref => "Foo[one]" - two = stub 'two', :to_pson_data_hash => "two_resource", :ref => "Foo[two]" + one = stub 'one', :to_data_hash => "one_resource", :ref => "Foo[one]" + two = stub 'two', :to_data_hash => "two_resource", :ref => "Foo[two]" @catalog.add_resource(one) @catalog.add_resource(two) # TODO this should really guarantee sort order - PSON.parse(@catalog.to_pson,:create_additions => false)['data']['resources'].sort.should == ["one_resource", "two_resource"].sort + PSON.parse(@catalog.to_pson,:create_additions => false)['resources'].sort.should == ["one_resource", "two_resource"].sort end it "should convert its edges to a PSON-encoded array and store it as the 'edges' data" do - one = stub 'one', :to_pson_data_hash => "one_resource", :ref => 'Foo[one]' - two = stub 'two', :to_pson_data_hash => "two_resource", :ref => 'Foo[two]' - three = stub 'three', :to_pson_data_hash => "three_resource", :ref => 'Foo[three]' + one = stub 'one', :to_data_hash => "one_resource", :ref => 'Foo[one]' + two = stub 'two', :to_data_hash => "two_resource", :ref => 'Foo[two]' + three = stub 'three', :to_data_hash => "three_resource", :ref => 'Foo[three]' @catalog.add_edge(one, two) @catalog.add_edge(two, three) - @catalog.edges_between(one, two )[0].expects(:to_pson_data_hash).returns "one_two_pson" - @catalog.edges_between(two, three)[0].expects(:to_pson_data_hash).returns "two_three_pson" + @catalog.edges_between(one, two )[0].expects(:to_data_hash).returns "one_two_pson" + @catalog.edges_between(two, three)[0].expects(:to_data_hash).returns "two_three_pson" - PSON.parse(@catalog.to_pson,:create_additions => false)['data']['edges'].sort.should == %w{one_two_pson two_three_pson}.sort + PSON.parse(@catalog.to_pson,:create_additions => false)['edges'].sort.should == %w{one_two_pson two_three_pson}.sort end end describe Puppet::Resource::Catalog, "when converting from pson" do before do @data = { 'name' => "myhost" } - @pson = { - 'document_type' => 'Puppet::Resource::Catalog', - 'data' => @data, - 'metadata' => {} - } end it "should create it with the provided name" do @data['version'] = 50 @data['tags'] = %w{one two} @data['classes'] = %w{one two} @data['edges'] = [Puppet::Relationship.new("File[/foo]", "File[/bar]", :event => "one", :callback => "refresh").to_data_hash] @data['resources'] = [Puppet::Resource.new(:file, "/foo").to_data_hash, Puppet::Resource.new(:file, "/bar").to_data_hash] - catalog = PSON.parse @pson.to_pson + catalog = Puppet::Resource::Catalog.from_data_hash PSON.parse @data.to_pson expect(catalog.name).to eq('myhost') expect(catalog.version).to eq(@data['version']) expect(catalog).to be_tagged("one") expect(catalog).to be_tagged("two") expect(catalog.classes).to eq(@data['classes']) expect(catalog.resources.collect(&:ref)).to eq(["File[/foo]", "File[/bar]"]) expect(catalog.edges.collect(&:event)).to eq(["one"]) expect(catalog.edges[0].source).to eq(catalog.resource(:file, "/foo")) expect(catalog.edges[0].target).to eq(catalog.resource(:file, "/bar")) end it "should fail if the source resource cannot be found" do @data['edges'] = [Puppet::Relationship.new("File[/missing]", "File[/bar]").to_data_hash] @data['resources'] = [Puppet::Resource.new(:file, "/bar").to_data_hash] - expect { PSON.parse @pson.to_pson }.to raise_error(ArgumentError, /Could not find relationship source/) + expect { Puppet::Resource::Catalog.from_data_hash PSON.parse @data.to_pson }.to raise_error(ArgumentError, /Could not find relationship source/) end it "should fail if the target resource cannot be found" do @data['edges'] = [Puppet::Relationship.new("File[/bar]", "File[/missing]").to_data_hash] @data['resources'] = [Puppet::Resource.new(:file, "/bar").to_data_hash] - expect { PSON.parse @pson.to_pson }.to raise_error(ArgumentError, /Could not find relationship target/) + expect { Puppet::Resource::Catalog.from_data_hash PSON.parse @data.to_pson }.to raise_error(ArgumentError, /Could not find relationship target/) end end