Merge branch 'stable-2.16' into stable-2.17
[ganeti-github.git] / lib / tools / common.py
1 #
2 #
3
4 # Copyright (C) 2014 Google Inc.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are
9 # met:
10 #
11 # 1. Redistributions of source code must retain the above copyright notice,
12 # this list of conditions and the following disclaimer.
13 #
14 # 2. Redistributions in binary form must reproduce the above copyright
15 # notice, this list of conditions and the following disclaimer in the
16 # documentation and/or other materials provided with the distribution.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
19 # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20 # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30 """Common functions for tool scripts.
31
32 """
33
34 import logging
35 import os
36 import time
37
38 from cStringIO import StringIO
39
40 import OpenSSL
41
42 from ganeti import constants
43 from ganeti import errors
44 from ganeti import pathutils
45 from ganeti import utils
46 from ganeti import serializer
47 from ganeti import ssconf
48 from ganeti import ssh
49
50
51 def VerifyOptions(parser, opts, args):
52 """Verifies options and arguments for correctness.
53
54 """
55 if args:
56 parser.error("No arguments are expected")
57
58 return opts
59
60
61 def _VerifyCertificateStrong(cert_pem, error_fn,
62 _check_fn=utils.CheckNodeCertificate):
63 """Verifies a certificate against the local node daemon certificate.
64
65 Includes elaborate tests of encodings etc., and returns formatted
66 certificate.
67
68 @type cert_pem: string
69 @param cert_pem: Certificate and key in PEM format
70 @type error_fn: callable
71 @param error_fn: function to call in case of an error
72 @rtype: string
73 @return: Formatted key and certificate
74
75 """
76 try:
77 cert = \
78 OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
79 except Exception, err:
80 raise error_fn("(stdin) Unable to load certificate: %s" % err)
81
82 try:
83 key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
84 except OpenSSL.crypto.Error, err:
85 raise error_fn("(stdin) Unable to load private key: %s" % err)
86
87 # Check certificate with given key; this detects cases where the key given on
88 # stdin doesn't match the certificate also given on stdin
89 try:
90 utils.X509CertKeyCheck(cert, key)
91 except OpenSSL.SSL.Error:
92 raise error_fn("(stdin) Certificate is not signed with given key")
93
94 # Standard checks, including check against an existing local certificate
95 # (no-op if that doesn't exist)
96 _check_fn(cert)
97
98 key_encoded = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)
99 cert_encoded = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM,
100 cert)
101 complete_cert_encoded = key_encoded + cert_encoded
102 if not cert_pem == complete_cert_encoded:
103 logging.error("The certificate differs after being reencoded. Please"
104 " renew the certificates cluster-wide to prevent future"
105 " inconsistencies.")
106
107 # Format for storing on disk
108 buf = StringIO()
109 buf.write(cert_pem)
110 return buf.getvalue()
111
112
113 def _VerifyCertificateSoft(cert_pem, error_fn,
114 _check_fn=utils.CheckNodeCertificate):
115 """Verifies a certificate against the local node daemon certificate.
116
117 @type cert_pem: string
118 @param cert_pem: Certificate in PEM format (no key)
119
120 """
121 try:
122 OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
123 except OpenSSL.crypto.Error, err:
124 pass
125 else:
126 raise error_fn("No private key may be given")
127
128 try:
129 cert = \
130 OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
131 except Exception, err:
132 raise errors.X509CertError("(stdin)",
133 "Unable to load certificate: %s" % err)
134
135 _check_fn(cert)
136
137
138 def VerifyCertificateSoft(data, error_fn, _verify_fn=_VerifyCertificateSoft):
139 """Verifies cluster certificate if existing.
140
141 @type data: dict
142 @type error_fn: callable
143 @param error_fn: function to call in case of an error
144 @rtype: string
145 @return: Formatted key and certificate
146
147 """
148 cert = data.get(constants.SSHS_NODE_DAEMON_CERTIFICATE)
149 if cert:
150 _verify_fn(cert, error_fn)
151
152
153 def VerifyCertificateStrong(data, error_fn,
154 _verify_fn=_VerifyCertificateStrong):
155 """Verifies cluster certificate. Throws error when not existing.
156
157 @type data: dict
158 @type error_fn: callable
159 @param error_fn: function to call in case of an error
160 @rtype: string
161 @return: Formatted key and certificate
162
163 """
164 cert = data.get(constants.NDS_NODE_DAEMON_CERTIFICATE)
165 if not cert:
166 raise error_fn("Node daemon certificate must be specified")
167
168 return _verify_fn(cert, error_fn)
169
170
171 def VerifyClusterName(data, error_fn, cluster_name_constant,
172 _verify_fn=ssconf.VerifyClusterName):
173 """Verifies cluster name.
174
175 @type data: dict
176
177 """
178 name = data.get(cluster_name_constant)
179 if name:
180 _verify_fn(name)
181 else:
182 raise error_fn("Cluster name must be specified")
183
184 return name
185
186
187 def VerifyHmac(data, error_fn):
188 """Verifies the presence of the hmac secret.
189
190 @type data: dict
191
192 """
193 hmac = data.get(constants.NDS_HMAC)
194 if not hmac:
195 raise error_fn("Hmac key must be provided")
196
197 return hmac
198
199
200 def LoadData(raw, data_check):
201 """Parses and verifies input data.
202
203 @rtype: dict
204
205 """
206 result = None
207 try:
208 result = serializer.LoadAndVerifyJson(raw, data_check)
209 logging.debug("Received data: %s", serializer.DumpJson(result))
210 except Exception as e:
211 logging.warn("Received data is not valid json: %s.", str(raw))
212 raise e
213 return result
214
215
216 def GenerateRootSshKeys(key_type, key_bits, error_fn, _suffix="",
217 _homedir_fn=None):
218 """Generates root's SSH keys for this node.
219
220 """
221 ssh.InitSSHSetup(key_type, key_bits, error_fn=error_fn,
222 _homedir_fn=_homedir_fn, _suffix=_suffix)
223
224
225 def GenerateClientCertificate(
226 data, error_fn, client_cert=pathutils.NODED_CLIENT_CERT_FILE,
227 signing_cert=pathutils.NODED_CERT_FILE):
228 """Regenerates the client certificate of the node.
229
230 @type data: string
231 @param data: the JSON-formated input data
232
233 """
234 if not os.path.exists(signing_cert):
235 raise error_fn("The signing certificate '%s' cannot be found."
236 % signing_cert)
237
238 # TODO: This sets the serial number to the number of seconds
239 # since epoch. This is technically not a correct serial number
240 # (in the way SSL is supposed to be used), but it serves us well
241 # enough for now, as we don't have any infrastructure for keeping
242 # track of the number of signed certificates yet.
243 serial_no = int(time.time())
244
245 # The hostname of the node is provided with the input data.
246 hostname = data.get(constants.NDS_NODE_NAME)
247 if not hostname:
248 raise error_fn("No hostname found.")
249
250 utils.GenerateSignedSslCert(client_cert, serial_no, signing_cert,
251 common_name=hostname)