We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
404 on cm.destroy should be a warning.
It's all too easy to run into the case where something errored in formations:create and we have a Django database entry for the formation, but no Chef node. The recent merge makes it impossible to delete the Formation in that case. If we instead treat the Chef 404 as a warning, this will work.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `cm/chef.py`
Content:
```
1 """
2 Deis configuration management implementation for Opscode Chef.
3 """
4 
5 from __future__ import unicode_literals
6 
7 import os
8 import re
9 import subprocess
10 import tempfile
11 import time
12 import socket
13 
14 from celery.canvas import group
15 
16 from api.ssh import exec_ssh, connect_ssh
17 from cm.chef_api import ChefAPI
18 
19 
20 CHEF_CONFIG_PATH = '/etc/chef'
21 CHEF_INSTALL_TYPE = 'gems'
22 CHEF_RUBY_VERSION = '1.9.1'
23 CHEF_ENVIRONMENT = '_default'
24 CHEF_CLIENT_VERSION = '11.6.2'
25 
26 # load chef config using CHEF_CONFIG_PATH
27 try:
28     # parse controller's chef config for server_url and client_name
29     _client_cfg_path = os.path.join(CHEF_CONFIG_PATH, 'client.rb')
30     if not os.path.exists(_client_cfg_path):
31         raise EnvironmentError('Could not find {}'.format(_client_cfg_path))
32     with open(_client_cfg_path) as f:
33         _data = f.read()
34     # construct a dict from the ruby client.rb
35     _d = {}
36     for m in re.findall(r'''^([a-zA-Z0-9_]+)[ \t]+(.*)$''',
37                         _data, re.MULTILINE):
38         _d[m[0]] = m[1].strip("'").strip('"')
39     # set global variables from client.rb
40     CHEF_SERVER_URL = _d['chef_server_url']
41     CHEF_NODE_NAME = _d.get('node_name', socket.gethostname())
42     CHEF_CLIENT_NAME = _d.get('node_name', socket.gethostname())
43     CHEF_VALIDATION_NAME = _d['validation_client_name']
44     # read the client key
45     _client_pem_path = os.path.join(CHEF_CONFIG_PATH, 'client.pem')
46     CHEF_CLIENT_KEY = subprocess.check_output(
47         ['sudo', '/bin/cat', _client_pem_path]).strip('\n')
48     # read the validation key
49     _valid_pem_path = os.path.join(CHEF_CONFIG_PATH, 'validation.pem')
50     CHEF_VALIDATION_KEY = subprocess.check_output(
51         ['sudo', '/bin/cat', _valid_pem_path]).strip('\n')
52 except Exception as err:
53     msg = "Failed to auto-configure Chef -- {}".format(err)
54     if os.environ.get('READTHEDOCS'):
55         # Just print the error if Sphinx is running
56         print(msg)
57     else:
58         raise EnvironmentError(msg)
59 
60 
61 def _get_client():
62     """
63     Return a new instance of a Chef API Client
64 
65     :rtype: a :class:`~cm.chef_api.ChefAPI` object
66     """
67     return ChefAPI(CHEF_SERVER_URL, CHEF_CLIENT_NAME, CHEF_CLIENT_KEY)
68 
69 
70 def bootstrap_node(node):
71     """
72     Bootstrap the Chef configuration management tools onto a node.
73 
74     :param node: a dict containing the node's fully-qualified domain name and SSH info
75     :raises: RuntimeError
76     """
77     # block until we can connect over ssh
78     ssh = connect_ssh(node['ssh_username'], node['fqdn'], node.get('ssh_port', 22),
79                       node['ssh_private_key'], timeout=120)
80     # block until ubuntu cloud-init is finished
81     initializing = True
82     while initializing:
83         time.sleep(10)
84         initializing, _rc = exec_ssh(ssh, 'ps auxw | egrep "cloud-init" | grep -v egrep')
85     # write out private key and prepare to `knife bootstrap`
86     try:
87         _, pk_path = tempfile.mkstemp()
88         _, output_path = tempfile.mkstemp()
89         with open(pk_path, 'w') as f:
90             f.write(node['ssh_private_key'])
91         # build knife bootstrap command
92         args = ['knife', 'bootstrap', node['fqdn']]
93         args.extend(['--identity-file', pk_path])
94         args.extend(['--node-name', node['id']])
95         args.extend(['--sudo', '--ssh-user', node['ssh_username']])
96         args.extend(['--ssh-port', str(node.get('ssh_port', 22))])
97         args.extend(['--bootstrap-version', CHEF_CLIENT_VERSION])
98         args.extend(['--no-host-key-verify'])
99         args.extend(['--run-list', _construct_run_list(node)])
100         print(' '.join(args))
101         # tee the command's output to a tempfile
102         args.extend(['|', 'tee', output_path])
103         # TODO: figure out why home isn't being set correctly for knife exec
104         env = os.environ.copy()
105         env['HOME'] = '/opt/deis'
106         # execute knife bootstrap
107         p = subprocess.Popen(' '.join(args), env=env, shell=True)
108         rc = p.wait()
109         # always print knife output
110         with open(output_path) as f:
111             output = f.read()
112         print(output)
113         # raise an exception if bootstrap failed
114         if rc != 0:
115             raise RuntimeError('Node Bootstrap Error')
116     # remove temp files from filesystem
117     finally:
118         os.remove(pk_path)
119         os.remove(output_path)
120 
121 
122 def _construct_run_list(node):
123     config = node['config']
124     # if run_list override specified, use it (assumes csv)
125     run_list = config.get('run_list', [])
126     # otherwise construct a run_list using proxy/runtime flags
127     if not run_list:
128         run_list = ['recipe[deis]']
129         if node.get('runtime') is True:
130             run_list.append('recipe[deis::runtime]')
131         if node.get('proxy') is True:
132             run_list.append('recipe[deis::proxy]')
133     return ','.join(run_list)
134 
135 
136 def purge_node(node):
137     """
138     Purge a node and its client from Chef configuration management.
139 
140     :param node: a dict containing the id of a node to purge
141     """
142     client = _get_client()
143     node_id = node['id']
144     body, status = client.delete_node(node_id)
145     if status != 200:
146         raise RuntimeError('Could not purge node {node_id}: {body}'.format(**locals()))
147     body, status = client.delete_client(node_id)
148     if status != 200:
149         raise RuntimeError('Could not purge node client {node_id}: {body}'.format(**locals()))
150 
151 
152 def converge_controller():
153     """
154     Converge this controller node.
155 
156     "Converge" means to change a node's configuration to match that defined by
157     configuration management.
158 
159     :returns: the output of the convergence command, in this case `sudo chef-client`
160     """
161     try:
162         return subprocess.check_output(['sudo', 'chef-client'])
163     except subprocess.CalledProcessError as err:
164         print(err)
165         print(err.output)
166         raise err
167 
168 
169 def converge_node(node):
170     """
171     Converge a node.
172 
173     "Converge" means to change a node's configuration to match that defined by
174     configuration management.
175 
176     :param node: a dict containing the node's fully-qualified domain name and SSH info
177     :returns: a tuple of the convergence command's (output, return_code)
178     """
179     ssh = connect_ssh(node['ssh_username'],
180                       node['fqdn'], 22,
181                       node['ssh_private_key'])
182     output, rc = exec_ssh(ssh, 'sudo chef-client')
183     print(output)
184     if rc != 0:
185         e = RuntimeError('Node converge error')
186         e.output = output
187         raise e
188     return output, rc
189 
190 
191 def run_node(node, command):
192     """
193     Run a command on a node.
194 
195     :param node: a dict containing the node's fully-qualified domain name and SSH info
196     :param command: the command-line to execute on the node
197     :returns: a tuple of the command's (output, return_code)
198     """
199     ssh = connect_ssh(node['ssh_username'], node['fqdn'],
200                       node['ssh_port'], node['ssh_private_key'])
201     output, rc = exec_ssh(ssh, command, pty=True)
202     return output, rc
203 
204 
205 def converge_formation(formation):
206     """
207     Converge all nodes in a formation.
208 
209     "Converge" means to change a node's configuration to match that defined by
210     configuration management.
211 
212     :param formation: a :class:`~api.models.Formation` to converge
213     :returns: the combined output of the nodes' convergence commands
214     """
215     nodes = formation.node_set.all()
216     subtasks = []
217     for n in nodes:
218         subtask = converge_node.s(n.id,
219                                   n.layer.flavor.ssh_username,
220                                   n.fqdn,
221                                   n.layer.flavor.ssh_private_key)
222         subtasks.append(subtask)
223     job = group(*subtasks)
224     return job.apply_async().join()
225 
226 
227 def publish_user(user, data):
228     """
229     Publish a user to configuration management.
230 
231     :param user: a dict containing the username
232     :param data: data to store with the user
233     :returns: a tuple of (body, status) from the underlying HTTP response
234     :raises: RuntimeError
235     """
236     _publish('deis-users', user['username'], data)
237 
238 
239 def publish_app(app, data):
240     """
241     Publish an app to configuration management.
242 
243     :param app: a dict containing the id of the app
244     :param data: data to store with the app
245     :returns: a tuple of (body, status) from the underlying HTTP response
246     :raises: RuntimeError
247     """
248     _publish('deis-apps', app['id'], data)
249 
250 
251 def purge_app(app):
252     """
253     Purge an app from configuration management.
254 
255     :param app: a dict containing the id of the app
256     :returns: a tuple of (body, status) from the underlying HTTP response
257     :raises: RuntimeError
258     """
259     _purge('deis-apps', app['id'])
260 
261 
262 def publish_formation(formation, data):
263     """
264     Publish a formation to configuration management.
265 
266     :param formation: a dict containing the id of the formation
267     :param data: data to store with the formation
268     :returns: a tuple of (body, status) from the underlying HTTP response
269     :raises: RuntimeError
270     """
271     _publish('deis-formations', formation['id'], data)
272 
273 
274 def purge_formation(formation):
275     """
276     Purge a formation from configuration management.
277 
278     :param formation: a dict containing the id of the formation
279     :returns: a tuple of (body, status) from the underlying HTTP response
280     :raises: RuntimeError
281     """
282     _purge('deis-formations', formation['id'])
283 
284 
285 def _publish(data_bag, item_name, item_value):
286     """
287     Publish a data bag item to the Chef server.
288 
289     :param data_bag: the name of a Chef data bag
290     :param item_name: the name of the item to publish
291     :param item_value: the value of the item to publish
292     :returns: a tuple of (body, status) from the underlying HTTP response
293     :raises: RuntimeError
294     """
295     client = _get_client()
296     body, status = client.update_databag_item(data_bag, item_name, item_value)
297     if status != 200:
298         body, status = client.create_databag_item(data_bag, item_name, item_value)
299         if status != 201:
300             raise RuntimeError('Could not publish {item_name}: {body}'.format(**locals()))
301     return body, status
302 
303 
304 def _purge(databag_name, item_name):
305     """
306     Purge a data bag item from the Chef server.
307 
308     :param databag_name: the name of a Chef data bag
309     :param item_name: the name of the item to purge
310     :returns: a tuple of (body, status) from the underlying HTTP response
311     :raises: RuntimeError
312     """
313     client = _get_client()
314     body, status = client.delete_databag_item(databag_name, item_name)
315     if status == 200 or status == 404:
316         return body, status
317     raise RuntimeError('Could not purge {item_name}: {body}'.format(**locals()))
318
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/cm/chef.py b/cm/chef.py
--- a/cm/chef.py
+++ b/cm/chef.py
@@ -142,11 +142,11 @@
     client = _get_client()
     node_id = node['id']
     body, status = client.delete_node(node_id)
-    if status != 200:
-        raise RuntimeError('Could not purge node {node_id}: {body}'.format(**locals()))
+    if status not in [200, 404]:
+        raise RuntimeError("Could not purge node {node_id}: {body}".format(**locals()))
     body, status = client.delete_client(node_id)
-    if status != 200:
-        raise RuntimeError('Could not purge node client {node_id}: {body}'.format(**locals()))
+    if status not in [200, 404]:
+        raise RuntimeError("Could not purge node client {node_id}: {body}".format(**locals()))
 
 
 def converge_controller():
@@ -312,6 +312,6 @@
     """
     client = _get_client()
     body, status = client.delete_databag_item(databag_name, item_name)
-    if status == 200 or status == 404:
+    if status in [200, 404]:
         return body, status
     raise RuntimeError('Could not purge {item_name}: {body}'.format(**locals()))

{"golden_diff": "diff --git a/cm/chef.py b/cm/chef.py\n--- a/cm/chef.py\n+++ b/cm/chef.py\n@@ -142,11 +142,11 @@\n     client = _get_client()\n     node_id = node['id']\n     body, status = client.delete_node(node_id)\n-    if status != 200:\n-        raise RuntimeError('Could not purge node {node_id}: {body}'.format(**locals()))\n+    if status not in [200, 404]:\n+        raise RuntimeError(\"Could not purge node {node_id}: {body}\".format(**locals()))\n     body, status = client.delete_client(node_id)\n-    if status != 200:\n-        raise RuntimeError('Could not purge node client {node_id}: {body}'.format(**locals()))\n+    if status not in [200, 404]:\n+        raise RuntimeError(\"Could not purge node client {node_id}: {body}\".format(**locals()))\n \n \n def converge_controller():\n@@ -312,6 +312,6 @@\n     \"\"\"\n     client = _get_client()\n     body, status = client.delete_databag_item(databag_name, item_name)\n-    if status == 200 or status == 404:\n+    if status in [200, 404]:\n         return body, status\n     raise RuntimeError('Could not purge {item_name}: {body}'.format(**locals()))\n", "issue": "404 on cm.destroy should be a warning.\nIt's all too easy to run into the case where something errored in formations:create and we have a Django database entry for the formation, but no Chef node. The recent merge makes it impossible to delete the Formation in that case. If we instead treat the Chef 404 as a warning, this will work.\n\n", "before_files": [{"content": "\"\"\"\nDeis configuration management implementation for Opscode Chef.\n\"\"\"\n\nfrom __future__ import unicode_literals\n\nimport os\nimport re\nimport subprocess\nimport tempfile\nimport time\nimport socket\n\nfrom celery.canvas import group\n\nfrom api.ssh import exec_ssh, connect_ssh\nfrom cm.chef_api import ChefAPI\n\n\nCHEF_CONFIG_PATH = '/etc/chef'\nCHEF_INSTALL_TYPE = 'gems'\nCHEF_RUBY_VERSION = '1.9.1'\nCHEF_ENVIRONMENT = '_default'\nCHEF_CLIENT_VERSION = '11.6.2'\n\n# load chef config using CHEF_CONFIG_PATH\ntry:\n    # parse controller's chef config for server_url and client_name\n    _client_cfg_path = os.path.join(CHEF_CONFIG_PATH, 'client.rb')\n    if not os.path.exists(_client_cfg_path):\n        raise EnvironmentError('Could not find {}'.format(_client_cfg_path))\n    with open(_client_cfg_path) as f:\n        _data = f.read()\n    # construct a dict from the ruby client.rb\n    _d = {}\n    for m in re.findall(r'''^([a-zA-Z0-9_]+)[ \\t]+(.*)$''',\n                        _data, re.MULTILINE):\n        _d[m[0]] = m[1].strip(\"'\").strip('\"')\n    # set global variables from client.rb\n    CHEF_SERVER_URL = _d['chef_server_url']\n    CHEF_NODE_NAME = _d.get('node_name', socket.gethostname())\n    CHEF_CLIENT_NAME = _d.get('node_name', socket.gethostname())\n    CHEF_VALIDATION_NAME = _d['validation_client_name']\n    # read the client key\n    _client_pem_path = os.path.join(CHEF_CONFIG_PATH, 'client.pem')\n    CHEF_CLIENT_KEY = subprocess.check_output(\n        ['sudo', '/bin/cat', _client_pem_path]).strip('\\n')\n    # read the validation key\n    _valid_pem_path = os.path.join(CHEF_CONFIG_PATH, 'validation.pem')\n    CHEF_VALIDATION_KEY = subprocess.check_output(\n        ['sudo', '/bin/cat', _valid_pem_path]).strip('\\n')\nexcept Exception as err:\n    msg = \"Failed to auto-configure Chef -- {}\".format(err)\n    if os.environ.get('READTHEDOCS'):\n        # Just print the error if Sphinx is running\n        print(msg)\n    else:\n        raise EnvironmentError(msg)\n\n\ndef _get_client():\n    \"\"\"\n    Return a new instance of a Chef API Client\n\n    :rtype: a :class:`~cm.chef_api.ChefAPI` object\n    \"\"\"\n    return ChefAPI(CHEF_SERVER_URL, CHEF_CLIENT_NAME, CHEF_CLIENT_KEY)\n\n\ndef bootstrap_node(node):\n    \"\"\"\n    Bootstrap the Chef configuration management tools onto a node.\n\n    :param node: a dict containing the node's fully-qualified domain name and SSH info\n    :raises: RuntimeError\n    \"\"\"\n    # block until we can connect over ssh\n    ssh = connect_ssh(node['ssh_username'], node['fqdn'], node.get('ssh_port', 22),\n                      node['ssh_private_key'], timeout=120)\n    # block until ubuntu cloud-init is finished\n    initializing = True\n    while initializing:\n        time.sleep(10)\n        initializing, _rc = exec_ssh(ssh, 'ps auxw | egrep \"cloud-init\" | grep -v egrep')\n    # write out private key and prepare to `knife bootstrap`\n    try:\n        _, pk_path = tempfile.mkstemp()\n        _, output_path = tempfile.mkstemp()\n        with open(pk_path, 'w') as f:\n            f.write(node['ssh_private_key'])\n        # build knife bootstrap command\n        args = ['knife', 'bootstrap', node['fqdn']]\n        args.extend(['--identity-file', pk_path])\n        args.extend(['--node-name', node['id']])\n        args.extend(['--sudo', '--ssh-user', node['ssh_username']])\n        args.extend(['--ssh-port', str(node.get('ssh_port', 22))])\n        args.extend(['--bootstrap-version', CHEF_CLIENT_VERSION])\n        args.extend(['--no-host-key-verify'])\n        args.extend(['--run-list', _construct_run_list(node)])\n        print(' '.join(args))\n        # tee the command's output to a tempfile\n        args.extend(['|', 'tee', output_path])\n        # TODO: figure out why home isn't being set correctly for knife exec\n        env = os.environ.copy()\n        env['HOME'] = '/opt/deis'\n        # execute knife bootstrap\n        p = subprocess.Popen(' '.join(args), env=env, shell=True)\n        rc = p.wait()\n        # always print knife output\n        with open(output_path) as f:\n            output = f.read()\n        print(output)\n        # raise an exception if bootstrap failed\n        if rc != 0:\n            raise RuntimeError('Node Bootstrap Error')\n    # remove temp files from filesystem\n    finally:\n        os.remove(pk_path)\n        os.remove(output_path)\n\n\ndef _construct_run_list(node):\n    config = node['config']\n    # if run_list override specified, use it (assumes csv)\n    run_list = config.get('run_list', [])\n    # otherwise construct a run_list using proxy/runtime flags\n    if not run_list:\n        run_list = ['recipe[deis]']\n        if node.get('runtime') is True:\n            run_list.append('recipe[deis::runtime]')\n        if node.get('proxy') is True:\n            run_list.append('recipe[deis::proxy]')\n    return ','.join(run_list)\n\n\ndef purge_node(node):\n    \"\"\"\n    Purge a node and its client from Chef configuration management.\n\n    :param node: a dict containing the id of a node to purge\n    \"\"\"\n    client = _get_client()\n    node_id = node['id']\n    body, status = client.delete_node(node_id)\n    if status != 200:\n        raise RuntimeError('Could not purge node {node_id}: {body}'.format(**locals()))\n    body, status = client.delete_client(node_id)\n    if status != 200:\n        raise RuntimeError('Could not purge node client {node_id}: {body}'.format(**locals()))\n\n\ndef converge_controller():\n    \"\"\"\n    Converge this controller node.\n\n    \"Converge\" means to change a node's configuration to match that defined by\n    configuration management.\n\n    :returns: the output of the convergence command, in this case `sudo chef-client`\n    \"\"\"\n    try:\n        return subprocess.check_output(['sudo', 'chef-client'])\n    except subprocess.CalledProcessError as err:\n        print(err)\n        print(err.output)\n        raise err\n\n\ndef converge_node(node):\n    \"\"\"\n    Converge a node.\n\n    \"Converge\" means to change a node's configuration to match that defined by\n    configuration management.\n\n    :param node: a dict containing the node's fully-qualified domain name and SSH info\n    :returns: a tuple of the convergence command's (output, return_code)\n    \"\"\"\n    ssh = connect_ssh(node['ssh_username'],\n                      node['fqdn'], 22,\n                      node['ssh_private_key'])\n    output, rc = exec_ssh(ssh, 'sudo chef-client')\n    print(output)\n    if rc != 0:\n        e = RuntimeError('Node converge error')\n        e.output = output\n        raise e\n    return output, rc\n\n\ndef run_node(node, command):\n    \"\"\"\n    Run a command on a node.\n\n    :param node: a dict containing the node's fully-qualified domain name and SSH info\n    :param command: the command-line to execute on the node\n    :returns: a tuple of the command's (output, return_code)\n    \"\"\"\n    ssh = connect_ssh(node['ssh_username'], node['fqdn'],\n                      node['ssh_port'], node['ssh_private_key'])\n    output, rc = exec_ssh(ssh, command, pty=True)\n    return output, rc\n\n\ndef converge_formation(formation):\n    \"\"\"\n    Converge all nodes in a formation.\n\n    \"Converge\" means to change a node's configuration to match that defined by\n    configuration management.\n\n    :param formation: a :class:`~api.models.Formation` to converge\n    :returns: the combined output of the nodes' convergence commands\n    \"\"\"\n    nodes = formation.node_set.all()\n    subtasks = []\n    for n in nodes:\n        subtask = converge_node.s(n.id,\n                                  n.layer.flavor.ssh_username,\n                                  n.fqdn,\n                                  n.layer.flavor.ssh_private_key)\n        subtasks.append(subtask)\n    job = group(*subtasks)\n    return job.apply_async().join()\n\n\ndef publish_user(user, data):\n    \"\"\"\n    Publish a user to configuration management.\n\n    :param user: a dict containing the username\n    :param data: data to store with the user\n    :returns: a tuple of (body, status) from the underlying HTTP response\n    :raises: RuntimeError\n    \"\"\"\n    _publish('deis-users', user['username'], data)\n\n\ndef publish_app(app, data):\n    \"\"\"\n    Publish an app to configuration management.\n\n    :param app: a dict containing the id of the app\n    :param data: data to store with the app\n    :returns: a tuple of (body, status) from the underlying HTTP response\n    :raises: RuntimeError\n    \"\"\"\n    _publish('deis-apps', app['id'], data)\n\n\ndef purge_app(app):\n    \"\"\"\n    Purge an app from configuration management.\n\n    :param app: a dict containing the id of the app\n    :returns: a tuple of (body, status) from the underlying HTTP response\n    :raises: RuntimeError\n    \"\"\"\n    _purge('deis-apps', app['id'])\n\n\ndef publish_formation(formation, data):\n    \"\"\"\n    Publish a formation to configuration management.\n\n    :param formation: a dict containing the id of the formation\n    :param data: data to store with the formation\n    :returns: a tuple of (body, status) from the underlying HTTP response\n    :raises: RuntimeError\n    \"\"\"\n    _publish('deis-formations', formation['id'], data)\n\n\ndef purge_formation(formation):\n    \"\"\"\n    Purge a formation from configuration management.\n\n    :param formation: a dict containing the id of the formation\n    :returns: a tuple of (body, status) from the underlying HTTP response\n    :raises: RuntimeError\n    \"\"\"\n    _purge('deis-formations', formation['id'])\n\n\ndef _publish(data_bag, item_name, item_value):\n    \"\"\"\n    Publish a data bag item to the Chef server.\n\n    :param data_bag: the name of a Chef data bag\n    :param item_name: the name of the item to publish\n    :param item_value: the value of the item to publish\n    :returns: a tuple of (body, status) from the underlying HTTP response\n    :raises: RuntimeError\n    \"\"\"\n    client = _get_client()\n    body, status = client.update_databag_item(data_bag, item_name, item_value)\n    if status != 200:\n        body, status = client.create_databag_item(data_bag, item_name, item_value)\n        if status != 201:\n            raise RuntimeError('Could not publish {item_name}: {body}'.format(**locals()))\n    return body, status\n\n\ndef _purge(databag_name, item_name):\n    \"\"\"\n    Purge a data bag item from the Chef server.\n\n    :param databag_name: the name of a Chef data bag\n    :param item_name: the name of the item to purge\n    :returns: a tuple of (body, status) from the underlying HTTP response\n    :raises: RuntimeError\n    \"\"\"\n    client = _get_client()\n    body, status = client.delete_databag_item(databag_name, item_name)\n    if status == 200 or status == 404:\n        return body, status\n    raise RuntimeError('Could not purge {item_name}: {body}'.format(**locals()))\n", "path": "cm/chef.py"}], "after_files": [{"content": "\"\"\"\nDeis configuration management implementation for Opscode Chef.\n\"\"\"\n\nfrom __future__ import unicode_literals\n\nimport os\nimport re\nimport subprocess\nimport tempfile\nimport time\nimport socket\n\nfrom celery.canvas import group\n\nfrom api.ssh import exec_ssh, connect_ssh\nfrom cm.chef_api import ChefAPI\n\n\nCHEF_CONFIG_PATH = '/etc/chef'\nCHEF_INSTALL_TYPE = 'gems'\nCHEF_RUBY_VERSION = '1.9.1'\nCHEF_ENVIRONMENT = '_default'\nCHEF_CLIENT_VERSION = '11.6.2'\n\n# load chef config using CHEF_CONFIG_PATH\ntry:\n    # parse controller's chef config for server_url and client_name\n    _client_cfg_path = os.path.join(CHEF_CONFIG_PATH, 'client.rb')\n    if not os.path.exists(_client_cfg_path):\n        raise EnvironmentError('Could not find {}'.format(_client_cfg_path))\n    with open(_client_cfg_path) as f:\n        _data = f.read()\n    # construct a dict from the ruby client.rb\n    _d = {}\n    for m in re.findall(r'''^([a-zA-Z0-9_]+)[ \\t]+(.*)$''',\n                        _data, re.MULTILINE):\n        _d[m[0]] = m[1].strip(\"'\").strip('\"')\n    # set global variables from client.rb\n    CHEF_SERVER_URL = _d['chef_server_url']\n    CHEF_NODE_NAME = _d.get('node_name', socket.gethostname())\n    CHEF_CLIENT_NAME = _d.get('node_name', socket.gethostname())\n    CHEF_VALIDATION_NAME = _d['validation_client_name']\n    # read the client key\n    _client_pem_path = os.path.join(CHEF_CONFIG_PATH, 'client.pem')\n    CHEF_CLIENT_KEY = subprocess.check_output(\n        ['sudo', '/bin/cat', _client_pem_path]).strip('\\n')\n    # read the validation key\n    _valid_pem_path = os.path.join(CHEF_CONFIG_PATH, 'validation.pem')\n    CHEF_VALIDATION_KEY = subprocess.check_output(\n        ['sudo', '/bin/cat', _valid_pem_path]).strip('\\n')\nexcept Exception as err:\n    msg = \"Failed to auto-configure Chef -- {}\".format(err)\n    if os.environ.get('READTHEDOCS'):\n        # Just print the error if Sphinx is running\n        print(msg)\n    else:\n        raise EnvironmentError(msg)\n\n\ndef _get_client():\n    \"\"\"\n    Return a new instance of a Chef API Client\n\n    :rtype: a :class:`~cm.chef_api.ChefAPI` object\n    \"\"\"\n    return ChefAPI(CHEF_SERVER_URL, CHEF_CLIENT_NAME, CHEF_CLIENT_KEY)\n\n\ndef bootstrap_node(node):\n    \"\"\"\n    Bootstrap the Chef configuration management tools onto a node.\n\n    :param node: a dict containing the node's fully-qualified domain name and SSH info\n    :raises: RuntimeError\n    \"\"\"\n    # block until we can connect over ssh\n    ssh = connect_ssh(node['ssh_username'], node['fqdn'], node.get('ssh_port', 22),\n                      node['ssh_private_key'], timeout=120)\n    # block until ubuntu cloud-init is finished\n    initializing = True\n    while initializing:\n        time.sleep(10)\n        initializing, _rc = exec_ssh(ssh, 'ps auxw | egrep \"cloud-init\" | grep -v egrep')\n    # write out private key and prepare to `knife bootstrap`\n    try:\n        _, pk_path = tempfile.mkstemp()\n        _, output_path = tempfile.mkstemp()\n        with open(pk_path, 'w') as f:\n            f.write(node['ssh_private_key'])\n        # build knife bootstrap command\n        args = ['knife', 'bootstrap', node['fqdn']]\n        args.extend(['--identity-file', pk_path])\n        args.extend(['--node-name', node['id']])\n        args.extend(['--sudo', '--ssh-user', node['ssh_username']])\n        args.extend(['--ssh-port', str(node.get('ssh_port', 22))])\n        args.extend(['--bootstrap-version', CHEF_CLIENT_VERSION])\n        args.extend(['--no-host-key-verify'])\n        args.extend(['--run-list', _construct_run_list(node)])\n        print(' '.join(args))\n        # tee the command's output to a tempfile\n        args.extend(['|', 'tee', output_path])\n        # TODO: figure out why home isn't being set correctly for knife exec\n        env = os.environ.copy()\n        env['HOME'] = '/opt/deis'\n        # execute knife bootstrap\n        p = subprocess.Popen(' '.join(args), env=env, shell=True)\n        rc = p.wait()\n        # always print knife output\n        with open(output_path) as f:\n            output = f.read()\n        print(output)\n        # raise an exception if bootstrap failed\n        if rc != 0:\n            raise RuntimeError('Node Bootstrap Error')\n    # remove temp files from filesystem\n    finally:\n        os.remove(pk_path)\n        os.remove(output_path)\n\n\ndef _construct_run_list(node):\n    config = node['config']\n    # if run_list override specified, use it (assumes csv)\n    run_list = config.get('run_list', [])\n    # otherwise construct a run_list using proxy/runtime flags\n    if not run_list:\n        run_list = ['recipe[deis]']\n        if node.get('runtime') is True:\n            run_list.append('recipe[deis::runtime]')\n        if node.get('proxy') is True:\n            run_list.append('recipe[deis::proxy]')\n    return ','.join(run_list)\n\n\ndef purge_node(node):\n    \"\"\"\n    Purge a node and its client from Chef configuration management.\n\n    :param node: a dict containing the id of a node to purge\n    \"\"\"\n    client = _get_client()\n    node_id = node['id']\n    body, status = client.delete_node(node_id)\n    if status not in [200, 404]:\n        raise RuntimeError(\"Could not purge node {node_id}: {body}\".format(**locals()))\n    body, status = client.delete_client(node_id)\n    if status not in [200, 404]:\n        raise RuntimeError(\"Could not purge node client {node_id}: {body}\".format(**locals()))\n\n\ndef converge_controller():\n    \"\"\"\n    Converge this controller node.\n\n    \"Converge\" means to change a node's configuration to match that defined by\n    configuration management.\n\n    :returns: the output of the convergence command, in this case `sudo chef-client`\n    \"\"\"\n    try:\n        return subprocess.check_output(['sudo', 'chef-client'])\n    except subprocess.CalledProcessError as err:\n        print(err)\n        print(err.output)\n        raise err\n\n\ndef converge_node(node):\n    \"\"\"\n    Converge a node.\n\n    \"Converge\" means to change a node's configuration to match that defined by\n    configuration management.\n\n    :param node: a dict containing the node's fully-qualified domain name and SSH info\n    :returns: a tuple of the convergence command's (output, return_code)\n    \"\"\"\n    ssh = connect_ssh(node['ssh_username'],\n                      node['fqdn'], 22,\n                      node['ssh_private_key'])\n    output, rc = exec_ssh(ssh, 'sudo chef-client')\n    print(output)\n    if rc != 0:\n        e = RuntimeError('Node converge error')\n        e.output = output\n        raise e\n    return output, rc\n\n\ndef run_node(node, command):\n    \"\"\"\n    Run a command on a node.\n\n    :param node: a dict containing the node's fully-qualified domain name and SSH info\n    :param command: the command-line to execute on the node\n    :returns: a tuple of the command's (output, return_code)\n    \"\"\"\n    ssh = connect_ssh(node['ssh_username'], node['fqdn'],\n                      node['ssh_port'], node['ssh_private_key'])\n    output, rc = exec_ssh(ssh, command, pty=True)\n    return output, rc\n\n\ndef converge_formation(formation):\n    \"\"\"\n    Converge all nodes in a formation.\n\n    \"Converge\" means to change a node's configuration to match that defined by\n    configuration management.\n\n    :param formation: a :class:`~api.models.Formation` to converge\n    :returns: the combined output of the nodes' convergence commands\n    \"\"\"\n    nodes = formation.node_set.all()\n    subtasks = []\n    for n in nodes:\n        subtask = converge_node.s(n.id,\n                                  n.layer.flavor.ssh_username,\n                                  n.fqdn,\n                                  n.layer.flavor.ssh_private_key)\n        subtasks.append(subtask)\n    job = group(*subtasks)\n    return job.apply_async().join()\n\n\ndef publish_user(user, data):\n    \"\"\"\n    Publish a user to configuration management.\n\n    :param user: a dict containing the username\n    :param data: data to store with the user\n    :returns: a tuple of (body, status) from the underlying HTTP response\n    :raises: RuntimeError\n    \"\"\"\n    _publish('deis-users', user['username'], data)\n\n\ndef publish_app(app, data):\n    \"\"\"\n    Publish an app to configuration management.\n\n    :param app: a dict containing the id of the app\n    :param data: data to store with the app\n    :returns: a tuple of (body, status) from the underlying HTTP response\n    :raises: RuntimeError\n    \"\"\"\n    _publish('deis-apps', app['id'], data)\n\n\ndef purge_app(app):\n    \"\"\"\n    Purge an app from configuration management.\n\n    :param app: a dict containing the id of the app\n    :returns: a tuple of (body, status) from the underlying HTTP response\n    :raises: RuntimeError\n    \"\"\"\n    _purge('deis-apps', app['id'])\n\n\ndef publish_formation(formation, data):\n    \"\"\"\n    Publish a formation to configuration management.\n\n    :param formation: a dict containing the id of the formation\n    :param data: data to store with the formation\n    :returns: a tuple of (body, status) from the underlying HTTP response\n    :raises: RuntimeError\n    \"\"\"\n    _publish('deis-formations', formation['id'], data)\n\n\ndef purge_formation(formation):\n    \"\"\"\n    Purge a formation from configuration management.\n\n    :param formation: a dict containing the id of the formation\n    :returns: a tuple of (body, status) from the underlying HTTP response\n    :raises: RuntimeError\n    \"\"\"\n    _purge('deis-formations', formation['id'])\n\n\ndef _publish(data_bag, item_name, item_value):\n    \"\"\"\n    Publish a data bag item to the Chef server.\n\n    :param data_bag: the name of a Chef data bag\n    :param item_name: the name of the item to publish\n    :param item_value: the value of the item to publish\n    :returns: a tuple of (body, status) from the underlying HTTP response\n    :raises: RuntimeError\n    \"\"\"\n    client = _get_client()\n    body, status = client.update_databag_item(data_bag, item_name, item_value)\n    if status != 200:\n        body, status = client.create_databag_item(data_bag, item_name, item_value)\n        if status != 201:\n            raise RuntimeError('Could not publish {item_name}: {body}'.format(**locals()))\n    return body, status\n\n\ndef _purge(databag_name, item_name):\n    \"\"\"\n    Purge a data bag item from the Chef server.\n\n    :param databag_name: the name of a Chef data bag\n    :param item_name: the name of the item to purge\n    :returns: a tuple of (body, status) from the underlying HTTP response\n    :raises: RuntimeError\n    \"\"\"\n    client = _get_client()\n    body, status = client.delete_databag_item(databag_name, item_name)\n    if status in [200, 404]:\n        return body, status\n    raise RuntimeError('Could not purge {item_name}: {body}'.format(**locals()))\n", "path": "cm/chef.py"}]}